JavaScript | Как клонировать объект?

JavaScript | Как клонировать объект?

Иногда в JavaScript возникает задача по клонированию какого-то объекта. И в этот момент не всегда ясно как это правильно нужно делать.

 

Решение, когда объект имеет один уровень вложенности

Способ № 1 —

У нас есть объект, который мы хотим клонировать. Его глубина равна 1. Вложенных объектов в нём нет.

То есть мы хотим взять все свойства этого объекта и перенести их в новый объект.

let obj22 = {s:123, z:456}

Теперь нам нужно создать новый идентификатор привязки и присвоить ему результат выражения:

литерального объявления объекта и оператора троеточия.

 

Производство ObjectLiteral по стандарту ECMAScript
Производство ObjectLiteral по стандарту ECMAScript
let obj33 = {...obj22}

С этого момента в оперативной памяти живут уже два объекта и две ссылки на них. Клонирование объекта одного уровня вложенности прошло успешно.

Склонировали объект в JavaScript
Склонировали объект в JavaScript

 

Проверка клонирования объекта с одним уровнем вложенности

Давайте изменим их свойства и проверим, что всё правильно склонировалось.

obj22.x = 111
obj33.r = 777
Добавили свойства в разные объекты - в клон и в оригинал - JavaScript
Добавили свойства в разные объекты — в клон и в оригинал — JavaScript

Оригинальный объект хранит свои свойства, а клонированный объект свои. Добавление новых свойств произошло только для каждого из объектов.

Теперь можем изменить свойство, которое клонировали и ещё раз убедиться, что всё ок.

obj22.s = 0
Свойства объекта успешно склонированы и независимы - JavaScript
Свойства объекта успешно склонированы и независимы — JavaScript

 

Проблемы, которые возникают при неправильном клонировании

Если пытаться присвоить новому идентификатору привязки, какой-нибудь существующий идентификатор привязки, то мы просто получаем ссылку на один и тот же объект, а не клонируем новый.

Пример проблемы:

let ob1 = {a:"a", b:"b"};

let ob2 = ob1;

В таком виде ob2 будет ссылаться на тот же самый объект что и ob1. Оба идентификатора привязки ссылаются на один и тот же объект.

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

Привязали два идентификатора на один объект - JavaScript
Привязали два идентификатора на один объект — JavaScript

Если мы у любого идентификатора поменяем значение у свойства A, то изменения отразятся на другом идентификаторе, так как они оба ссылаются на один и тот же объект.

ob1.a = 100
Поменяли свойство у одного идентификатора привязки - JavaScript
Поменяли свойство у одного идентификатора привязки — JavaScript

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

 

Способ № 2 — Через конструктор Object и его свойство assign

Функция assign используется для копирования значений всех перечисляемых собственных свойств из одного или нескольких исходных объектов в целевой объект.

Пример использования. У нас есть оригинальный объект:

let obj44 = {44:44, 55:55};

В новую переменную obj700 положим результат вызова метода assign()

let obj700 = Object.assign({}, obj44);

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

В результате мы получаем новый клон оригинального объекта.

Склонировали объект через конструктор Object и метод assign - JavaScript
Склонировали объект через конструктор Object и метод assign — JavaScript

 

Решение, когда объект имеет несколько уровней вложенности

Пример вложенного объекта:

let o1 = {a:1, b:{f:2, r:3}}

Что будет, если мы попытаемся клонировать этот объект, описанными ранее способами?

// способ через троеточие ...

let o2 = {...o1}
o2.a = 'R'
o1
o2
Клонировали объект с несколькими уровнями вложенности через spread оператор - JavaScript
Клонировали объект с несколькими уровнями вложенности через spread оператор — JavaScript

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

Но…

Если мы изменим значение у свойства вложенного объекта:

// способ через assign()

let o3 = Object.assign({}, o1)
o3.b.f = 'S'
o1
o3
Клонировали объект с несколькими уровнями вложенности через метод assign() - JavaScript
Клонировали объект с несколькими уровнями вложенности через метод assign() — JavaScript

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

 

Как решить задачу в ситуации с вложенными объектами?

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

Делается это при помощи класса JSON и его методов stringify() и parse().

Пример вложенного объекта:

let o1 = {a:1, b:{f:2, r:3}}
let o4 = JSON.parse(JSON.stringify(o1))
o4.b.r = 'G'
o1
o4

Результат работы:

Клонирование глубокого объекта с примитивами через приведение к строке через JSON - JavaScript
Клонирование глубокого объекта с примитивами через приведение к строке через JSON — JavaScript

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

 

Функция клонирования объекта, рекурсивная

function clone(object) {
  let result = {};
  for (let key in object) {
    let value = object[key];
    if (value.constructor.name === 'Object' && value !== null) {
      value = clone(value);
    }
  result[key] = value;
  }
  return result;
}

 

Информационные ссылки

Стандарт ECMAScript — Раздел «Производство ObjectLiteral» — https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#prod-ObjectLiteral