Как скопировать объект в js
Перейти к содержимому

Как скопировать объект в js

  • автор:

Клонирование и копирование — JS: Объекты

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

В JavaScript клонирование можно эмулировать с помощью Object.assign() . Для этого нужно первым параметром передать пустой объект, а вторым — тот, который нужно клонировать:

const user =  name: 'Tirion', email: 'support@hexlet.io', age: 44 >; // Данные из user копируются во вновь созданный объект const copyOfUser = Object.assign(<>, user); user === copyOfUser; // false 

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

Клонирование также выполняют с помощью функции clone() библиотеки lodash. Результат выполнения этой функции идентичен примерам выше, но благодаря своему имени, она лучше выражает смысл операции:

import _ from 'lodash'; const user =  name: 'Tirion', email: 'support@hexlet.io', age: 44 >; const copyOfUser = _.clone(user); 

Клонирование способами выше не затрагивает вложенные объекты. Они оказываются в новом объекте по ссылке из старого:

const user =  company:  name: 'Hexlet' > >; const copyOfUser = Object.assign(<>, user); // Это тот же объект user.company === copyOfUser.company; // true user.company.createdAt = 2012; console.log(copyOfUser.company.createdAt); // 2012 

Такое клонирование называется поверхностным (shallow copying). Очень важно запомнить, что именно это имеют ввиду в JavaScript, когда употребляют термин «клонирование». Поверхностное клонирование подходит для многих ситуаций, но иногда его недостаточно. В таких случаях нужно использовать полное или глубокое клонирование (deep copying).

В JavaScript есть встроенный метод structuredClone() , c помощью которого можно выполнить глубокое копирование объектов:

const user =  company:  name: 'Hexlet' > >; const copyOfUser = structuredClone(user); user.company === copyOfUser.company; // false 

Открыть доступ

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

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Наши выпускники работают в компаниях:

Копирование объектов и ссылки

Одно из фундаментальных отличий объектов от примитивов заключается в том, что объекты хранятся и копируются «по ссылке», тогда как примитивные значения: строки, числа, логические значения и т.д. – всегда копируются «как целое значение».

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

Давайте начнём с примитива, такого как строка.

Здесь мы помещаем копию message во phrase :

let message = "Привет!"; let phrase = message;

В результате мы имеем две независимые переменные, каждая из которых хранит строку «Привет!» .

Вполне очевидный результат, не так ли?

Объекты ведут себя иначе.

Переменная, которой присвоен объект, хранит не сам объект, а его «адрес в памяти» – другими словами, «ссылку» на него.

Давайте рассмотрим пример такой переменной:

let user = < name: "John" >;

И вот как это на самом деле хранится в памяти:

Объект хранится где-то в памяти (справа от изображения), в то время как переменная user (слева) имеет лишь «ссылку» на него.

Мы можем думать о переменной объекта, такой как user , как о листе бумаги с адресом объекта на нем.

Когда мы выполняем действия с объектом, к примеру, берём свойство user.name , движок JavaScript просматривает то, что находится по этому адресу, и выполняет операцию с самим объектом.

Теперь вот почему это важно.

При копировании переменной объекта копируется ссылка, но сам объект не дублируется.

let user = < name: "John" >; let admin = user; // копируется ссылка

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

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

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

let user = < name: 'John' >; let admin = user; admin.name = 'Pete'; // изменено по ссылке из переменной "admin" alert(user.name); // 'Pete', изменения видны по ссылке из переменной "user"

Это как если бы у нас был шкафчик с двумя ключами, и мы использовали один из них ( admin ), чтобы войти в него и внести изменения. А затем, если мы позже используем другой ключ ( user ), мы все равно открываем тот же шкафчик и можем получить доступ к изменённому содержимому.

Сравнение по ссылке

Два объекта равны только в том случае, если это один и тот же объект.

Например, здесь a и b ссылаются на один и тот же объект, поэтому они равны:

let a = <>; let b = a; // копирование по ссылке alert( a == b ); // true, обе переменные ссылаются на один и тот же объект alert( a === b ); // true

И здесь два независимых объекта не равны, даже если они выглядят одинаково (оба пусты):

let a = <>; let b = <>; // два независимых объекта alert( a == b ); // false

