- Независимое глубокое клонирование объектов в JavaScript
- Object.assign()
- Try it
- Syntax
- Parameters
- Return value
- Description
- Examples
- Cloning an object
- Warning for Deep Clone
- Merging objects
- Merging objects with same properties
- Copying symbol-typed properties
- Properties on the prototype chain and non-enumerable properties cannot be copied
- Primitives will be wrapped to objects
- Exceptions will interrupt the ongoing copying task
- Copying accessors
- Specifications
- Browser compatibility
- See also
Независимое глубокое клонирование объектов в JavaScript
В любом языке программирования есть типы данных, которыми программисты описывают субъекты, чтобы в дальнейшем работать и, в случае необходимости, обрабатывать их. Язык JavaScript не исключения, он имеет примитивные ( Number , String , Boolean , Symbol и др) и ссылочные ( Array , Object , Function , Maps , Sets и др) типы данных. Нужно отметить, что примитивные типы данных, являются иммутабельными — их значения не могут быть модифицированы, а только перезаписаны новым полным значением, а вот с ссылочными типами данных все наоборот. Например, объявим переменные типа Number и Object :
Мы не можем модифицировать переменную num , нам лишь можно перезаписать ее значение, а вот переменную obj мы модифицировать можем:
Как видим, в первом случае мы перезаписали значение переменной, а во втором расширили объект. Отсюда делаем вывод, что примитивные типы данных невозможно расширять, а с ссылочными типами данных мы можем это делать, даже с модификатором const .
Последние можно заморозить, к примеру, с помощью Object.freeze(obj) , но данная тема выходит за рамки статьи (ссылки для любознательных Object.defineProperty, защита объекта от изменения).
Как типы данных передаются в функции в JavaScript? Каждый js-программист наверняка без труда ответит на этот вопрос, но все же скажем: примитивные типы данных передаются в функцию всегда только по значению, а ссылочные всегда только по ссылке. И вот тут с последними, в некоторых ситуациях, возникают проблемы. Давайте рассмотрим пример:
const arr = [0, 1, 2, 3, 4, 5]; console.log("Array: ", arr); // output: Array: [0, 1, 2, 3, 4, 5]
В данном случае, мы просто объявили массив чисел и вывели его в консоли. Теперь передадим его в функцию, которая возвращает новый массив, но с добавлением некоторого значения во втором аргументе, к концу нового массива:
const arr = [0, 1, 2, 3, 4, 5]; console.log("Old array: ", arr); // "Old array: " [0, 1, 2, 3, 4, 5] const newArr = insertValToArr(arr, 15); console.log("New array: ", newArr); // output: "New array: " [0, 1, 2, 3, 4, 5, 15] console.log("Old array: ", arr); // output: "Old array: " [0, 1, 2, 3, 4, 5, 15] function insertValToArr(arr, val)
Как видим из консольных выводов, поменялся не только новый массив, но и старый. Это произошло потому что в функции insertValToArr мы просто присвоили один массив к другому const newArr = arr , а следовательно создали ссылку на существующий массив и когда попытались модифицировать уже новый массив, он сослался на область памяти старого массива и, грубо говоря, изменил ее. А так как оба массива ссылаются на одну и ту же область памяти, они и будут иметь одно и тоже значения. Давайте изменим нашу функцию, чтобы она не могла изменить старый массив:
const arr = [0, 1, 2, 3, 4, 5]; const newArr = insertValToArr(arr, 15); console.log("New array: ", newArr); // output: "New array: " [0, 1, 2, 3, 4, 5, 15] console.log("Old array: ", arr); // output: "Old array: " [0, 1, 2, 3, 4, 5] function insertValToArr(arr, val) < const newArr = []; arr.forEach((value, ind) =>< newArr[ind] = value>); newArr.push(val); return newArr; >
Старый массив не изменился, потому что мы получили каждый его элемент и по отдельности присвоили значения элемента к элементам нового массива. Теперь последний имеет отдельную область памяти и если его изменить, то старого массива это никак не коснется. Но все это простые примеры и в реальных программах, скорее всего будут встречаться не только одномерные массивы, а и двумерные, реже трехмерные, еще реже четырехмерные. Преимущественно они встречаются в виде ассоциативных массивов (хеш-таблицы). В JavaScript чаще всего это объекты.
Давайте рассмотрим стандартные способы копирования объектов, которые предоставляет JavaScript — Object.assign() используется для копирования значений всех собственных перечисляемых свойств из одного или более исходных объектов в целевой объект. После копирования он возвращает целевой объект. Рассмотрим его:
const obj = < a: 1 >; const newObj = Object.assign(<>, obj); console.log(newObj); // output: < a: 1, b: 2 >console.log(obj); // output:
И снова старая проблема, мы ссылаемся на одну и ту же область памяти, что приводит к модификации двух объектов сразу — изменяя один будет изменяться и другой. Что же делать, если нам нужно получить копию сложного объекта (с множественным разветвлением) и при этом, изменяя объект, не модифицировать другой? Ответить на этот вопрос и есть цель данной статьи. Дальше мы рассмотрим, как написать собственный метод глубокого клонирования (копирования) объектов любого ветвления. Преступим к написанию кода.
1 шаг: объявляем и инициализируем объект Z , а также делаем консольный вывод для сравнения до и после клонирования:
const Z = < a: 5, b: < g: 8, y: 9, t: < q: 48 >>, x: 47, l: < f: 85, p: < u: 89, m: 7 >, s: 71 >, r: < h: 9, a: 'test', s: 'test2' >>; console.log('Z object before cloning: ', Z);
2 шаг: присваиваем объект Z объекту refToZ для того, чтобы показать разницу между обычным присваиванием и глубоким клонированием:
3 шаг: присваиваем объект Z объекту Y с помощью функции deepClone и добавим новое свойство к объекту Y . После чего выведем эти два объекта в консоли:
const Y = deepClone(Z); function deepClone(obj) < const clObj = <>; for(const i in obj) < if (obj[i] instanceof Object) < clObj[i] = deepClone(obj[i]); continue; >clObj[i] = obj[i]; > return clObj; > Y.addnlProp = < fd: 45 >; console.log('Z object after cloning: ', Z); console.log('Y object: ', Y);
В консоли мы отчетливо видим, что изменяя объект Y , добавив новое свойство, мы не изменяем объект Z и последний не будет иметь свойство addnlProp в своем теле.
4 шаг: изменим свойство x , которое есть в теле объектах Z и Y и снова выведем оба объекта в консоль:
Y.x = 76; console.log('Y object: ', Y); console.log('Z object: ', Z);
Изменяя одно и то же свойство в объекте Y , мы не затрагиваем свойство в теле Z .
5 шаг: на последнем шаге мы просто для сравнения к объекту refToZ добавим свойство addToZ со значением 100 и выведем все три объекта в консоль:
refToZ.addToZ = 100; console.log('refToZ object: ', refToZ); console.log('Z object: ', Z); console.log('Y object: ', Y);
Изменив объект refToZ мы изменили и Z , однако Y не затронули. Отсюда сделаем вывод, что наша функция создает независимый новый объект со свойствами и их значениями из существующего объекта (код реализации функции deepClone можно найти на CodePen).
Немного остановимся над реализацией данной функции. Последняя находит, любую вложенность объекта, даже не зная ее. Как она это делает? Все дело в том, что в данном случае мы применяем известный алгоритм для графов — поиск в глубину. Объект — граф, который имеет одну или множество веток, которые в свою очередь могут иметь свои ветки и тд. Чтобы нам найти все нам нужно зайти в каждую ветку и продвигаться в ее глубь, таким образом мы найдем каждый узел в графе и получим его значения. Поиск в глубину можно реализовать 2 способами: рекурсией и с помощью цикла. Второй может оказаться быстрее, так как не будет заполнять стек вызовов, что в свою очередь делает рекурсия. В нашей реализации функции deepClone мы применили комбинацию рекурсию с циклом. Если хотите почитать книги об алгоритмах, то советую начать Адитъя Бхаргава «Грокаем алгоритмы» или более углубленное Томас Кормен «Алгоритмы: построение и анализ».
Подведем итоги, мы вспомнили об типах данных в JavaScript и как они передаются в функции. Рассмотрели простой пример независимого клонирования простого одномерного массива. Рассмотрели одну из стандартных реализации языка для копирования объектов и в итоге написали маленький (по размеру) функцию для независимого глубокого клонирования сложных объектов. Подобная функция может найти свое применения как на стороне сервера (Node js), что более вероятно, так и на клиенте. Надеюсь данная статья была полезна для вас. До новых встреч.
Object.assign()
The Object.assign() static method copies all enumerable own properties from one or more source objects to a target object. It returns the modified target object.
Try it
Syntax
Parameters
The target object — what to apply the sources’ properties to, which is returned after it is modified.
The source object(s) — objects containing the properties you want to apply.
Return value
Description
Properties in the target object are overwritten by properties in the sources if they have the same key. Later sources’ properties overwrite earlier ones.
The Object.assign() method only copies enumerable and own properties from a source object to a target object. It uses [[Get]] on the source and [[Set]] on the target, so it will invoke getters and setters. Therefore it assigns properties, versus copying or defining new properties. This may make it unsuitable for merging new properties into a prototype if the merge sources contain getters.
For copying property definitions (including their enumerability) into prototypes, use Object.getOwnPropertyDescriptor() and Object.defineProperty() instead.
Both String and Symbol properties are copied.
In case of an error, for example if a property is non-writable, a TypeError is raised, and the target object is changed if any properties are added before the error is raised.
Note: Object.assign() does not throw on null or undefined sources.
Examples
Cloning an object
const obj = a: 1 >; const copy = Object.assign(>, obj); console.log(copy); //
Warning for Deep Clone
For deep cloning, we need to use alternatives like structuredClone() , because Object.assign() copies property values.
If the source value is a reference to an object, it only copies the reference value.
const obj1 = a: 0, b: c: 0 > >; const obj2 = Object.assign(>, obj1); console.log(obj2); // < a: 0, b: < c: 0 >> obj1.a = 1; console.log(obj1); // < a: 1, b: < c: 0 >> console.log(obj2); // < a: 0, b: < c: 0 >> obj2.a = 2; console.log(obj1); // < a: 1, b: < c: 0 >> console.log(obj2); // < a: 2, b: < c: 0 >> obj2.b.c = 3; console.log(obj1); // < a: 1, b: < c: 3 >> console.log(obj2); // < a: 2, b: < c: 3 >> // Deep Clone const obj3 = a: 0, b: c: 0 > >; const obj4 = structuredClone(obj3); obj3.a = 4; obj3.b.c = 4; console.log(obj4); // < a: 0, b: < c: 0 >>
Merging objects
const o1 = a: 1 >; const o2 = b: 2 >; const o3 = c: 3 >; const obj = Object.assign(o1, o2, o3); console.log(obj); // console.log(o1); // < a: 1, b: 2, c: 3 >, target object itself is changed.
Merging objects with same properties
const o1 = a: 1, b: 1, c: 1 >; const o2 = b: 2, c: 2 >; const o3 = c: 3 >; const obj = Object.assign(>, o1, o2, o3); console.log(obj); //
The properties are overwritten by other objects that have the same properties later in the parameters order.
Copying symbol-typed properties
const o1 = a: 1 >; const o2 = [Symbol("foo")]: 2 >; const obj = Object.assign(>, o1, o2); console.log(obj); // < a : 1, [Symbol("foo")]: 2 >(cf. bug 1207182 on Firefox) Object.getOwnPropertySymbols(obj); // [Symbol(foo)]
Properties on the prototype chain and non-enumerable properties cannot be copied
const obj = Object.create( // foo is on obj's prototype chain. foo: 1 >, bar: value: 2, // bar is a non-enumerable property. >, baz: value: 3, enumerable: true, // baz is an own enumerable property. >, >, ); const copy = Object.assign(>, obj); console.log(copy); //
Primitives will be wrapped to objects
const v1 = "abc"; const v2 = true; const v3 = 10; const v4 = Symbol("foo"); const obj = Object.assign(>, v1, null, v2, undefined, v3, v4); // Primitives will be wrapped, null and undefined will be ignored. // Note, only string wrappers can have own enumerable properties. console.log(obj); //
Exceptions will interrupt the ongoing copying task
const target = Object.defineProperty(>, "foo", value: 1, writable: false, >); // target.foo is a read-only property Object.assign(target, bar: 2 >, foo2: 3, foo: 3, foo3: 3 >, baz: 4 >); // TypeError: "foo" is read-only // The Exception is thrown when assigning target.foo console.log(target.bar); // 2, the first source was copied successfully. console.log(target.foo2); // 3, the first property of the second source was copied successfully. console.log(target.foo); // 1, exception is thrown here. console.log(target.foo3); // undefined, assign method has finished, foo3 will not be copied. console.log(target.baz); // undefined, the third source will not be copied either.
Copying accessors
const obj = foo: 1, get bar() return 2; >, >; let copy = Object.assign(>, obj); console.log(copy); // // The value of copy.bar is obj.bar's getter's return value. // This is an assign function that copies full descriptors function completeAssign(target, . sources) sources.forEach((source) => const descriptors = Object.keys(source).reduce((descriptors, key) => descriptors[key] = Object.getOwnPropertyDescriptor(source, key); return descriptors; >, >); // By default, Object.assign copies enumerable Symbols, too Object.getOwnPropertySymbols(source).forEach((sym) => const descriptor = Object.getOwnPropertyDescriptor(source, sym); if (descriptor.enumerable) descriptors[sym] = descriptor; > >); Object.defineProperties(target, descriptors); >); return target; > copy = completeAssign(>, obj); console.log(copy); // < foo:1, get bar() < return 2 >>
Specifications
Browser compatibility
BCD tables only load in the browser