Копирование значений кажется несколько тривиальным. Тем не менее, почти невозможно найти разработчика, у которого никогда не было проблем с неправильной ссылкой или указателем в случае C-подобных языков. В этой статье я сосредоточусь на том, как копировать переменные/значения в JavaScript.

Примитивные и эталонные значения

Примитивные значения

В JavaScript примитив (примитивное значение, примитивный тип данных) — это данные, которые не являются объектом и не имеют методов. Они следующие:

  • Нить
  • Число
  • BigInt
  • логический
  • неопределенный
  • Символ
  • null (что действительно является особым случаем для каждого объекта)

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

let example = '0';
let example_copy = example;
example = '1';
console.log('example');      // '1'
console.log('example_copy'); // '0'

Справочные значения

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

Они передаются по ссылке, и перед их передачей не делается никаких копий. Эта статья посвящена этим и различным способам их копирования.

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

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

Давайте создадим объект корзины, указав, что у нас есть 1 клавиатура и 2 монитора в качестве значений по умолчанию, и попытаемся скопировать его.

let basket = { keyboard: 1, monitor: 2 };
let basketShallow = basket;
basket.keyboard = 3;
console.log(basket);        // { keyboard: 3, monitor: 2 }
console.log(basketShallow); // { keyboard: 3, monitor: 2 }

Обратите внимание, что значение Basket_shallow.keyboard изменилось. Это произошло потому, что значение было скопировано ссылкой, и изменение в одном месте изменит все переменные, использующие эту ссылку на объект.

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

Глубокое копирование означает не передачу элемента по ссылке, а передачу фактических значений.

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

Вариант 1: Object.assign()

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

let basket = { keyboard: 1, monitor: 2 };
let basketDeep = Object.assign({}, basket)
basket.keyboard = 3;
console.log(basket);     // { keyboard: 3, monitor: 2 }
console.log(basketDeep); // { keyboard: 1, monitor: 2 }

Вариант 2: Оператор спреда

Представленный в ES6, он копирует свои собственные перечисляемые свойства из предоставленного объекта в новый объект, и это коротко и просто. «Распространяет» все значения в новый объект. Вы можете использовать его следующим образом

let basket = { keyboard: 1, monitor: 2 };
let basketDeep = { ...basket };
basket.keyboard = 3;
console.log(basket);     // { keyboard: 3, monitor: 2 }
console.log(basketDeep); // { keyboard: 1, monitor: 2 }

Примечание: если вы используете массив, оператор расширения следует использовать с квадратной скобкой, например. […массив].

Вложенная глубокая копия

И Object.assign(), и оператор расширения делают глубокие копии данных, если данные не являются вложенными.

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

let basket = { keyboard: { quantity: 1 } , monitor: { quantity: 2 } };
let basketDeep = { ...basket };
basket.keyboard.quantity = 3;
console.log(basket); 
// { keyboard: { quantity: 3}, monitor: { quantity: 2 } }
console.log(basketDeep); 
// { keyboard: { quantity: 3}, monitor: { quantity: 2 } }

Обратите внимание на предыдущий пример. keyboard.quantity была затронута в обеих переменных, потому что оператор распространения будет копировать значения по ссылке, когда переменные имеют глубину более 1 измерения.

JSON.parse(JSON.stringify())

Самое простое решение этой проблемы — закодировать объект в строку JSON, а затем снова встроить его в объект. См. пример ниже:

let basket = {keyboard: { quantity: 1 } , monitor: { quantity: 2 }};
let basketDeep = JSON.parse( JSON.stringify(basket) );
basket.keyboard.quantity = 3;
console.log(basket); 
// { keyboard: { quantity: 3}, monitor: { quantity: 2 } }
console.log(basketDeep);
// { keyboard: { quantity: 1}, monitor: { quantity: 2 } }

Функция динамического вложенного глубокого копирования

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

Имея это в виду, ниже представлена ​​динамическая реализация глубокой копии объекта.

function deepCopy(objToCopy) {
    let res = {} ;
    for (const property in objToCopy) {
        if (isPrimitiveValue(objToCopy[property])) {
            res[property] = objToCopy[property]; 
        } else {
        res[property] = deepCopy(objToCopy[property]);
        }
    }
    return res;
}
function isPrimitiveValue(value) {
    return !(typeof value === 'object' && value !== null);
}

Заключение

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

Оператор распространения довольно прост и мощен, но для вложенных объектов, как мы видели, не подходит. Рассмотрите каждый сценарий и выберите наилучший подход к вашему делу.

Надеюсь, что эта статья поможет понять эти концепции.