Для сравнений типа obj1 > obj2 или для сравнения с примитивом obj == 5 объекты преобразуются в примитивы. Очень скоро мы изучим, как работают преобразования объектов, но, по правде говоря, такие сравнения требуются очень редко и обычно они появляются в результате ошибок программиста.

Клонирование и объединение, Object.assign

Итак, копирование объектной переменной создаёт ещё одну ссылку на тот же объект.

Но что, если нам всё же нужно дублировать объект? Создать независимую копию, клон?

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

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

let user = < name: "John", age: 30 >; let clone = <>; // новый пустой объект // давайте скопируем все свойства user в него for (let key in user) < clone[key] = user[key]; >// теперь clone это полностью независимый объект с тем же содержимым clone.name = "Pete"; // изменим в нём данные alert( user.name ); // все ещё John в первоначальном объекте

Также мы можем использовать для этого метод Object.assign.

Object.assign(dest, [src1, src2, src3. ])
  • Первый аргумент dest — целевой объект.
  • Остальные аргументы src1, . srcN (может быть столько, сколько необходимо) являются исходными объектами
  • Метод копирует свойства всех исходных объектов src1, . srcN в целевой объект dest . Другими словами, свойства всех аргументов, начиная со второго, копируются в первый объект.
  • Возвращает объект dest .

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

let user = < name: "John" >; let permissions1 = < canView: true >; let permissions2 = < canEdit: true >; // копируем все свойства из permissions1 и permissions2 в user Object.assign(user, permissions1, permissions2); // теперь user =

Если скопированное имя свойства уже существует, оно будет перезаписано:

let user = < name: "John" >; Object.assign(user, < name: "Pete" >); alert(user.name); // теперь user =

Мы также можем использовать Object.assign для замены цикла for..in для простого клонирования:

let user = < name: "John", age: 30 >; let clone = Object.assign(<>, user);

Он копирует все свойства user в пустой объект и возвращает его.

Также существуют и другие методы клонирования объекта. Например, с использованием оператора расширения clone = <. user>, рассмотренного далее в учебнике.

Вложенное клонирование

До сих пор мы предполагали, что все свойства user примитивныe. Но свойства могут быть и ссылками на другие объекты. Что с ними делать?

Например, есть объект:

let user = < name: "John", sizes: < height: 182, width: 50 >>; alert( user.sizes.height ); // 182

Теперь недостаточно просто скопировать clone.sizes = user.sizes , потому что user.sizes – это объект, он будет скопирован по ссылке. Таким образом, clone и user будут иметь общий объект sizes :

let user = < name: "John", sizes: < height: 182, width: 50 >>; let clone = Object.assign(<>, user); alert( user.sizes === clone.sizes ); // true, тот же объект // user и clone обладают общим свойством sizes user.sizes.width++; // изменяем свойства в первом объекте alert(clone.sizes.width); // 51, видим результат в другом

Чтобы исправить это, мы должны использовать цикл клонирования, который проверяет каждое значение user[key] и, если это объект, тогда также копирует его структуру. Это называется «глубоким клонированием».

Мы можем реализовать глубокое клонирование, используя рекурсию. Или, чтобы не изобретать велосипед заново, возьмите готовую реализацию, например _.cloneDeep(obj) из библиотеки JavaScript lodash.

Также мы можем использовать глобальный метод structuredClone(), который позволяет сделать полную копию объекта. К сожалению он поддерживается только современными браузерами. Здесь можно ознакомиться с поддержкой этого метода.

Объекты, объявленные как константа, могут быть изменены

Важным побочным эффектом хранения объектов в качестве ссылок является то, что объект, объявленный как const , может быть изменён.

const user = < name: "John" >; user.name = "Pete"; // (*) alert(user.name); // Pete

Может показаться, что строка (*) вызовет ошибку, но, это не так. Значение user это константа, оно всегда должно ссылаться на один и тот же объект, но свойства этого объекта могут свободно изменяться.

Другими словами, const user выдаст ошибку только в том случае, если мы попытаемся задать user=. в целом.

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

Итого

