Мы знаем, что объекты js передаются по доле (call by sharing
), поэтому при работе со сложными js-объектами побочные эффекты часто вызываются модификацией объекта — потому что я не знаю, кто еще ссылается на эти данные, и я не знаю, на кого эти модификации повлияют. Поэтому мы часто делаем копию объекта и помещаем ее в функцию-обработчик. Наиболее распространенной копией является использованиеObject.assign()
Сделайте новую копию или воспользуйтесь преимуществами операций деструктурирования объектов ES6, но это всего лишь поверхностные копии.
глубокая копия
Если требуется глубокая копия, оцените тип значения атрибута при копировании. Если это объект, то рекурсивно вызовите функцию глубокой копии. Конкретную реализацию см. в jQuery$.extend
. На самом деле там много логических ветвей, которые нужно обработать.Функция глубокого копирования cloneDeep в lodash даже имеет сотни строк.Есть простой и грубый способ?
JSON.parse
Самый примитивный и эффективный способ — использоватьJSON.parse
Преобразуйте объект в его строковое представление JSON, а затем проанализируйте его обратно в объект:
const deepClone(obj) => JSON.parse(JSON.stringify(obj));
Для большинства сценариев, за исключением того, что синтаксический анализ строк потребляет небольшую производительность (которая на самом деле незначительна), это действительно практичный метод. Но смущает то, что он не может обрабатывать случай круговых объектов (родительские и дочерние узлы ссылаются друг на друга), и он не может обрабатывать ситуацию, когда в объекте есть функции, закономерности и т. д.
MessageChannel
Интерфейс MessageChannel — это интерфейс API связи канала, который позволяет нам создавать новый канал и передавать данные через два свойства канала MessagePort.
Используя эту функцию, мы можем создать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 API
,Notification API
д., все используют алгоритм структурированного клонирования (Structured Clone
)
Осознайте трансфертную стоимость.
Immutable
Если вам нужно часто работать со сложным объектом, слишком неэффективно каждый раз делать полную глубокую копию. В большинстве случаев обновляются только одно или два поля объекта, а остальные поля остаются неизменными, копировать эти неизмененные поля явно избыточно. Посмотрите, что сказал Дэн Абрамов:
)Ключевая идея этих библиотек заключается в создании持久化的数据结构
(Persistent data structure
), при работе с объектом клонировать только измененный узел и его узлы-предки, а остальные остаются без изменений.结构共享
(structural sharing
). Например, после изменения красного узла на рисунке ниже будут регенерированы только три зеленых узла, а остальные узлы останутся повторно используемыми (аналогично ощущению мягкой цепи). Это уменьшает необходимость создания 8 новых узлов для глубокого копирования до 3 новых узлов.
Immutable.js
существуетImmutable.js
«Узел» здесь нельзя понимать просто как «ключ» в объекте, который использует внутреннююTrie(字典树)
структура данных,Immutable.js
Все ключи объекта будут сопоставлены с хэшем, а полученное хэш-значение будет преобразовано в двоичное, разделено каждые 5 бит от конца к началу, а затем преобразовано в дерево Trie.
Например, если есть зоопарк объектов:
zoo={
'frog':🐸
'panda':🐼,
'monkey':🐒,
'rabbit':🐰,
'tiger':🐯,
'dog':{
'dog1':🐶,
'dog2':🐕,
...// 还有 100 万只 dog
}
...// 剩余还有 100 万个的字段
}
Значение «лягушки» после хеширования равно 3151780, преобразовано в двоичный код.11 00000 00101 11101 00100
, точно так же хэш «собака» после перехода на второй механизм11 00001 01001 11100
Тогда позиции лягушки и собаки в дереве неизменяемых объектов Trie следующие:
Конечно, фактическое дерево Trie будет обрезано в соответствии с фактическим объектом, ветви без значения будут обрезаны, и каждый узел не будет заполнен 32 дочерними узлами.
Например, однажды вам нужно изменить zoo.frog с 🐸 на 👽 , изменившиеся узлы — это только зеленые на рисунке выше, а другие узлы напрямую используются повторно, что намного эффективнее, чем глубокое копирование для генерации 1 миллион узлов.
В целом использование Immutable.js намного эффективнее прямого глубокого копирования при работе с большими объемами данных, но для небольших объектов в целом разница невелика. Однако, если вам нужно изменить глубоко вложенный объект, Immutable.js более лаконичен, чем прямое Object.assign или деструктурирование.
Такие как модификацияzoo.dog.dog1.name.firstName = 'haha'
, есть два способа записи:
// 对象解构
const zoo2 = {...zoo,dog:{...zoo.dog,dog1:{...zoo.dog.dog1,name:{...zoo.dog.dog1,firstName:'haha'}}}}
//Immutable.js 这里的 zoo 是 Immutable 对象
const zoo2 = zoo.updateIn(['dog','dog1','name','firstName'],(oldValue)=>'haha')
seamless-immutable
Если объем данных невелик, но вы хотите использовать что-то вроде этогоupdateIn
Если синтаксис удобен, вы можете использоватьseamless-immutable
. В этой библиотеке нет вышеперечисленных триев, она просто расширена для нихupdateIn
,merge
Обычные простые объекты с 9 методами и т. д., используяObject.freeze
Сам замороженный объект изменяется каждый раз, когда вы изменяете копию return. Ощущение кастрированной версии, производительность меньше, чем у Immutable.js, но в некоторых сценариях применима.
Подобные библиотеки упоминает и Дэн Абрамов.immutability-helper
а такжеupdeep
, их использование и реализация аналогичны, напримерupdateIn
методы черезObject.assign
и деструктурирование объекта.
Immer.js
И способ написания Immer.js можно назвать четким потоком:
import produce from "immer"
const zoo2 = produce(zoo, draft=>{
draft.dog.dog1.name.firstName = 'haha'
})
Хотя на расстоянии это выглядит не очень изящно, но написать его относительно просто, и всю логику, которую нужно изменить, можно поместить вproduce
Функция второго аргумента (называетсяproducer 函数
) внутри, не окажет никакого влияния на исходный объект. В функции-производителе можно одновременно изменить несколько полей, что очень удобно для разовой операции.
Этот метод использования оператора «точка» как встроенной операции, очевидно, захватывает результат данных и выполняет новые операции. Теперь многие фреймворки также любят делать это, используяObject.defineProperty
Добейтесь желаемого эффекта. Immer.js используетProxy
Реализовано: создать прокси для каждого посещенного узла в исходных данных, изменить копию, не оперируя исходными данными при изменении узла, и, наконец, вернуться к объекту, состоящему из неизмененной части и измененной копии.
Структура каждого прокси-объекта в immer.js выглядит следующим образом:
function createState(parent, base) {
return {
modified: false, // 是否被修改过,
assigned:{},// 记录哪些 key 被改过或者删除,
finalized: false // 是否完成
base, // 原数据
parent, // 父节点
copy: undefined, // base 和 proxies 属性的浅拷贝
proxies: {}, // 记录哪些 key 被代理了
}
}
При вызове геттера ключа исходного объекта, если ключ был изменен, будет возвращено значение соответствующего ключа в копии, если не изменено, будет создан прокси для дочернего узла и исходное значение будет возвращено напрямую. При вызове сеттера ключа значение в копии напрямую изменяется. Если это первая модификация, вам также необходимо мелко копировать свойства базы и свойства копируемых прокси. В то же время родительский узел рекурсивно копируется в соответствии с родительским атрибутом, а поверхностное копирование продолжается до корневого узла.
все еще сdraft.dog.dog1.name.firstName = 'haha'
Например, геттеры узлов dog, dog1 и name будут запускаться по очереди для создания прокси. Когда операция установки выполняется для firstName узла имени, все атрибуты имени будут неглубоко скопированы в атрибут копии узла, а затем копия будет изменена напрямую, а затем все родительские узлы имени узел будет по очереди неглубоко скопирован в свой собственный атрибут копирования. После завершения всех модификаций просматривается все дерево и возвращается новый объект, включающий немодифицированную часть базы каждого узла и измененную часть копии.
Суммировать
Immutable.js — хороший выбор при работе с большими объемами данных. Как правило, когда объем данных невелик, для глубоко вложенных объектов хорошо использовать immer или Seamless-immutable, в зависимости от того, к какому методу записи вы привыкли. Если вам нужна «идеальная» глубокая копия, вы должны использовать lodash 😂.