Skip to content

Instantly share code, notes, and snippets.

@dSalieri
Last active December 30, 2025 13:53
Show Gist options
  • Select an option

  • Save dSalieri/6755d09257e28f1249e64f9c0ac0e1fe to your computer and use it in GitHub Desktop.

Select an option

Save dSalieri/6755d09257e28f1249e64f9c0ac0e1fe to your computer and use it in GitHub Desktop.
Реализация глубокого копирования объекта

Цель: Добиться копирования объекта в глубину.

Примечания:

  1. Копирование работает с: undefined, null, number, bigint, string, boolean, array, object.
  2. Копирование не работает с: symbol, function. Они переносятся в клонируемый объект как есть (это из-за того как устроены внутри).
  3. Копирование поддерживает другие сложные объекты, но реализация лежит на ваших плечах (внизу есть пример как это сделать).
  4. Поддержка тех типов что не предоставлена, будут пропущены, ключи для них созданы не будут, но есть специальный флаг, который позволит сделать попытку скопировать свойство.
  5. Копирование работает со всеми типами свойств, дескриптор каждого свойства учитывается.

Опции:

{
  prototype: true/false/null, /// копирует ссылку на прототип в копируемый объект, null - установка прототипа в значение null
  compatibleType: true/false, /// данная опция делает попытку скопировать объект, тип которого не поддерживается, если попытка неудачна свойство не будет создано
  descriptorType: "both"/"data"/"accessor", /// от указанного типа зависит копирование свойств конкретного типа
  descriptorProps: { 
    /// если указывается false то это означает что свойство с данным дескриптором и со значением false не учитывается, а значит не копируется; 
    /// если указывается true, тогда не играет роли какое значение имеет свойство true или false - оно будет скопировано
    writable: true/false,
    enumerable: true/false,
    configurable: true/false,
  },
  supplementalTypes: {} /// в объекте указываются специальные методы, которые реализуют специальные объекты
}

Советую поэкспериментировать чтобы четко понять как работают данные флаги.

Как добавить поддержку специального типа (на примере Set, Map и Date):

Нужно передать во второй аргумент объект со свойством supplementalTypes где свойства это имена типов данных, например для типа Set, свойство set, для Map свойство map, для Date свойство date итд. Функция, которая устанавливается такому свойству может быть определена как угодно. Параметры у функции: первый это объект, который клонируется, второй это клонируемая функция из алгоритма.

const cloneObject = {
    set: function (object, cloneF) {
        let result = new Set();
        for (let data of object) {
            if (typeof data === "object" && data !== null) result.add(cloneF(data));
            else result.add(data);
        }
        return result;
    },

    map: function (object, cloneF) {
        let result = new Map();
        for (let [key, value] of object) {
            if (typeof value === "object" && value !== null) result.set(key, cloneF(value));
            else result.set(key, value);
        }
        return result;
    },

    date: function (object, cloneF) {
        return new Date(object.getTime());
    },
};

Тест (не забываем взять из исходника выше реализованные типы данных):

let o1 = {
    name: "Maxim",
    data: {
        key1: "62s34i8g72s",
        key2: "82s3438g72s",
        key3: "72s34m8g72s",
    },
    list: [1, 2, 3, 4, 5, new Set(["look", "at", "that"])],
    specialList: new Set(["one", "two", "three"]),
    date: new Date(),
    map: new Map([[{id:1},"water"],[{id:2},"fire"],[{id:3},"air"]])
};
/// Накидываем циклические ссылки
o1.data.toListCycle = o1.list;
o1.list.toDataCycle = o1.data;
o1.toItself = o1;
/// Клонируем
let cloned = deepClone(o1, {supplementalTypes: cloneObject});
/// Смотрим на результат, можно конечно вручную в консоли потрогать для убедительности
console.log(o1.list[5] === cloned.list[5]); /// expected: false
function deepClone(obj, options) {
options = {
prototype: true,
compatibleType: true,
descriptorType: "both",
...{
...options,
descriptorProps: {
writable: true,
enumerable: true,
configurable: true,
...options?.descriptorProps,
},
supplementalTypes: {
...options?.supplementalTypes,
},
},
};
const empty = Symbol("empty");
const map = new Map();
return (function clone(obj) {
if (!(typeof obj === "object" && obj !== null)) {
if (map.size > 0 || typeof obj === "function") {
/// Возвращается примитив (если он является значением свойства) или функция
return obj;
} else {
/// Защита от примитивов, нельзя для примитива создать клон
throw Error("Can't clone primitive value");
}
} else if (map.has(obj)) {
/// Если существует в карте объект, значит мы встречаем его не в первый раз,
/// это защищает от бесконечной рекурсии
return map.get(obj);
} else if (Object.hasOwnProperty.call(options.supplementalTypes, sortOf(obj))) {
/// Поддержка дополнительных типов (если переданы)
return options.supplementalTypes[sortOf(obj)](obj, clone);
}
const ownKeys = Reflect.ownKeys(obj);
let result = options.compatibleType === true && ownKeys.length > 0 ? new Object() : empty;
switch (sortOf(obj)) {
case "array": {
result = new Array();
break;
}
case "object": {
result = new Object();
break;
}
}
/// Клонируемая оболочка получена и ключей нет
if (result !== empty && ownKeys.length === 0) {
return result;
}
/// Клонируемая оболочка не получена и режим совместимости выключен или ключей нет
if (result === empty && (options.compatibleType === false || ownKeys.length === 0)) {
/// Если это родитель, тогда просто возврат null
if (map.size === 0) return null;
/// В противном случае если это вложенный объект, вернуть empty (клонирование будет пропущено)
return empty;
}
map.set(obj, result);
/// Если есть ключи то начинаем клонирование
for (let key of ownKeys) {
const { descriptor, type } = descriptorWithType(obj, key);
if (
(descriptor.enumerable === false && options.descriptorProps.enumerable === false) ||
(descriptor.writable === false && options.descriptorProps.writable === false) ||
(descriptor.configurable === false && options.descriptorProps.configurable === false)
)
continue;
let cloned = clone(descriptor.value);
if (cloned !== empty) {
if (type === "data" && ["data", "both"].some((v) => v === options.descriptorType)) {
Reflect.defineProperty(result, key, { ...descriptor, value: cloned });
} else if (type === "accessor" && ["accessor", "both"].some((v) => v === options.descriptorType)) {
Reflect.defineProperty(result, key, { ...descriptor });
}
}
}
if (options.prototype === true || options.prototype === null) {
Reflect.setPrototypeOf(result, options.prototype === null ? null : Reflect.getPrototypeOf(obj));
}
return result;
})(obj);
function sortOf(arg) {
return Object.prototype.toString.call(arg).slice(8, -1).toLowerCase();
}
function descriptorWithType(obj, prop) {
const desc = Reflect.getOwnPropertyDescriptor(obj, prop);
const result = { descriptor: desc, type: undefined };
if (["writable", "value"].some((v) => Reflect.has(desc, v))) result.type = "data";
else if (["get", "set"].some((v) => Reflect.has(desc, v))) result.type = "accessor";
return result;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment