javascript

Клонируем правильно: structuredClone() в JS

  • среда, 9 апреля 2025 г. в 00:00:14
https://habr.com/ru/companies/otus/articles/897886/

Привет, Хабр!

Глубокое копирование в JavaScript всегда было немного проблемой. До тех пор, пока в языке не появился structuredClone() — метод, который решил многие наши проблемы.

Почему structuredClone() — это не JSON.stringify() 2.0

Если раньше вы использовали JSON.stringify() + JSON.parse() для глубокого копирования, то вы знаете: работает, но только до первого нестандартного объекта.

const data = {
  date: new Date(),
  regex: /hello/gi,
  map: new Map([['key', 'value']]),
};

const jsonCopy = JSON.parse(JSON.stringify(data));
console.log(jsonCopy); 
// => { date: "2025-02-02T12:00:00.000Z", regex: {}, map: {} }

Ни тебе Date, ни Map, ни RegExp. Просто пустые оболочки. А если в объекте будет циклическая ссылка — вообще упадёт с ошибкой.

Теперь тот же самый объект через structuredClone():

const structuredCopy = structuredClone(data);

console.log(structuredCopy.date instanceof Date); // true
console.log(structuredCopy.regex instanceof RegExp); // true
console.log(structuredCopy.map instanceof Map); // true

Тут все склонируется корректно, без сюрпризов. structuredClone() умеет гораздо больше, чем JSON.parse(), и делает это куда безопаснее.

Что поддерживает structuredClone()

Функция работает с самыми разными типами данных:

  • Примитивы: undefined, null, boolean, number, bigint, string, symbol

  • Объекты и массивы

  • Date, RegExp, Map, Set

  • ArrayBuffer, SharedArrayBuffer, Blob, File, ImageData, MessagePort и прочие API браузера

Пример поинтересней — сериализация бинарных данных:

const buffer = new ArrayBuffer(16);
const clone = structuredClone(buffer);

console.log(clone.byteLength); // 16
console.log(clone === buffer); // false

Можно использовать с TypedArray, Blob, и передавать это всё в Web Worker.

Как обрабатываются вложенные структуры и циклы

Одна из фич structuredClone() — умение работать с циклическими структурами. JSON тут бессилен, а вот герой статьи — справляется:

const node = { value: 1 };
node.self = node;

const clone = structuredClone(node);
console.log(clone.self === clone); // true

Циклические ссылки не рвутся, всё сохраняется.

Где structuredClone() ломается

Но не всё так радужно. Есть вещи, с которыми structuredClone() не работает принципиально:

  1. Функции — нельзя передать поведение:

    const withFn = {
      sayHi: () => console.log('hi')
    };
    
    structuredClone(withFn); // DataCloneError
  2. DOM-элементы:

    const div = document.createElement('div');
    structuredClone(div); // DataCloneError
  3. Прототипы — теряются. То есть объект останется со своими свойствами, но уже без цепочки наследования:

    class MyClass {
      constructor(x) { this.x = x }
    }
    
    const instance = new MyClass(10);
    const clone = structuredClone(instance);
    
    console.log(clone instanceof MyClass); // false
    console.log(clone.x); // 10

Если важен не только state, но и поведение объекта (методы, прототипы, инстансы классов), лучше использовать сериализацию вручную или библиотеку вроде class-transformer.

Производительность: стоит ли беспокоиться?

structuredClone() не самый быстрый метод. Он надёжный, универсальный, но за это платишь временем.

Бенчмарки показывают, что structuredClone() медленнее, чем JSON.stringify() на простых структурах, но выигрывает в стабильности при сложных и вложенных объектах.

Если вы клонируете часто и большие объекты — профилируйте. Возможно, в критичных местах лучше будет использовать Object.assign() (поверхностно) или cloneDeep() из lodash (с компромиссами).

Альтернативы и полифиллы

structuredClone() поддерживается во всех современных браузерах и в Node.js 17+. Для более старых окружений можно использовать полифиллы вроде structured-clone или просто перейти на lodash.

Пример с Lodash:

import cloneDeep from 'lodash/cloneDeep';

const obj = { a: 1, b: { c: 2 } };
const deep = cloneDeep(obj);

Но lodash не умеет копировать Map, Set, Date корректно. Он клонирует "поверху", и не всегда это безопасно.

Примеры применения

Клонирование состояния в редакторах

Представим, что делаем WYSIWYG-редактор или графический UI-конструктор (в духе Figma, Notion, Miro и т.д.). Есть массив объектов, вложенные структуры, Map, Set, Date, возможно — граф связей между блоками (да, с циклами).

Каждое действие пользователя должно добавляться в стек истории, чтобы работали Undo/Redo. Поверхностного копирования тут недостаточно — нужно сохранить полное состояние на момент изменения.

Раньше приходилось что-то вроде:

function deepCloneState(state) {
  return JSON.parse(JSON.stringify(state)); // криво, ненадёжно
}

Теперь:

function saveSnapshot(state) {
  historyStack.push(structuredClone(state));
}

И всё. Вы уверены, что даже если у вас внутри Set, вложенные структуры и циклы — они склонируются корректно. Это позволяет избавиться от миллиона проверок и не плодить баги, когда history[2].nodes === currentState.nodes.

Передача состояния между вкладками через BroadcastChannel

Допустим, есть сложное веб-приложение с несколькими вкладками — например, админка или редактор. И хочетс, чтобы все вкладки были в синхроне. Для этого используем BroadcastChannel:

const channel = new BroadcastChannel('sync');

channel.postMessage(currentState); // допустим, вы храните store

Если currentState — это обычный объект с Map, Set, Blob, ArrayBuffer, или циклическими ссылками, то postMessage() либо уронит ошибку, либо передаст обрубок. Решение:

channel.postMessage(structuredClone(currentState));

В приёмной вкладке:

channel.onmessage = (event) => {
  const syncedState = event.data;
  // можно обновить store или выполнить reconcile
};

В итоге:

  • Map с ключами на объекты — сохраняются

  • Date — остаются объектами, а не строками

  • Циклы — не приводят к крашу

  • Состояние остаётся настоящим snapshot-ом, а не обрубком


Когда structuredClone() — не ваш выбор:

  • Нужны методы / прототипы (например, бизнес-логика на классах)

  • Производительность важна, и вы клонируете каждую миллисекунду

  • Вы работаете в старом окружении (до Node.js 17 / Safari 14)

Тем не менее, structuredClone() прост, надёжен и, в отличие от большинства других решений, не требует зависимостей.

Активно используется в React для Undo/Redo и сохранения снапшотов состояния, в Vue при работе с reactive-структурами, в Svelte при передаче сложных объектов, в Web Workers, при синхронизации вкладок через BroadcastChannel, в state-менеджерах вроде Redux и Pinia, а также при сериализации данных в IndexedDB и postMessage.

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


Если вам интересна глубина JavaScript и подходы к архитектуре современных фронтенд-приложений, загляните на открытые уроки в Otus — там обсудим практику, реальные кейсы и инструменты, которые стоит держать под рукой.

Ближайшие темы:

  • 8 апреля — Vue 3: Реактивность на максимум — как управлять реактивностью в Composition API. Записаться

  • 21 апреля — JavaScript: как стать профессионалом и зачем знать больше одного фреймворка? Записаться