Для того, чтобы получить копию объекта, как сложного типа данных, придется приложить некоторое количество усилий, т.к. в отличие от примитивных типов Number, String, Boolean, которые чаще всего являются значениями свойств объекта, обычное присваивание не создаст нам второй объект с такими же точно данными. Давайте посмотрим, почему.
Способ 1. Создание дополнительной ссылки на объект
Начнем с примитивов, чтобы была видна разница.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let num1 = 90; let num2 = num1; console.log(num1, num2, num1 === num2); //90 90 true num1 = 120; num2 = -8; console.log(num1, num2,num1 === num2); //120 -8 false let str1 = 'Hello'; let str2 = str1; console.log(str1, str2, str1 === str2); //Hello Hello true str1 = 'HTML/CSS'; str2 = 'JavaScript'; console.log(str1, str2, str1 === str2); // HTML/CSS JavaScript false |
Из примера видно, что каждая из числовых или строковых переменных, которые имели при присвоении (копировании) одно и то же значение, впоследствии изменяется абсолютно автономно. Обратите внимание, что, если значения этих типов равны, то равны и сами переменные.
Давайте теперь то же самое проделаем с объектом. То есть мы просто присвоим объект в другую переменную. А затем изменим одно свойство в исходном объекте и в его клоне, или копии.
1 2 3 4 5 6 7 8 9 10 11 |
let myAnimal = { type: 'собака', name: 'Рекс', age: 7 } let animal1 = myAnimal; console.log(myAnimal === animal1); //true animal1.name = 'Пират'; myAnimal.age = 5; console.log(myAnimal, animal1); //Object { type: "собака", name: "Пират", age: 5 } Object { type: "собака", name: "Пират", age: 5 } |
Как видно из кода, изменились свойства обоих объектов вне зависимости от того, для какой из переменных изменялось значение свойства.
Это указывает на то, что обе переменные ведут на один и тот же адрес в памяти компьютера, и при присвоении происходит передача объекта по ссылке. То есть 2 переменные ссылаются на одно и то же место в оперативной памяти, занятое нашим объектом, поэтому нет ничего удивительного, что он изменяется и с помощью исходной переменной, и с помощью копии.
Поэтому нельзя назвать копией вторую переменную. Это больше похоже на телефонную связь с одним абонентом. Номер его телефона может храниться в любом количестве других девайсов, но дозваниваться (ссылаться в случае объектов в JS) с них будут всегда на одно и то же устройство с этим номером. Поэтому слова "способ 1" в заголовке перечеркнуты, т.к. этот процесс не является копированием с получением 2-х разных объектов с идентичными свойствами и методами.
Есть еще один нюанс, касающийся сравнения объектов. Когда мы пишем строки вида obj1===obj2
, то true
вернется только в случае, рассмотренном выше, когда переменные ссылаются на один и тот же объект Если же объекты являются идентичными, но созданы вручную при написании кода или любым из перечисленных ниже способов, то сравнение на равенство вернет false
, т.к. каждый объект имеет свой собственный адрес в памяти компьютера. И они не совпадают!
Способ 2. Быстрое копирование одноуровневого объекта оператором spread
В том случае, когда у нас есть объект, содержащий только свойства (ключи), значениями которых являются примитивные типы данных (не объекты), то можно использовать простой способ копирования этих объектов, основанный на spread-операторе (оператор расширения):
1 2 3 4 5 6 7 8 9 10 |
const circle1 = {x: 20, y: 135, radius: 40, color: 'yellow'}; const cloneCircle1 = {...circle1 }; console.log(circle1 === cloneCircle1 ); //false // проверяем, будут ли объекты изменяться одинаково circle1.radius = 50; cloneCircle1.color = 'brown'; console.log(circle1); // Object { x: 20, y: 135, radius: 50, color: "yellow" } console.log(cloneCircle1); //Object { x: 20, y: 135, radius: 40, color: "brown" } |
Такой способ называется поверхностным копированием объектов. При этом он отлично справился с копированием свойств из одного объекта в другой. Когда мы изменили свойство radius
для исходного объекта и свойство color
для копии, изменения коснулись только тех объектов, которые изменялись.
Способ 3. Поверхностное копирование с помощью Object.assign()
Метод Object.assign() позволяет скопировать свойства одного объекта в другой. Этот метод предполагает, что мы передаем первым аргументом пустой объект, а вторым - тот объект, который копируем, или несколько таких объектов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const animal = { type: 'кот', name: 'Мурзик', age: 2, say: function(){ console.log('Мяу'); }, toys: ['мяч', 'мышка'] } const animal2 = Object.assign({}, animal); console.log(animal === animal2); //false //изменяем свойство name для animal2 animal2.name = "Фиона"; console.log(animal.name, animal2.name); //Мурзик Фиона //Методы совпадают console.log(animal.say === animal2.say); //true //если заменять элементы массива, то значения меняются в обоих объектах //и в исходном, и в копии animal2.toys[0] = 'Погремушка'; console.log(animal.toys, animal2.toys, animal.toys === animal2.toys); //Array [ "Погремушка", "мышка" ] Array [ "Погремушка", "мышка" ] true |
Этот способ также дает нам поверхностное копирование объектов, как и предыдущий в силу того, что свойства копируются, как отдельные пары вида "ключ: значение", а методы и свойства, значения которых являются другими объектами (массив в примере выше) - по ссылке.
Способ 4. Глубокое копирование объектов с помощью цикла for...in или Object.keys()
Допустим, нам нужно сделать точную копию - клон объекта, в котором одно или 2 свойства тоже являются объектами + есть один метод или два. Ни один из предыдущих способов не годится, т.к. первый - это не клонирование, а создание ссылки на уже существующий объект, второй просто не подходит, т.к. объект сложный, а в третьем копируются только свойства, но методы и свойства-объекты передаются как ссылки, т.е. как в способе 1.
Используем цикл for...in
В этом случае можно использовать цикл for...in
для прохода по всем собственным свойствам и методам объекта (без свойств и методов прототипа), что реализуется использованием метода hasOwnProperty(key)
. Проще всего делать это с помощью функции, которую можно использовать столько раз, сколько вам нужно. В том случае, если свойство само является объектом (проверяем это с помощью строки typeof obj[key] === 'object'
), то мы рекурсивно вызываем нашу функцию cloneSomeObject()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
let cloneSomeObject = (obj) => { const clone = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { if (typeof obj[key] === 'object') clone[key] = cloneSomeObject(obj[key]); else clone[key] = obj[key]; } } return clone; } let john = { firstName: 'John', lastName: 'Daniels', age: 34, address: { street: '2nd Avenu', appartment: '27', city: 'New York' }, showInfo: function(){ console.log(`${this.firstName} ${this.lastName} from ${this.address.city}`) } } let cloneJohn = cloneSomeObject(john); console.log(cloneJohn === john); //false console.log(john, cloneJohn); |
Результатом вывода в консоль будет полная копия существующего объекта, включая вложенный объект.
Сделаем проверку: изменим свойства в исходном и скопированном объекте и выведем, что получилось, используя метод showInfo()
:
1 2 3 4 |
john.address.city = 'London'; cloneJohn.lastName = 'Greenwood'; john.showInfo(); //John Daniels from London cloneJohn.showInfo(); //John Greenwood from New York |
Результаты отличаются, т.е. каждый из объектов является независимым от другого, причем вложенный объект скопировался также, как независимый от исходного.
Используем метод Object.keys()
Очень похожий вариант глубокого копирования объекта мы можем получить, используя метод Object.keys(). Здесь также мы проходим по свойствам объектов методом map()
массивов. В каждой итерации map()
проверяем свойства по одному на содержание в нем какого-либо объекта. Если да, то выполняется рекурсивный вызов той же самой функции deepCloneObject
, и значением текущего свойства будет вложенный объект.
1 2 3 4 5 6 7 8 9 |
const deepCloneObject = (obj) => { let cloneObject = {}; Object.keys(obj).map(key => { if(typeof obj[key] === 'object'){ cloneObject[key] = deepCloneObject(obj[key]); } else{ cloneObject[key] = obj[key]; } }); return cloneObject ; } |
Снова проверяем копии на индивидуальность:
1 2 3 4 5 6 7 |
let cloneJohn2 = deepCloneObject(john); console.log(john, cloneJohn2); //свойства и методы совпадают console.log(cloneJohn === john); //false john.firstName = 'Billy'; cloneJohn2.address.city = 'Paris'; john.showInfo(); //Billy Daniels from New York cloneJohn2.showInfo(); //John Daniels from Paris |
Как видно из комментариев, вывод данных в функции различается для исходного объекта и копии.
Подводные камни при копировании дат и массивов
Код 2-х функций выше предполагает, что мы копируем объекты с вложенными объектами. Однако вложенными объектами также могут быть массивы (экземпляры объекта Array) или даты (экземпляры объекта Date). И вот тут мы можем получить совсем не то, что планировали.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
let lisa = { firstName: 'Lisa', lastName: 'Darrel', age: 22, address: { street: 'Fulton Street ', appartment: '12', city: 'New York' }, skills: ['HTML/CSS', 'JavaScript', 'PHP'], arrivalDate: new Date(), showInfo: function () { console.log(`${this.firstName} ${this.lastName} from ${this.address.city}`) }, showArrivalInfo: function () { console.log(`${this.firstName} ${this.lastName} came from ${this.address.city} ${this.arrivalDate.toLocaleDateString()}.`) } } let lisaCopy = deepCloneObject(lisa); console.log(lisa, lisaCopy); |
На скриншоте ниже можно увидеть, что в объекте-копии массив превратился в объект типа {}
, а объект Date не имеет значения, а только лишь имеет прототип в виде стандартного класса Object:
При проверке объектов путем изменения скопированного массива методом pop()
и вывода метода showArrivalInfo()
, использующего объект Date, мы получаем ошибки:
1 2 3 4 5 6 7 8 9 10 |
//проверяем копирование массива как свойства объекта lisaCopy.skills.pop(); // Ошибка !!! Uncaught TypeError: lisaCopy.skills.pop is not a function lisaCopy.skills.push('React', 'Angular'); console.log(lisa.skills); console.log(lisaCopy.skills); //Проверяем копирование объекта Date lisa.showArrivalInfo(); //Lisa Darrel came from New York 04.09.2021 lisaCopy.showArrivalInfo(); // Ошибка! this.arrivalDate.toLocaleDateString is not a function at Object.showArrivalInfo |
Давайте перепишем одну из предыдущих функций, используя дополнительные проверки с помощью условной конструкции if...else:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let cloneDifferentObject = (obj) => { const clone = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { if (typeof obj[key] === 'object') { if (obj[key] instanceof Array) {clone[key] = obj[key].slice(); console.log(clone[key]);} else if (obj[key] instanceof Date) clone[key] = new Date(obj[key] ); else clone[key] = cloneSomeObject(obj[key]); } else clone[key] = obj[key]; } } return clone; } |
Снова создаем копию существующего объекта, меняем массив skills
и выводим информацию методом showArrivalInfo()
. Предварительно изменим фамилию в нашей копии, чтобы видеть изменения и в других свойствах.
1 2 3 4 5 6 7 8 9 |
let lisaCopy2 = cloneDifferentObject(lisa); console.log(lisa, lisaCopy2); lisaCopy2.lastName = "Ivanova"; lisaCopy2.skills.pop(); lisaCopy2.skills.push('React', 'Angular'); console.log(lisa.skills); //['HTML/CSS', 'JavaScript', 'PHP'] console.log(lisaCopy2.skills); //['HTML/CSS', 'JavaScript', 'React', 'Angular'] lisa.showArrivalInfo(); //Lisa Darrel came from New York 04.09.2021 lisaCopy2.showArrivalInfo(); //Lisa Ivanova came from New York 04.09.2021. |
Как видно из кода, все работает, как и предполагалось. У нас теперь и в копии объекта есть массив и дата в виде объектов соответствующего типа, к которым можно применить характерные для них методы.
Способ 5. Использование JSON.stringify() и JSON.parse() для глубокого копирования объектов
Самый короткий способ по тому признаку, сколько кода нужно записать в нем.
Если в объекте отсутствуют функции, значения типа undefined
, NaN
или Date
, то глубокое копирование такого объекта можно совершить с помощью JSON.stringify()
и JSON.parse()
. С помощью этих методов мы последовательно превращаем наш объект сначала в строку, а затем снова в объект, но при этом начальный объект и результат наших манипуляций - это уже 2 разных объекта.
Тестируем объект, аналогичный предыдущему по своим свойствам и методам:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const michael = { firstName: 'Michael', lastName: 'Brown', age: 22, address: { street: 'Dyer Avenue', appartment: '33', city: 'New York' }, skills: ['HTML/CSS', 'JavaScript', 'C#'], arrivalDate: new Date(), showInfo: function () { console.log(`${this.firstName} ${this.lastName} from ${this.address.city}`) }, showArrivalInfo: function () { console.log(`${this.firstName} ${this.lastName} came from ${this.address.city} ${this.arrivalDate.toLocaleDateString()}.`) } } const michaelCopy = JSON.parse(JSON.stringify(michael)); console.log(michael === michaelCopy); //false console.log(michael, michaelCopy); |
Смотрим в консоль и видим, что в копии все хорошо со свойствами, у которых значения в виде вложенного объекта и массива, а вот с методами и свойством в виде объекта Date не все так, как хотелось бы. Дата имеет строковый формат, а методов просто нет.
Вывод: этот способ копирования нельзя использовать для копирования методов объекта, которые были записаны в нем самом, а не в его прототипе, а также объект Date
.
Дополнительная проверка:
1 2 3 4 5 6 7 8 9 10 11 12 |
michaelCopy.lastName = 'Johnson'; michaelCopy.skills[0] = 'PHP'; console.log(michael.skills, michaelCopy.skills); michael.showInfo(); //Michael Brown from New York michaelCopy.showInfo(); // Ошибка! Uncaught TypeError: michaelCopy.showInfo is not a function console.log(michael.arrivalDate); //Sat Sep 04 2021 15:07:09 GMT+0300 (Восточная Европа, летнее время) console.log(michaelCopy.arrivalDate); //2021-09-04T12:07:09.549Z console.log(michael.arrivalDate.toLocaleDateString()); //04.09.2021 console.log(michaelCopy.arrivalDate.toLocaleDateString()); //Ошибка! Uncaught TypeError: michaelCopy.arrivalDate.toLocaleDateString is not a function |
Проверка показала, что JSON.parse(JSON.stringify(obj))
не работает, если свойством объекта является функция, т.е. это уже не свойство, а метод объекта. Также нельзя скопировать значения undefined
, NaN
и Date
, потому что они не подходят для синтаксиса формата JSON. Поэтому используйте этот способ, только когда объект содержит строки и числа, а также вложенные объекты с теми же типами данных и массивы.
Заключение
Копировать, или клонировать объекты можно разными способами в зависимости от того, какую цель вы преследуете. Далеко не всегда нужно глубокое копирование, поэтому для простых объектов подойдет способ с оператором расширения или методом Object.assign()
. В зависимости от того, какие типы данных у вас присутствуют в ключах объекта, для глубокого копирования вы можете выбрать JSON.parse(JSON.stringify(obj))
или рекурсивный проход по свойствам и методам объекта с помощью цикла for...in
или Object.keys()
. Помните о том, что глубокое копирование объекта - это трудозатратная операция, поэтому выбирайте оптимальный способ в зависимости от задачи.