Как копировать объекты в JavaScript

Введение Очень распространенной задачей в программировании, независимо от языка, является копирование (или клонирование) объекта по значению, в отличие от копирования по ссылке. Разница в том, что при копировании по значению у вас есть два несвязанных объекта с одним и тем же значением или данными. Копирование по ссылке означает, что у вас есть два объекта, которые указывают на одни и те же данные в памяти. Это означает, что если вы, например, манипулируете объектом A, он также будет управлять объектом B, поскольку они оба ссылаются на одни и те же базовые данные. В

Вступление

Очень распространенной задачей в программировании, независимо от языка, является копирование (или клонирование) объекта по значению, в отличие от копирования по ссылке. Разница в том, что при копировании по значению у вас есть два несвязанных объекта с одним и тем же значением или данными. Копирование по ссылке означает, что у вас есть два объекта, которые указывают на одни и те же данные в памяти. Это означает, что если вы, например, манипулируете объектом A, он также будет управлять объектом B, поскольку они оба ссылаются на одни и те же базовые данные.

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

Примечание . Поскольку Node.js - это просто среда выполнения, построенная на движке V8 JavaScript, все методы клонирования, которые я показываю в этой статье, также будут работать для Node.

Сторонние библиотеки

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

Лодаш

Библиотека Lodash предоставляет несколько различных методов для копирования или клонирования объектов в зависимости от вашего варианта использования.

Самый общий метод - это метод clone() , который предоставляет неглубокие копии объектов. Он работает, просто передавая объект в качестве первого аргумента, и копия будет возвращена:

 const _ = require('lodash'); 
 
 let arrays = {first: [1, 2, 3], second: [4, 5, 6]}; 
 let copy = _.clone(arrays); 
 console.log(copy); 

 { first: [ 1, 2, 3 ], second: [ 4, 5, 6 ] } 

Это означает, что объект «верхнего уровня» (или массив, буфер, карта и т. Д.) Клонируется, но любые более глубокие объекты будут скопированы по ссылке. Приведенный ниже код демонстрирует, что first массив в исходном arrays - это тот же объект, что и first массив в объекте copy

 const _ = require('lodash'); 
 
 let arrays = {first: [1, 2, 3], second: [4, 5, 6]}; 
 let copy = _.clone(arrays); 
 console.log(copy.first === arrays.first); 

 true 

Если вы предпочитаете, чтобы все объекты, как мелкие, так и глубокие, копировались, то вместо cloneDeep()

 const _ = require('lodash'); 
 
 let arrays = {first: [1, 2, 3], second: [4, 5, 6]}; 
 let copy = _.cloneDeep(arrays); 
 console.log(copy); 

 { first: [ 1, 2, 3 ], second: [ 4, 5, 6 ] } 

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

Выполнив ту же проверку на равенство, описанную выше, мы видим, что исходный и скопированный массивы больше не равны, поскольку они являются уникальными копиями:

 const _ = require('lodash'); 
 
 let arrays = {first: [1, 2, 3], second: [4, 5, 6]}; 
 let copy = _.cloneDeep(arrays); 
 console.log(copy.first === arrays.first); 

 false 

Lodash предлагает еще несколько методов клонирования, включая cloneWith() и cloneDeepWith() . Оба эти метода принимают еще один параметр, называемый customizer , который представляет собой функцию, используемую для создания скопированного значения.

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

 const _ = require('lodash'); 
 
 let tweet = { 
 username: '@ScottWRobinson', 
 text: 'I didn\'t actually tweet this', 
 created_at: new Date('December 21, 2018'), 
 updated_at: new Date('January 01, 2019'), 
 deleted_at: new Date('February 28, 2019'), 
 }; 
 let tweetCopy = l.cloneDeepWith(tweet, (val) => { 
 if (l.isDate(val)) { 
 return val.getTime(); 
 } 
 }); 
 console.log(tweetCopy); 

 { username: '@ScottWRobinson', 
 text: 'I didn\'t actually tweet this', 
 created_at: 1545372000000, 
 updated_at: 1546322400000, 
 deleted_at: 1551333600000 } 

