JS StructuredClone: ​​не совсем глубокая копия?

Сегодня, исследуя ошибку в нашем приложении, я стал свидетелем очень неожиданного поведения 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, потому что:

  1. Он утверждает, что выполняет глубокое клонирование
  2. Такого поведения не происходит при использовании JSON.parse(JSON.stringify(value))

Если бы вместо этого вы сделали myObj.field2[1] = 'baz', у вас уже было бы точно такое же поведение в myObj. Так почему же клон должен вести себя иначе, чем оригинал?

CBroe 05.07.2024 12:19

Это также отражается на myObj? Если да: это было бы удивительно. Если нет: возможно, myObjCloned ведет себя точно так же, как myObj.

deceze 05.07.2024 12:19

Удивительно, но знание этого делает structuredClone еще лучше

Konrad 05.07.2024 12:21
myObjCloned Глубоко клонирован. Как и массив. Структура сохранена как есть и ссылок с someSharedArray на клон нет. Все требования для глубокого клонирования выполнены. Кажется, вы ожидаете, что общий массив внезапно станет несколькими массивами, но я не думаю, что это разумное ожидание.
VLAZ 05.07.2024 12:22

@deceze Нет, исходный объект myObj не затрагивается, только клон.

Liran H 05.07.2024 12:24

И еще, в чем здесь вопрос? Хотите использовать structuredClone, но при этом деструктурировать его, чтобы создать отдельный клон для общих объектов?

VLAZ 05.07.2024 12:24
JSON.parse(JSON.stringify) всегда был уродливым хаком IMO, к которому всегда было прикреплено несколько звездочек, например, невозможность «клонировать» определенные типы или вызывать возможное нарушение ссылок на общий массив. structuredClone — гораздо более надежный механизм клонирования. До сих пор вы полагались только на побочные эффекты обходного пути JSON.
deceze 05.07.2024 12:24

Кроме того, если вам не нужны общие объекты, почему решение, к которому вы стремитесь, «исправляет» это при клонировании? Почему бы вам не предотвратить это в источнике клонирования?

VLAZ 05.07.2024 12:25

@VLAZ Причины, по которым это меня удивило, как уже упоминалось в теме, следующие: 1. Это отклонение от результирующего значения использования JSON.parse(JSON.stringify(value)), 2. Я не ожидал, что общие ссылки будут сохранены в клоне, даже если создается новая общая ссылка.

Liran H 05.07.2024 12:33
JSON.parse(JSON.stringify(value)) в любом случае на самом деле не является «клонированием». Таким образом, утверждение, что реальный алгоритм клонирования работает по-другому, не должно вызывать удивления. Он воссоздает данные, пропуская их через промежуточное представление. Промежуточное представление, более ограниченное, чем исходное. Конечно, он создает копии, не похожие на оригинал. Просто попробуйте JSON.parse(JSON.stringify({a: undefined, b: 1})) и вы сразу поймете, что это не клонирование.
VLAZ 05.07.2024 12:35

Опять же: о чем здесь спрашивают?

VLAZ 05.07.2024 12:37

@VLAZ Думаю, вопросы следующие: 1. Действительно ли structuredClone глубокое клонирование? 2. Предполагается ли, что structuredClone будет давать тот же результат, что и JSON.parse(JSON.stringify(value)) при тех же входных данных (с поддержкой JSON)? И я думаю, что ответы такие: 1: да, просто не так, как я думал, и 2. Не обязательно.

Liran H 05.07.2024 12:44

2. просто не может быть «да» никогда. Рассмотрим foo = {}; bar = {}; foo.bar = bar; bar.foo = foo — два объекта с циклической ссылкой между ними. JSON не может этого представить. Сериализация в JSON приведет к ошибке. Тем не менее, это совершенно достоверные данные. Это работает в JS. Вы можете получить вторую копию путем клонирования. Я не считаю разумным утверждать, что это невозможно клонировать или что клонирование циклической ссылки должно давать какие-то другие данные.

VLAZ 05.07.2024 12:47

Другими словами: заявлял ли когда-нибудь structuredClone, что ведет себя так же, как JSON.parse(JSON.stringify)? Где Javascript когда-либо официально одобрялся как канонический способ клонирования объектов?

deceze 05.07.2024 12:53
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
2
14
173
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

не действительно глубокая копия?

Это глубокая копия.

Правильная глубокая копия должна соответствовать нескольким условиям:

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

  2. Если два разных свойства имеют одинаковые значения (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 кажется расширенным набором [этой техники], но при этом поддерживает такие вещи, как […] циклические ссылки.»!

Bergi 09.07.2024 00:21

Другие вопросы по теме