Сегодня, исследуя ошибку в нашем приложении, я стал свидетелем очень неожиданного поведения JavaScript structuredClone
.
Этот метод обещает создать глубокий клон заданного значения. Ранее это достигалось с помощью метода JSON.parse(JSON.stringify(value))
, и на бумаге structuredClone
кажется расширенным набором этого метода, дающим тот же результат, а также поддерживающим такие вещи, как даты и циклические ссылки.
Однако сегодня я узнал, что если вы используете structuredClone
для клонирования объекта, содержащего переменные ссылочного типа, указывающие на одну и ту же ссылку, эти ссылки будут сохранены, а не будут создаваться новые значения с разными ссылками.
Вот игрушечный пример, демонстрирующий такое поведение:
const someSharedArray = ['foo', 'bar']
const myObj = {
field1: someSharedArray,
field2: someSharedArray,
field3: someSharedArray,
}
const myObjCloned = structuredClone(myObj)
console.info(myObjCloned)
/**
{
"field1": ["foo", "bar"],
"field2": ["foo", "bar"],
"field3": ["foo", "bar"],
}
**/
myObjCloned.field2[1] = 'baz'
// At this point:
// Expected: only `field2`'s value should change, because `myObjCloned` was deeply cloned.
// Actual: all fields' values change, because they all still point to `someSharedArray`
console.info(myObjCloned)
/**
{
"field1": ["foo", "baz"],
"field2": ["foo", "baz"],
"field3": ["foo", "baz"],
}
**/
Это очень удивительное поведение structuredClone
, потому что:
JSON.parse(JSON.stringify(value))
Это также отражается на myObj
? Если да: это было бы удивительно. Если нет: возможно, myObjCloned
ведет себя точно так же, как myObj
.
Удивительно, но знание этого делает structuredClone
еще лучше
myObjCloned
Глубоко клонирован. Как и массив. Структура сохранена как есть и ссылок с someSharedArray
на клон нет. Все требования для глубокого клонирования выполнены. Кажется, вы ожидаете, что общий массив внезапно станет несколькими массивами, но я не думаю, что это разумное ожидание.
@deceze Нет, исходный объект myObj
не затрагивается, только клон.
И еще, в чем здесь вопрос? Хотите использовать structuredClone
, но при этом деструктурировать его, чтобы создать отдельный клон для общих объектов?
JSON.parse(JSON.stringify)
всегда был уродливым хаком IMO, к которому всегда было прикреплено несколько звездочек, например, невозможность «клонировать» определенные типы или вызывать возможное нарушение ссылок на общий массив. structuredClone
— гораздо более надежный механизм клонирования. До сих пор вы полагались только на побочные эффекты обходного пути JSON.
Кроме того, если вам не нужны общие объекты, почему решение, к которому вы стремитесь, «исправляет» это при клонировании? Почему бы вам не предотвратить это в источнике клонирования?
@VLAZ Причины, по которым это меня удивило, как уже упоминалось в теме, следующие: 1. Это отклонение от результирующего значения использования JSON.parse(JSON.stringify(value))
, 2. Я не ожидал, что общие ссылки будут сохранены в клоне, даже если создается новая общая ссылка.
JSON.parse(JSON.stringify(value))
в любом случае на самом деле не является «клонированием». Таким образом, утверждение, что реальный алгоритм клонирования работает по-другому, не должно вызывать удивления. Он воссоздает данные, пропуская их через промежуточное представление. Промежуточное представление, более ограниченное, чем исходное. Конечно, он создает копии, не похожие на оригинал. Просто попробуйте JSON.parse(JSON.stringify({a: undefined, b: 1}))
и вы сразу поймете, что это не клонирование.
Опять же: о чем здесь спрашивают?
@VLAZ Думаю, вопросы следующие: 1. Действительно ли structuredClone
глубокое клонирование? 2. Предполагается ли, что structuredClone
будет давать тот же результат, что и JSON.parse(JSON.stringify(value))
при тех же входных данных (с поддержкой JSON)? И я думаю, что ответы такие: 1: да, просто не так, как я думал, и 2. Не обязательно.
2. просто не может быть «да» никогда. Рассмотрим foo = {}; bar = {}; foo.bar = bar; bar.foo = foo
— два объекта с циклической ссылкой между ними. JSON не может этого представить. Сериализация в JSON приведет к ошибке. Тем не менее, это совершенно достоверные данные. Это работает в JS. Вы можете получить вторую копию путем клонирования. Я не считаю разумным утверждать, что это невозможно клонировать или что клонирование циклической ссылки должно давать какие-то другие данные.
Другими словами: заявлял ли когда-нибудь structuredClone
, что ведет себя так же, как JSON.parse(JSON.stringify)
? Где Javascript когда-либо официально одобрялся как канонический способ клонирования объектов?
не действительно глубокая копия?
Это глубокая копия.
Правильная глубокая копия должна соответствовать нескольким условиям:
Он должен сопоставить каждый отдельный объект в оригинале ровно с одним отдельным объектом в результате: отображение 1 к 1. Это также руководящий принцип, обеспечивающий поддержку циклических ссылок.
Если два разных свойства имеют одинаковые значения (Object.is(a, b) === true
), то эти свойства в глубоком клоне также должны быть идентичны друг другу.
Во входных данных вашего примера есть два отдельных объекта: один массив и один сложный объект (верхнего уровня). Более того, результат Object.is(myObj.field1, myObj.field2)
верен.
То, что вы получаете с structuredClone
в вашем примере, соответствует этому. Примечательно, что Object.is(myObjCloned.field1, myObjCloned.field2)
правда.
То, что вы ожидали получить (и что возвращает JSON.parse(JSON.stringify(value))
), нарушает этот принцип: будут созданы три разных массива, а это означает, что один и тот же массив был скопирован более одного раза, и сопоставление 1-к-1 больше не существует. Ранее упомянутое выражение Object.is
оценивается как ложное.
Давайте возьмем ввод с обратной ссылкой:
const root = {};
root.arr = [root, root, root];
Здесь у нас есть один объект и один массив. Последний содержит три ссылки на первый объект. Также здесь мы ожидаем, что эти три ссылки на один объект приведут к появлению еще одной тройки ссылок, каждая из которых ссылается на единственный родительский объект-клон. Это тот же принцип, что и в вашем примере, только общая ссылка является родительским объектом.
ОП даже сам упомянул об этом: «structuredClone
кажется расширенным набором [этой техники], но при этом поддерживает такие вещи, как […] циклические ссылки.»!
Если бы вместо этого вы сделали
myObj.field2[1] = 'baz'
, у вас уже было бы точно такое же поведение вmyObj
. Так почему же клон должен вести себя иначе, чем оригинал?