Клонируем правильно: structuredClone() в JS
- среда, 9 апреля 2025 г. в 00:00:14
Привет, Хабр!
Глубокое копирование в JavaScript всегда было немного проблемой. До тех пор, пока в языке не появился structuredClone()
— метод, который решил многие наши проблемы.
Если раньше вы использовали 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(), и делает это куда безопаснее.
Функция работает с самыми разными типами данных:
Примитивы: 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() не работает принципиально:
Функции — нельзя передать поведение:
const withFn = {
sayHi: () => console.log('hi')
};
structuredClone(withFn); // DataCloneError
DOM-элементы:
const div = document.createElement('div');
structuredClone(div); // DataCloneError
Прототипы — теряются. То есть объект останется со своими свойствами, но уже без цепочки наследования:
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:
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: как стать профессионалом и зачем знать больше одного фреймворка? Записаться