При работе с JavaScript мы часто сталкиваемся со сценариями, в которых нам нужно воспроизвести структуру и данные объекта. В этих ситуациях мы можем либо создать поверхностную, либо глубокую копию объекта, в зависимости от требований нашего приложения. Однако понимание разницы между этими двумя типами операций копирования жизненно важно, чтобы избежать непреднамеренных побочных эффектов и ошибок.

Неглубокое копирование

Неглубокая копия дублирует структуру исходного объекта, и любые изменения, внесенные в скопированный объект, также повлияют на исходный объект, но только для вложенных объектов. Если свойства объекта являются примитивными типами данных (например, число, строка, логическое значение), их изменения не повлияют на исходный объект.

let originalObject = {
    a: 1,
    b: [1, 2, 3],
    c: { d: 4, e: 5 }
};

let shallowCopy = Object.assign({}, originalObject);

Здесь shallowCopy — неглубокая копия originalObject. Если вы измените shallowCopy.a, это не повлияет на originalObject.a, потому что это примитивный тип данных. Но если вы измените shallowCopy.b или shallowCopy.c, это повлияет на originalObject.b и originalObject.c, потому что это объекты (массив также является типом объекта), и они являются общими для исходного и скопированного объекта.

shallowCopy.a = 100; // Change primitive data type
shallowCopy.b.push(4); // Change array
shallowCopy.c.d = 400; // Change object

console.log(originalObject.a); // Output: 1
console.log(originalObject.b); // Output: [1, 2, 3, 4]
console.log(originalObject.c.d); // Output: 400

В этом случае неглубокой копии недостаточно, если вы хотите получить полную независимость между скопированным объектом и исходным.

Глубокое копирование

Глубокая копия дублирует не только структуру исходного объекта, но и содержащиеся в нем данные. Это означает, что изменения, внесенные в скопированный объект, не повлияют на исходный объект, даже для вложенных объектов.

Чтобы создать глубокую копию, мы обычно используем методы JSON JSON.stringify и JSON.parse.

let originalObject = {
    a: 1,
    b: [1, 2, 3],
    c: { d: 4, e: 5 }
};

let deepCopy = JSON.parse(JSON.stringify(originalObject));

Теперь любые изменения в deepCopy не повлияют на originalObject даже для вложенных объектов.

deepCopy.a = 100; // Change primitive data type
deepCopy.b.push(4); // Change array
deepCopy.c.d = 400; // Change object

console.log(originalObject.a); // Output: 1
console.log(originalObject.b); // Output: [1, 2, 3]
console.log(originalObject.c.d); // Output: 4

Пограничные случаи и ограничения

Хотя показанные выше методы хорошо работают во многих случаях, существуют некоторые пограничные случаи и ограничения, о которых вам следует знать.

1. Циклические объекты. Объекты JavaScript могут ссылаться сами на себя, создавая циклическую структуру. Методы JSON, используемые для глубокого копирования, не могут обрабатывать циклические объекты и выдают ошибку.

let cyclicObject = {};
cyclicObject.self = cyclicObject;

let deepCopy = JSON.parse(JSON.stringify(cyclicObject)); // Throws an error

2. Функции и специальные типы. Методы JSON также не могут копировать функции или специальные типы (например, Date, RegExp, Map, Set и т. д.), поскольку они не являются частью стандарта JSON. Эти свойства будут потеряны в глубокой копии.

let originalObject = {
    a: () => console.log('Hello'),
    b: new Date(),
    c: /test/g
};

let deepCopy = JSON.parse(JSON.stringify(originalObject));

console.log(deepCopy.a); // Output: undefined
console.log(deepCopy.b); // Output: String, not a Date object
console.log(deepCopy.c); // Output: undefined

Функции глубокого копирования и специальные типы в JavaScript нетривиальны из-за их уникальной природы.

Функции

Функции в JavaScript имеют контекстное значение this и могут иметь добавленные к ним свойства. Таким образом, простым решением было бы создать новую функцию и скопировать все свойства исходной функции в новую.

Однако это не настоящая глубокая копия, поскольку исходная функция и скопированная функция по-прежнему имеют общую область закрытия.

Вот функция для этого

function deepCopyFunction(func) {
    var funcStr = func.toString();
    if(func.prototype) {
        var paramArr = funcStr.match(/\((.*?)\)/)[1].split(', ');
        var funcBody = funcStr.match(/\{([\s\S]*)\}/)[1];
        var funcCopy = new Function(...paramArr, funcBody);
        funcCopy.prototype = Object.assign({}, func.prototype);
        return funcCopy;
    } else {
        return eval(funcStr);
    }
}

Специальные типы

Глубокое копирование специальных типов, таких как Date, RegExp, Map, Set и т. д., требует особого отношения. Лучший способ справиться с ними - в каждом конкретном случае.

Вот пример функции, которая глубоко копирует некоторые специальные типы.

function deepCopySpecialTypes(obj) {
    if (obj instanceof Date) {
        return new Date(obj.valueOf());
    }
    if (obj instanceof RegExp) {
        var pattern = obj.valueOf().toString();
        var flags = pattern.substring(pattern.lastIndexOf("/") + 1);
        var realPattern = pattern.substring(1, pattern.lastIndexOf("/"));
        return new RegExp(realPattern, flags);
    }
    if (obj instanceof Map) {
        var mapCopy = new Map();
        obj.forEach((v, k) => {
            mapCopy.set(k, v);
        });
        return mapCopy;
    }
    if (obj instanceof Set) {
        var setCopy = new Set();
        obj.forEach((v) => {
            setCopy.add(v);
        });
        return setCopy;
    }
    // add more if needed
}

Затем вы можете создать более полную функцию глубокого копирования, которая использует эти функции, а также проверяет массивы и объекты для их рекурсивного копирования.

function deepCopy(obj) {
    if (obj instanceof Function) {
        return deepCopyFunction(obj);
    }
    if (obj instanceof Date || obj instanceof RegExp || obj instanceof Map || obj instanceof Set) {
        return deepCopySpecialTypes(obj);
    }
    if (obj instanceof Array) {
        return obj.map(deepCopy);
    }
    if (obj instanceof Object) {
        var copy = {};
        Object.keys(obj).forEach(key => {
            copy[key] = deepCopy(obj[key]);
        });
        return copy;
    }
    return obj;
}

Помните, что эта функция глубокого копирования все еще имеет ограничения. Например, он не может обрабатывать циклические объекты. В зависимости от потребностей вашего приложения вам может понадобиться библиотека, обеспечивающая более надежную функциональность глубокого копирования. Такие библиотеки, как Lodash и Rambda, имеют cloneDeep функций, которые обрабатывают множество крайних случаев.

Наконец, вы должны знать о последствиях для производительности. Глубокое копирование может быть значительно медленнее поверхностного, особенно для больших объектов, из-за необходимости рекурсивно копировать каждое свойство.

Понимание концепций глубокого и поверхностного копирования имеет решающее значение при работе с JavaScript. Выбор между поверхностной и глубокой копией зависит от требований вашего приложения, и понимание разницы может помочь вам предотвратить неожиданные побочные эффекты и ошибки. Всегда учитывайте ограничения и потенциальное влияние на производительность при реализации этих операций, особенно при работе со сложными объектами или большими наборами данных.

Если вам нравится моя работа, нажмите на 👏, подпишитесь на меня, чтобы узнать больше, и купите мне кофе, если вы чувствуете себя очень щедрым.

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .