Skip to content

Instantly share code, notes, and snippets.

@dSalieri
Last active October 31, 2023 18:18
Show Gist options
  • Save dSalieri/6755d09257e28f1249e64f9c0ac0e1fe to your computer and use it in GitHub Desktop.
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, who) {
if (!(typeof obj === "object" && obj !== null)) {
if (who === "child" || typeof obj === "function") return obj;
else throw Error("Can't clone primitive value");
} else if (map.has(obj)) {
return map.get(obj);
}
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;
}
default: {
if (Object.hasOwnProperty.call(options.supplementalTypes, sortOf(obj))) {
return options.supplementalTypes[sortOf(obj)](obj, clone);
}
}
}
map.set(obj, result);
if (result === empty && options.compatibleType === false) {
if (who === "parent") return null;
return empty;
}
if (ownKeys.length === 0) {
if (result !== empty) return result;
if (who === "child") return empty;
return null;
}
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, "child");
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, "parent");
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