Объекты присваиваются и копируются по ссылке. Другими словами, переменная хранит не «значение объекта», а «ссылку» (адрес в памяти) на это значение. Таким образом, копирование такой переменной или передача её в качестве аргумента функции копирует эту ссылку, а не сам объект.

Все операции с использованием скопированных ссылок (например, добавление/удаление свойств) выполняются с одним и тем же объектом.

Чтобы создать «реальную копию» (клон), мы можем использовать Object.assign для так называемой «поверхностной копии» (вложенные объекты копируются по ссылке) или функцию «глубокого клонирования», такую как _.cloneDeep(obj).

Клонирование объектов JS

Всем привет, я — Кирилл, frontend разработчик компании Usetech.

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

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

  • structuredClone(obj)
  • lodash.cloneDeep(obj)
  • JSON.parse(JSON.stringify(obj))
  • Полифил cloneDeep(obj)

Поверхностное клонирование:

  • Оператор Spread
  • Object.assign()
  • Object.create()

Знали ли вы, что теперь в JavaScript есть встроенный способ делать глубокие копии объекта?

Итак, мы говорим про structuredClone — это функция встроенная в среду JavaScript:

const person = < name: "Ivan", date: new Date(123), friends: ["John"] >// клон объекта const copiedPerson = structuredClone(person)

Обратите внимание, мы скопировали не только объект, но ещё и вложенный массив, и даже объект Date.

Как видите, всё работает так, как ожидалось.

copiedPerson.friends // ["John"] copiedPerson.date // Date: Wed Dec 31 1969 16:00:00 copiedPerson.friends === person.friends // false

Метод structuredClone может выполнять следующие задачи:

  • Клонировать бесконечно вложенные объекты и массивы;
  • Клонировать циклические ссылки;
  • Клонировать различные типы JS, такие, как Date , Error , RegExp , ArrayBuffer , Blob , File , ImageData и другие;
  • Передача любых передаваемых объектов.

Даже такой безумный пример сработал, как мы и хотели.

const obj = < set: new Set([1123, 36, 3, 4, 4]), map: new Map([[133, 2222]]), regex: /foo/, deep: < array: [ new File(someBlobData, 'test.txt') ] >, error: new Error('Error!') > obj.circular = obj // ✅ Все хорошо, клонировали весь объект. const clonedObj = structuredClone(obj)

Почему бы просто взять и не развернуть объект?

Тут важно понимать, какое клонирование мы хотим: глубокое или поверхностное. Если нам нужно сделать поверхностное клонирование, то есть копию, которая не копирует вложенные объекты или массивы, то тогда можно обойтись Spread оператором.

const obj = < title: "Builder.io Conf", >// ✅ нет проблем, нет вложенных массивов и объектов const shallowCopy =

Есть иной способ поверхностного клонирования:

const shallowCopy = Object.assign(<>, obj); const shallowCopy = Object.create(obj);

Рассмотрим пример, когда у нас вложенные значения — это объект.

const personObj = < title: "Hello world", date: new Date(123), friends: ["John"] >const shallowCopy = // �� Мы только что добавили “Bob” и к копии и оригиналу shallowCopy.friends.push("Bob") // �� Мы только что изменили время у клона и оригинала shallowCopy.date.setTime(456)

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

А может выполнить глубокое клонирование через JSON.parse(JSON.stringify(obj)) ?

Этот трюк удивительно производительный, но имеет некоторые недостатки, которые structuredClone устраняет.

В качестве примера возьмем данный объект:

const person = < name: "Ivan", date: new Date(123), friends: ["John"] >// �� Выполним глубокое клонирование. const problematicPersonCopy = JSON.parse(JSON.stringify(person))

Если мы посмотрим на результат problematicPersonCopy , то увидим следующее:

Это немного не то, чего мы хотели: date должен быть Date объектом, а не строкой. Это произошло потому что JSON.stringify() может обрабатывать только базовые объекты, массивы, примитивы. С любыми другими типами он может повести себя не так, как бы вы хотели. Например, дату преобразует в строку, а Set преобразует в <>. JSON.stringify() полностью игнорирует некоторые вещи, такие как undefined или функции.

const obj = < set: new Set([1, 3, 3]), map: new Map([[1, 2]]), regex: /foo/, deep: < array: [ new File(someBlobData, 'file.txt') ] >, error: new Error('Hello!') > const veryProblematicObjCopy = JSON.parse(JSON.stringify(obj))
< "set": <>, "map": <>, "regex": <>, "deep": < "array": [ <>] >, "error": <>, >

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

Рассмотрим следующий способ через lodash.cloneDeep.

Сегодня cloneDeep функция lodash очень распространённое решение этой проблемы.

И действительно работает, так как ожидалось:

import cloneDeep from 'lodash/cloneDeep' const person = < name: "Ivan", date: new Date(123), friends: ["John"] >// ✅ Все хорошо! const clonedEvent = cloneDeep(person)

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

Если не придавать значения и сразу импортировать из библиотеки, то мы увидим, насколько тяжелее стал импорт. И это только для одной функции cloneDeep .

Но затаскивать целую библиотеку для глубокого клонирования в данный момент уже не нужно, когда есть structuredClone .

Полифил cloneDeep

Также мы всегда можем написать свой собственный полифил, если это крайне необходимо для глубокого клонирования.

const deepClone = (obj) => < if (obj === null) return null const clone = Object.assign(<>, obj) Object.keys(clone).forEach( (key) => (clone[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key]) ) if (Array.isArray(obj)) < clone.length = obj.length return Array.from(clone) >return clone > const a = < foo: 'bar', obj: < a: 1, b: 2 >> const b = deepClone(a) // a !== b = true, a.obj === b.obj = false

Реализация достаточно несложная, но если на ваш взгляд, я что-то упустил, можете доработать и рассказать об этом в комментариях.

Что в structuredClone нельзя клонировать?

1. Функции.

Они будут генерировать DataCloneError исключение.

// �� Ошибка! structuredClone( < fn: () => < >>)

2. DOW-узлы.

Также выдает DataCloneError исключение.

// �� Ошибка! structuredClone(< el: document.body >)

3. Дескрипторы свойств, сеттеры и геттеры. А также аналогичные функции, подобные метаданным не клонируются.

Например, с геттером клонируется результирующее значение, но не сама функция геттера.

structuredClone( < get foo() < return 'bar' >>)

4. Прототипы объектов.

Если вы планируете экземпляр myClass, клонированный объект больше не будет известен как экземпляр этого класса (но все действительные свойства этого класса будут клонированы).

class MyClass < foo = 'bar' myMethod() < /* . */ >> const myClass = new MyClass() const cloned = structuredClone(myClass) // Becomes: < foo: 'bar' >cloned instanceof myClass // false

Полный список поддерживаемых типов. Проще говоря, что не входит в список типов, то не может быть клонировано:

JS встроенные модули: Array, ArrayBuffer, Boolean, DataView, Date, Error, Map и т.д.

Типы ошибок: Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError и т.д.

Поддержка браузеров и среда выполнения.

И вот самое приятное — structuredClone поддерживается во всех основных браузерах, даже в Node.js и Deno.

Поддержка браузеров: Источник: MDN

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

  • клонирование объектов
  • глубокое клонирование
  • поверхностное клонирование
  • structuredClone
  • javascript
  • Блог компании Usetech
  • JavaScript
  • Программирование

Поверхностное и глубокое копирование

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

Время чтения: меньше 5 мин

Открыть/закрыть навигацию по статье

  1. Кратко
  2. Как понять
    1. Проблема поверхностного копирования
    2. Как получить глубокую копию

    Обновлено 25 июля 2023

    Кратко

    Скопировать ссылку «Кратко» Скопировано

    При копировании объектов или массивов JavaScript копирует данные только на один уровень вглубь. Этот тип копирования называется поверхностным (shallow).

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

    Как понять

    Скопировать ссылку «Как понять» Скопировано

    Проблема поверхностного копирования

    Скопировать ссылку «Проблема поверхностного копирования» Скопировано

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

     const itemsInCart = [ < product: 'Носки', quantity: 3 >, < product: 'Штаны', quantity: 1 >, < product: 'Кепка', quantity: 1 >,] const clonedCart = [. itemsInCart] const itemsInCart = [  product: 'Носки', quantity: 3 >,  product: 'Штаны', quantity: 1 >,  product: 'Кепка', quantity: 1 >, ] const clonedCart = [. itemsInCart]      

    Если изменять элементы этой структуры после копирования, то эти изменения будут также видны в исходной структуре:

     clonedCart[1].quantity = 5 console.log(clonedCart)// [// < product: 'Носки', quantity: 3 >,// < product: 'Штаны', quantity: 5 >,// < product: 'Кепка', quantity: 1 >,// ] console.log(itemsInCart)// [// < product: 'Носки', quantity: 3 >,// < product: 'Штаны', quantity: 5 >,// < product: 'Кепка', quantity: 1 >,// ] clonedCart[1].quantity = 5 console.log(clonedCart) // [ // < product: 'Носки', quantity: 3 >, // < product: 'Штаны', quantity: 5 >, // < product: 'Кепка', quantity: 1 >, // ] console.log(itemsInCart) // [ // < product: 'Носки', quantity: 3 >, // < product: 'Штаны', quantity: 5 >, // < product: 'Кепка', quantity: 1 >, // ]      

    Непримитивные типы данных, такие как массивы и объекты, хранятся по ссылке. Так как копирование происходит только на один уровень вглубь, то при копировании массива происходит копирование ссылок на старые объекты в новый массив.

    В итоге получается картина, когда разные массивы ссылаются на одни и те же объекты в памяти:

     console.log(itemsInCart[1] === clonedCart[1])// true console.log(itemsInCart[1] === clonedCart[1]) // true      

    Результат поверхностного копирования массива

    Как получить глубокую копию

    Скопировать ссылку «Как получить глубокую копию» Скопировано

    В JavaScript есть функция structuredClone() для глубокого копирования массивов или объектов. На странице с документацией можно проверить доступность этой функции для разных версий: например, она доступна в NodeJS, начиная с версии 17.0.0.

     const deep = structuredClone(itemsInCart)console.log(itemsInCart[1] === deep[1])// false const deep = structuredClone(itemsInCart) console.log(itemsInCart[1] === deep[1]) // false      

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

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

     import cloneDeep from 'lodash.clonedeep' const deep = cloneDeep(itemsInCart)console.log(itemsInCart[1] === deep[1])// false import cloneDeep from 'lodash.clonedeep' const deep = cloneDeep(itemsInCart) console.log(itemsInCart[1] === deep[1]) // false      

    Ещё один способ глубокого копирования звучит достаточно глупо — нужно сериализовать копируемый объект в JSON и тут же распарсить его. В результате появится полная копия объекта:

     const deep = JSON.parse(JSON.stringify(itemsInCart))console.log(itemsInCart[1] === deep[1])// false const deep = JSON.parse(JSON.stringify(itemsInCart)) console.log(itemsInCart[1] === deep[1]) // false      

    Результат глубокого копирования массива

    У этого метода есть ограничение — копируемые данные должны быть сериализуемы.

    Вот примеры несериализуемых данных: примитив undefined, функция, symbol — при вызове JSON.stringify получаем undefined

    Массивы и объекты — сериализуемы. Что будет если у них в качестве ключа или значения будут несериализуемые данные?

    • для массивов: такие значения будут превращены в null;
    • для объектов: такие значения будут опущены, а если symbol является ключом объекта, то он будет проигнорирован, даже при использовании функции replacer.
     const arr = [ undefined, function() < console.log('aaa') >, Symbol("foo"),]const copyArr = JSON.parse(JSON.stringify(arr)) console.log(copyArr)// [null, null, null] const obj =  a: undefined, method: () => <>, [Symbol("foo")]: "foo",>const copyObj = JSON.parse(JSON.stringify(obj), function(k, v)  if (typeof k === 'symbol')  return 'символ'; > return v;>) console.log(copyObj)// <> const arr = [ undefined, function()  console.log('aaa') >, Symbol("foo"), ] const copyArr = JSON.parse(JSON.stringify(arr)) console.log(copyArr) // [null, null, null] const obj =  a: undefined, method: () => >, [Symbol("foo")]: "foo", > const copyObj = JSON.parse(JSON.stringify(obj), function(k, v)  if (typeof k === 'symbol')  return 'символ'; > return v; >) console.log(copyObj) // <>      

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *