Skip to main content

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

  • Эта публикация - перевод статьи. Ее автор - Surma. Оригинал доступен по ссылке ниже:

Как скопировать объект в JavaScript? Это простой вопрос без простого ответа.

Вызов по ссылке

JavaScript передает все по ссылке. Если вы не знаете, что это значит, вот пример:

function mutate(obj) {
  obj.a = true;
}

const obj = {a: false};
mutate(obj)
console.log(obj.a); // prints true

Функция mutate изменяет объект, который приходит к ней в качестве параметра. В среде «вызов по значению» функция получит переданное значение копии - по нему функция может работать. Любые изменения, которые функция делает объекту, не будут видны вне этой функции. Но в среде «вызов по ссылке», такой как JavaScript, функция ссылается и будет мутировать сам фактический объект. Поэтому в console.log в конце будет напечатано true.

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

Мелкая копия: Object.assign ()

Один из способов копирования объекта - использовать Object.assign(target, sources...). Он принимает произвольное количество исходных объектов, перечисляя все свои свойства и назначая их target. Если мы используем новый пустой объект как target, мы в основном копируем.

const obj = /* ... */;
const copy = Object.assign({}, obj);

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

function mutateDeepObject(obj) {
  obj.a.thing = true;
}

const obj = {a: {thing: false}};
const copy = Object.assign({}, obj);
mutateDeepObject(copy)
console.log(obj.a.thing); // prints true

Другое дело, чтобы потенциально отключиться - это Object.assign() превращает getters в простые свойства.

И что теперь? Оказывается, существует несколько способов создать глубокуюкопию объекта.

Примечание. Некоторые люди спрашивают об операторе распространения объектов. Распространение объекта также создаст мелкую копию.

JSON.parse

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

const obj = /* ... */;
const copy = JSON.parse(JSON.stringify(obj));

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

const x = {};
const y = {x};
x.y = y; // Cycle: x.y.x.y.x.y.x.y.x...
const copy = JSON.parse(JSON.stringify(x)); // throws!

Кроме того, такие вещи, как Maps, Sets, RegExps, Dates, ArrayBuffers и другие встроенные типы, просто теряются при сериализации.

Структурированный клон

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

MessageChannel

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

function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

const obj = /* ... */;
const clone = await structuralClone(obj);

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

API истории

Если вы когда-либо использовали history.pushState() для создания SPA, вы знаете, что вы можете предоставить объект состояния для сохранения рядом с URL-адресом. Оказывается, этот объект состояния структурно клонирован - синхронно. Мы должны быть осторожны, чтобы не путаться с какой-либо программной логикой, которая могла бы использовать объект state, поэтому нам нужно восстановить исходное состояние после того, как мы закончили клонирование. Чтобы предотвратить любые события от стрельбы, используйте history.replaceState() вместо history.pushState().

function structuralClone(obj) {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
}

const obj = /* ... */;
const clone = structuralClone(obj);

Еще раз, немного тяжело, чтобы задействовать движок браузера только для копирования объекта, но вы должны делать то, что нужно делать. Кроме того, Safari ограничивает количество вызовов replaceStateдо 100 в течение 30 секунд окна.

API уведомлений

После твист-штурма об этом целом в Twitter, Джереми Бэнкс показал мне, что есть третий способ задействовать структурное клонирование: The Notification API. Уведомления имеют связанный с ними объект данных, который клонируется.

function structuralClone(obj) {
  return new Notification('', {data: obj, silent: true}).data;
}

const obj = /* ... */;
const clone = structuralClone(obj);

Короткий, краткий. Мне нравится! Тем не менее, это в основном пинает разрешительной техники в браузере, поэтому я подозревал, что он довольно медленный. Safari по какой-то причине всегда возвращает undefined объект данных.

Эффективная феерия

Я хотел измерить, какой из этих способов является самым результативным. В моей первой (наивной) попытке я взял небольшой объект JSON и передал его через эти различные способы клонирования объекта тысячу раз. К счастью, Mathias Bynens сказал мне, что V8 имеет кеш, когда вы добавляете свойства к объекту. Я сравнивал кеш больше всего. Чтобы я никогда не попадал в кеш, я написал функцию, которая генерирует объекты заданной глубины и ширины с использованием случайных имен ключей и повторного запуска теста.

Графы!

Вот как различные методы выполняются в Chrome, Firefox и Edge. Ниже - лучше.

Производительность в Chrome 63

Производительность в Firefox 58

Производительность в Edge 16

Вывод

Так что же мы уберем?

  • Если вы не ожидаете циклических объектов и не нуждаетесь в сохранении встроенных типов, вы получаете самый быстрый клон во всех браузерах, используя JSON.parse(JSON.stringify()), что я нашел довольно неожиданным.
  • Если вам нужен правильный структурированный клон, MessageChannel это ваш единственный надежный выбор между браузерами.

Разве не лучше, если бы просто structuredClone() работала на платформе? Я, конечно, так думаю, и поднял старую проблему в спецификации HTML, чтобы пересмотреть этот подход.