Копирование значений кажется несколько тривиальным. Тем не менее, почти невозможно найти разработчика, у которого никогда не было проблем с неправильной ссылкой или указателем в случае 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); }
Заключение
Каждый разработчик должен знать о понятиях поверхностного и глубокого копирования и точно знать, когда каждая реализация более уместна.
Оператор распространения довольно прост и мощен, но для вложенных объектов, как мы видели, не подходит. Рассмотрите каждый сценарий и выберите наилучший подход к вашему делу.
Надеюсь, что эта статья поможет понять эти концепции.