Как видите, единственными данными, которые были изменены нашим методом, были Date , которые теперь были преобразованы во временные метки Unix.

Нижнее подчеркивание

Метод Underscore clone() работает почти так же, как метод lodash clone() . Он предоставляет только поверхностную копию данного объекта, при этом любые вложенные объекты копируются по ссылке.

Тот же пример, что и раньше, демонстрирует это:

 const _ = require('underscore'); 
 
 let arrays = {first: [1, 2, 3], second: [4, 5, 6]}; 
 let copy = _.clone(arrays); 
 console.log(copy.first === arrays.first); 

 true 

К сожалению, у библиотеки Underscore, похоже, нет никакого метода для обработки глубокого копирования. Вы можете реализовать эту логику самостоятельно (используя некоторую логику, показанную ниже) и по-прежнему использовать clone Underscore для неглубокой копии, или вы можете попробовать одно из других решений в этой статье.

Индивидуальные решения

Как я упоминал ранее, решить эту задачу самому сложно, поскольку существует множество случаев (и сложных крайних случаев), которые необходимо обрабатывать при клонировании объекта в JavaScript. Хотя, если все сделано правильно, вы сможете добавить в свой метод некоторые приятные настройки, которые в противном случае были бы невозможны.

Использование методов JSON

Одно из часто цитируемых решений - просто использовать JSON.stringify и JSON.parse в ваших интересах, например:

 let arrays = {first: [1, 2, 3], second: [4, 5, 6]}; 
 let copy = JSON.parse(JSON.stringify(arrays)); 
 console.log(copy); 

 { first: [ 1, 2, 3 ], second: [ 4, 5, 6 ] } 

Это оставит вас с глубоко скопированным объектом, и это очень хорошо работает для простых объектов, которые легко конвертируются в JSON.

Мы можем снова проверить это, используя ту же проверку, что и выше:

 console.log(copy.first === arrays.first); 

 false 

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

Написание собственного с нуля

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

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

 function clone(thing, opts) { 
 var newObject = {}; 
 if (thing instanceof Array) { 
 return thing.map(function (i) { return clone(i, opts); }); 
 } else if (thing instanceof Date) { 
 return new Date(thing); 
 } else if (thing instanceof RegExp) { 
 return new RegExp(thing); 
 } else if (thing instanceof Function) { 
 return opts && opts.newFns ? new Function('return ' + thing.toString())() : thing; 
 } else if (thing instanceof Object) { 
 Object.keys(thing).forEach(function (key) { newObject[key] = clone(thing[key], opts); }); 
 return newObject; 
 } else if ([ undefined, null ].indexOf(thing) > -1) { 
 return thing; 
 } else { 
 if (thing.constructor.name === 'Symbol') { 
 return Symbol(thing.toString().replace(/^Symbol\(/, '').slice(0, -1)); 
 } 
 return thing.__proto__.constructor(thing); 
 } 
 } 

Эта функция работает при обращении с конкретными случаями , когда это необходимо (например , массивы, регулярными выражения, функции и т.д.), а затем и для всех других типов данных (например , число, строки, булевы и т.д.) По умолчанию этого значения thing «s собственного конструктора скопируйте значение. Если thing сама по себе является объектом, тогда она просто рекурсивно вызывает себя в дочерних атрибутах thing .

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

Заключение

Хотя в теории это просто, на практике копирование объекта в JavaScript совсем не просто. К счастью, есть несколько решений, которые вы можете использовать, например cloneDeep в Lodash или даже встроенные методы JSON И если по какой-либо причине ни один из них не подходит , можно написать свой собственный метод клонирования, если вы его тщательно протестируете.

Удачи, и мы знаем, есть ли у вас какие-либо мысли, идеи или советы в комментариях.

comments powered by Disqus