Встречайте JavaScript

Встречайте JavaScript: копирование объектов

Как объекты копируются JavaScript

Введение

Постановка проблемы

При кодировании своего приложения вы можете столкнуться с ситуацией, когда вам нужно передать свойства из одного или нескольких объектов в другой, чтобы сформировать совершенно новый. JavaScript предлагает встроенные способы сделать это, и именно об этом эта статья, но прежде чем перейти к ней, нужно сделать две вещи.

Что нам нужно знать

Любой символ, число, строка, логическое значение, null или неопределеннаяпеременная называется примитивной; потому что единственное значение одного из этих типов — это все, что хранится в такой переменной. Таким образом, присвоение одной такой переменной другой приводит к тому, что обе они имеют одинаковое значение, но изменение одной не влияет на другую, поскольку они занимают разные места в памяти!

Любая переменная объект, массив или функция называется непримитивной или просто объектной. Подобно примитивам, они имеют конкретное значение, но это просто не число или строка. Вместо этого это указатель на память, называемый ссылкой, которая определяет, где «живет» вся ее структура и в которую мы можем многое поместить. Вы спросите, а что именно? Ответ просто все; Примитивы и непримитивы оба законны; Мы называем такие элементы свойствами. Стоит упомянуть, что вы можете иметь несколько переменных с любыми именами — любыми, которые принимает JavaScript — сохраняя одну и ту же ссылку и, таким образом, указывая на одну и ту же структуру. -примитивную переменную в другую и мутировать такой объект.

ЧАСТЬ 1. Если вы сделаете это, то в конечном итоге вы получите две переменные, использующие одну и ту же ссылку.

source === target:  true
source:  
number: 1
object1:
  string: "I like cookies."
  __proto__: Object
object2:
  string: "You dared to eat my cookies!"
  __proto__: Object
__proto__: Object
target:  
number: 1
object1:
  string: "I like cookies."
  __proto__: Object
object2:
  string: "You dared to eat my cookies!"
  __proto__: Object
__proto__: Object

ЧАСТЬ 2. Таким образом, изменение любого из свойств, которые есть, приводит к тому, что вы заметите одно и то же изменение в обоих!

source:  
number: 10
object1:
  string: "I like cookies."
  __proto__: Object
object2:
  string: "You dared to eat my cookies!"
  __proto__: Object
__proto__: Object
target:  
number: 10
object1:
  string: "I like cookies."
  __proto__: Object
object2:
  string: "You dared to eat my cookies!"
  __proto__: Object
__proto__: Object

ЧАСТЬ 3. Но ничто не мешает вам присвоить одному из них другую ссылку; однако тогда они больше не указывают на одно и то же место.

source === target:  false
source:  
notTheSameReference:
  string: "Yeah, they were delicious."
  __proto__: Object
theSameReference:
  string: "You dared to eat my cookies!"
  __proto__: Object
__proto__: Object
target:  
number: 10
object1:
  string: "I like cookies."
  __proto__: Object
object2:
  string: "You dared to eat my cookies!"
  __proto__: Object
__proto__: Object

ЧАСТЬ 4. Это означает, что если вы измените объект, он не повлияет на другой объект, пока ни одно из его свойств не содержит ту же ссылку.

source.theSameReference === target.object2:  true
source:  
notTheSameReference:
  string: "I'm truly sorry. I couldn't handle."
  __proto__: Object
theSameReference:
  string: "Damn you, you owe me one."
  __proto__: Object
__proto__: Object
target:  
number: 10
object1:
  string: "I like cookies."
  __proto__: Object
object2:
  string: "Damn you, you owe me one."
  __proto__: Object
__proto__: Object

Что происходит от

Я почти чувствую эти вибрации в воздухе, потому что пришло время сделать вывод о том, что следует из нашего рассмотрения. Наконец, мы можем собрать все вместе и перейти к копированию объектов.
Допустим, у нас есть исходный объект, и мы хотим его скопировать. Что нам нужно сделать, так это перебрать его свойства одно за другим, одновременно присваивая сохраненное значение нашему целевому объекту в пределах соответствующего имени свойства.

source === target: false
source.object === target.object: true
source:
number: 1
object:
  array: (3) [1, 2, 3]
  string: "I like cookies"
  __proto__: Object
__proto__: Object
target:
number: 1
object:
  array: (3) [1, 2, 3]
  string: "I like cookies"
  __proto__: Object
__proto__: Object

Объекты выглядят почти одинаково. Но даже если они есть, они определяются в разных местах памяти!
Что мы сделали, так это присвоили нашей цели ссылку на пустую структуру • const target = {};, а затем поместили туда свойство каждого источника следующим образом:

target.number = source.number; // primitive -> a value
target.object = source.object; // non-primitive -> a reference

Теперь вы должны заметить, что при выполнении такой копии вы выполняете итерацию только по свойствам самого высокого уровня, и это имеет два значения, а именно:

  • Что происходит при копировании примитивного свойства, скажем, source.primitive, так это сохранение дублирующегося значения в target.primitve, после чего изменение одного свойства не влияет на другое.
  • Что происходит при копировании непримитивного свойства, скажем, source.nonPrimitive, так это сохранение его ссылки в target.nonPrimitive. И это имеет большое значение, так как вы не перебирали его внутреннюю структуру! То, что копируется, — это всего лишь указатель, который ведет к нему! В результате они указывают на одно и то же место. Вот почему изменить что-то в одном — не имеет значения, является ли свойство примитивным или непримитивным — следует, заметив такое же изменение и во втором.

Давайте посмотрим на эти два в действии, изменив наши свойства source.object.string, source.object.array и source.number следующим образом:

source:  
number: 10
object:
  array: (3) [4, 5, 6]
  string: "I like them too!"
  __proto__: Object
__proto__: Object
target:  
number: 1
object:
  array: (3) [4, 5, 6]
  string: "I like them too!"
  __proto__: Object
__proto__: Object

Как видите, оба условия, о которых мы только что заявили, реальны. Пришло время раскрыть его имя, и это неглубокая копия объекта.
Возник вопрос, можем ли мы полностью отделить наши объекты, а не только примитивные свойства самого высокого уровня. , и ответ - да, мы могли бы. Все, что нам нужно сделать, это повторить то, что мы делали во время копирования целевого объекта. То есть создает для каждого из непримитивов всего объекта новую соответствующую структуру и помещает туда копии соответствующих свойств. Это выглядит так:

source === target:  false
source.object === target.object:  false
source:  
number: 1
object:
  array: (3) [1, 2, 3]
  string: "I like cookies"
  __proto__: Object
__proto__: Object
target:  
number: 1
object:
  array: (3) [1, 2, 3]
  string: "I like cookies"
  __proto__: Object
__proto__: Object

Несколько строк кода и готово. Мы применили правило, о котором говорили, к source.object и source.object.array, поскольку все они не являются примитивами, хранящимися в нашем исходном объекте.
Теперь давайте изменим его свойства так же, как мы делали это раньше.

source:  
number: 10
object:
  array: (3) [4, 5, 6]
  string: "I like them too!"
  __proto__: Object
__proto__: Object
target:  
number: 1
object:
  array: (3) [1, 2, 3]
  string: "I like cookies"
  __proto__: Object
__proto__: Object

Вуаля, мы получили независимую, полностью отделенную копию нашего исходного объекта! И в завершение, этот тип известен как глубокая копия объекта.
Теперь мы можем углубиться и посмотреть, какие параметры, предоставляемые JavaScript для работы, у нас есть.

  • : Структура, созданная с помощью const object = {}, не является полностью пустой, потому что то, что всегда вставляется самим JavaScript, является свойством прототипа, которое сообщает, на основе какого конструктора был создан наш объект.

Мелкие копии

Давайте начнем нашу небольшую беседу с одного истинного предложения:

Вы покупаете что-то по цене чего-то.

В случае неглубоких копий мы выигрываем во времени, и, как мы все знаем, оно равно производительности, которая является одним из основных ключей для любого приложения. Чтобы увидеть, как это проявляется, мы должны сказать, какова вычислительная сложность, и это не так сложно увидеть.
При этом мы перебираем только свойства самого высокого уровня — все, что вложено, не учитывается. Теперь, если их количество равно просто n, то мы копируем одно за другим все, что они хранят, поэтому вычислительная сложность равна O(n), что означает, что она линейна. Как вы сейчас увидите, это намного лучше, чем у глубокого копирования.
Мы знаем, в чем преимущество, поэтому пришло время поговорить о худшей части, и это то, что мы теряем. Если вы прошли введение, вы уже знаете ответ; в противном случае я быстро повторю это; Мы теряем независимость, потому что копирование любого не-примитива происходит по его ссылке. В результате мы получаем объект, в который мы копировали, и объекты, которые мы копировали, где каждый примитив самого высокого уровня полностью разделен туда и сюда, в отличие от непримитивов, которые имеют одну и ту же ссылку.

Давайте посмотрим, что JavaScript приготовил для нас в своем арсенале.

Объект.назначить

Умный план должен идти сверху вниз, поэтому мы должны начать с раскрытия его определения и шаг за шагом погружаться в его детали, таким образом:

Object.assign(target, ...sources);

Несколько вещей должны привлечь ваше внимание. Во-первых, это метод, определенный как часть объекта; следовательно, это не какой-то особый синтаксис, созданный и управляемый движком JavaScript и разрешаемый его интерпретатором как нечто уникальное, нет. Следовательно, все, что нужно для создания большого беспорядка, — это мало:

Object.assign = function() {
    console.log(`
        You cannot copy me.
        I am unique, get use to it.
`)};

Параметры в круглых скобках говорят нам, что мы можем передать несколько аргументов функции (параметр sources, которому предшествует расширение.) Первый — это объект, который мы копируем, а затем возвращаем. Начиная со второго, это те, из которых берутся скопированные свойства. Вся процедура основана на копировании всех из них с сохранением их порядка в качестве определителя, когда речь идет о копировании нескольких с одинаковым именем из двух разных источников. В таком случае последний всегда перезаписывает более ранние.

Это то, что мы можем мгновенно увидеть и почувствовать. Теперь мы должны перейти к этой скрытой части, реализации, и тем самым расширить наши прежние предположения.

  • Цель не может быть нулевой или неопределенной, иначе JavaScript выдает ошибку TypeError.
  • Что происходит, когда примитив передается цели, так это обертывание его с помощью ToObject(argument), который создает объект, представляющий этот тип примитива. Чтобы подчеркнуть факт, этот обернутый объект — это место, куда мы копируем и что возвращаем.
  • При выполнении операции копирования учитываются только перечисляемые собственные свойства источников. Заполняя пробел, enumerable является одним из флагов, которые каждое свойство имеет в своем дескрипторе — это другой объект, описывающий это свойство. Этот флаг устанавливается в значение true при создании свойства с помощью присваивания или определения, поэтому его можно посетить в цикле с помощью for…in. Однако любое свойство также может быть создано с помощью Object.defineProperty (O, P, Attributes), которое по умолчанию установит свой перечисляемый флаг в значение false, тем самым скрывая его от зацикливания.
    Nextly owns означает, что все, что было создано путем прямого доступа к объекту, — например, все из цепочки прототипов будет отрезано.
  • В случае возникновения ошибки, свойства, добавленные до ее возникновения в цель, останутся там. Цель не сбрасывается в состояние, которое было в начале. Иными словами, то, что удалось успешно скопировать, остается, а все, что последует, не получает своего шанса, поэтому не будет скопировано.
  • Object.assign, как следует даже из его названия, присваивает свойства определения, поэтому он вызывает геттеры и сеттеры для выполнения операции копирования, поскольку это то, что делает присваивание. Следствием этого факта является то, какие свойства цели и ее хранилища-цепочки прототипов, включая их дескрипторы, имеют большое значение! И при этом результат может отличаться от ожидаемого. Следующие примеры прояснят это.

Попытка скопировать свойство, соответствующее которому уже существует в целевом объекте, и для которого установлен флаг записи, равный false.
Мы создали целевой и исходный объекты, напрямую определяя одно и то же свойство с именем p. Но мы использовали Object.defineProperty для целевого, что означает, что его флаг дескриптора с возможностью записи по умолчанию установлен на false. Наконец, мы пытаемся с помощью Object.assign скопировать источник в цель, но затем возникает TypeError. Вопрос в том, почему?

Чтобы понять, давайте подумаем о том, что происходит за кулисами. И это Object.assign вызывает геттер для source.p, чтобы получить его значение, и когда это делается, вызывается соответствующий сеттер из целевого объекта. Но обратите внимание, что такой сеттер — либо напрямую, либо с его цепочкой прототипов — никогда не создавался, поэтому выбирается сеттер по умолчанию. А здесь подождите и читайте медленно. Когда сеттер пытается выполнить следующую операцию присваивания:

target.p = source.p;

что выяснилось, что у цели уже есть свойство с таким именем, у которого флаг записи установлен на false. Следовательно, он не может быть перезаписан, тем самым, имея любой выбор, он выдает ошибку.

Попытаться скопировать свойство, соответствующее которому в целевой цепочке прототипов уже существует, и для которого флаг доступности для записи установлен в значение false.
Вы можете сказать, что это большое дело, все, что появилось, потому что p непосредственный член целевого объекта. Хорошо, я понял тебя. Но в таком случае давайте взглянем на этот пример, где мы переместили его определение в targetParent и установили target.__proto__ для этого объекта. Таким образом, p больше не является непосредственным членом цели, но определен в своей цепочке прототипов. Как видите, ничего не изменилось, возникает та же ошибка.

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

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

У нас снова есть целевой и исходный объекты, но на этот раз мы не определяем p как дескриптор данных, вместо этого мы выбираем его как дескриптор доступности. В такой ситуации сеттер, определенный таким образом, возьмет на себя сеттер по умолчанию и, таким образом, будет выполнен, выполняя все, что содержится в его теле.

Когда дело доходит до копирования свойства p источника, сначала вызывается соответствующий геттер для получения значения, затем вызывается соответствующий сеттер. Однако на этот раз при проверке его существования было обнаружено, что цель определяет установщик для свойства p, следовательно, он выбран. Таким образом происходит только console.log(“Set from target”, value);, но присваивания нет, а значит свойство не будет скопировано!

Попытка скопировать свойство, доступный дескриптор которого в целевом объекте уже определен в его цепочке прототипов.
И чтобы выполнить то, что осталось, мы должны показать, что определение сеттера в цепочке прототипов также требует контроль над стандартным, поэтому это то, что мы делаем здесь. Мы переместили сеттер p на targetParent и установили target.__proto__ на этот объект. Это означает, что он определен не непосредственно в цели, а в своей цепочке прототипов. И, как показано ниже, он вызывался при попытке скопировать свойство p.

Я чувствую, что мы высосали из этого все, что в нем было. Чтобы сделать вас еще более чувствительным, помните, что в целом эти правила следуют операции присваивания, которую использует Object.assign.

Синтаксис спреда

Появился в ES6 и зарезервировал для этой цели три точки, за которыми следует target, который может быть массивом, строкой или объектом.

...target

Вы можете использовать его всякий раз, когда вам нужно расширить содержимое цели, поскольку то, что делает ..., — это извлечение каждого перечисляемого, которое там хранится.

Кто-то может сказать: «Я мог бы добиться того же, опираясь на нативную часть языка, поддерживаемую моими собственными функциями», и будет прав. Но то, что он получит, без сомнения, не будет самым гладким, самым элегантным и безопасным кодом. Что может быть более понятным при выполнении одного и того же действия, чем просто три точки? Мог ли он быть так уверен, что его собственный код завершен и все правильно обработал? Или что, его код случайно не перезапишется? Посмотрите еще раз на Object.assign и посмотрите, как легко его сломать. Здесь ситуация иная, так как спред не может быть перезаписан или поврежден с самого уровня кода. Ну, вы можете сделать свой собственный транспайлер (который будет играть роль хакера от MITM атаки) и внести туда деструктивные изменения, но ведь это не может быть сделано случайно, это должно быть осознанно действие. Все это было сказано, чтобы побудить вас использовать специальный синтаксис в вашей собственной реализации всякий раз, когда возможно достижение той же цели.

Теперь, прежде чем говорить о копировании объектов с разворотом, давайте поговорим о том, что еще мы можем использовать, чтобы получить некоторую информацию об этом.

Распространение итерации на параметры функции

Предполагая, что при вызове функции вы хотите распаковать каждое перечисляемое из итерируемого (подобного массиву) в параметры этой функции, такая вещь является хорошим моментом для использования спреда. Просто знайте, что iterable может быть массивом или строкой без каких-либо дополнительных усилий, но также может быть и объектом — если определен [Symbol.iterator] и его метод next.

Предположим, у нас есть следующая функция, параметры которой, начиная со второй, мы хотим заполнить значениями массива. Мы обсудим сразу три разных сценария. Таким образом, с распространением это будет выглядеть так:

Поскольку в каждом случае спред является вторым аргументом, он начинает заполнять параметры со второго. Таким образом, первый элемент соответствующего массива переходит ко второму параметру, второй — к третьему параметру и так далее. Наконец, может произойти один из трех сценариев:

  • argument.length может быть меньше количества параметров, тогда некоторые из них останутся неопределенными. Не хватило аргументов, чтобы заполнить их все — в том числе вытащенные из массива спредом.
  • Между argument.length и количеством параметров может быть тесно, и тогда каждый из них будет иметь какое-то значение.
  • И последнее, когда параметров на всех не хватает, то те, которые не подходят, доступны только по объекту-аргументу. Вы не заметите их ни в одном из своих штатных параметров. Именно здесь нам приходит на ум расширение следующего юзабилити, потому что мы можем использовать его как, как мы это называем, остальный параметр.

Спред как остаточный параметр функции

Нет гарантии, что функция каждый раз будет требовать фиксированное количество аргументов. Причин может быть много, однако реальный вопрос заключается в том, что мы можем с этим поделать.

Ну, можно было бы сказать: «Хорошо, я поддержу самый пессимистичный вариант». Проблема с таким подходом заключается в том, что наихудший случай может меняться со временем, и было бы странно иметь функцию с огромным количеством параметров.

Вместо этого кто-то другой будет использовать объект аргумента. Это лучшая идея. Однако он сохраняет все аргументы, а не только те, которые не подошли ни по одному из параметров, а нас интересуют только они. А вот и спред, который для этой цели называется остальным.

Давайте применим оставшийся параметр к ранее определенной функции с объяснением того, что делает JavaScript, когда находит параметр, которому предшествует синтаксис …. Одно раньше, остальное можно было использовать только один раз для каждой функции, и это должен быть последний параметр. Почему? Иначе было бы непонятно, где кончается одно и начинается другое, и когда останавливаться. Вернемся к объяснению. То, что делает JavaScript, когда узнает, что остаток предшествует последнему параметру, присваивается там массиву, членами которого являются эти дополнительные аргументы. Обратите внимание, что операция распаковки не требуется. В таком случае … — это просто способ сказать, что все, что нам нужно, — это массив, заполненный этими дополнительными аргументами. В нашей функции у нас есть четыре обычных параметра note, one, two, three. Таким образом, если бы было передано больше аргументов, чем это количество, то все они стали бы членами остального массива.

Как видите, каждый аргумент, начиная с пятого, помещается в массив rest. Обратите внимание, что если сейчас вам нужно будет распаковать такой массив, это можно сделать, применив спред.

Когда мы знаем, что, кроме всего прочего, распространение означает вынимать вещи из контейнера, мы можем продолжить копирование объектов.

Копировать объекты с разворотом

То, что делает разворот, когда применяется для создания копии объекта или группы объектов, — это определение, а не присвоение свойств. Поэтому, в отличие от Object.assign, здесь не участвует сеттер. Почему? Потому что исходный объект еще не существует. Object.assign работает с уже существующим объектом, который при попытках изменения всегда выполняет операцию присваивания, которая разрешается JavaScript с помощью сеттеров. С распространением все происходит как часть создания объекта. Присваивания нет, так как объект еще не сформирован! Далее, вы можете спросить, а какое творение вы имеете в виду? И это просто создание объекта литералом, потому что спред взаимодействует с этим.

Допустим, у нас есть пустой объект, созданный литералом, как показано ниже.

const obj = {
}; 

но, как мы знаем, ничего не мешает определить его начальными свойствами, поэтому давайте создадим его еще раз, применив некоторые из них.

const obj = {
    n: 1,
    s: 'cookies',
    f: () => { return 'function'; },
    o: {
        oS: "they will all be mine"
    }
}

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

const copy = {
    n: obj.n,
    s: obj.s,
    f: obj.f,
    o: obj.o
}

И есть по крайней мере две основные проблемы с таким подходом. Во-первых, может быть гораздо больше свойств, чем в нашем примере, а во-вторых, нам нужно знать имя каждого из них. Если бы был только способ распаковать содержимое объекта, не зная всех этих деталей. Ну а что с разбросом? Что, если мы просто воспользуемся этим?

const copy = {
    ...obj
}

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

  • Только перечисляемые собственные свойства могут быть скопированы с распространением, потому что только они могут быть извлечены распространением, тем самым опуская все остальные.
  • Объекты не единственные, которые могут быть использованы спредом в качестве источника, допустимы также массивы и строки. При этом ключ копируемого свойства совпадает с индексом его вхождения в такой источник.
  • У вас может быть несколько источников, хотя, если в них будет существовать свойство с одним и тем же ключом, более поздний перезапишет более ранние.
  • Распространение работает с литералом, что означает, что вы всегда получаете ссылку на новый объект. Другими словами, его нельзя применять для слияния объектов.

Глубокие копии

Используя соответствующую аналогию, то, что мы говорили до сих пор, напоминает дрейф на воде, но сейчас мы хотим сделать шаг на борт лодки, прыгнуть и нырнуть в те объекты, с которых мы копируем, так же, как ныряльщик. нырять в воду. Это связано с тем, что при выполнении глубокого копирования, как было сказано во вводной части, когда свойство копирования, расположенное на любом пути вдоль исходного объекта, содержит ссылку, должна быть создана новая структура, соответствующая структуре этого свойства, а затем копирование все, что у него есть, к вновь созданной структуре.

Далее, если бы у нас было достаточно воздуха в легких, что относится к временной сложности, мы могли бы даже коснуться дна. Но при принятии решения о том, чтобы идти так глубоко, необходимо учитывать здравый смысл. Так как, может быть, мы можем безопасно погрузиться только на полпути или даже меньше? То есть как дайвер мог переоценить свои возможности и тем самым убить себя, так и мы можем убить наше приложение, когда частота создания глубоких копий и вложенности их исходных объектов будет слишком велика, что приведет к лагам приложения. Итак, давайте столкнемся с ее временной сложностью и выясним, как ее можно решить.

В случае, когда мы обозначаем количество всех свойств на всех уровнях вложенности через n, мы можем прийти к неправильному выводу, что, как и в случае поверхностного копирования, временная сложность является линейной. Что ж, такой подход, несомненно, можно было бы назвать хорошей манипуляцией. Почему? Было очевидно, что когда мы говорили о мелком копировании, n имело в виду только количество свойств на самом высоком уровне, но теперь мы пытаемся отметить их общее количество по всему объекту. Используя одно и то же обозначение, мы должны использовать одно и то же толкование. Следовательно, мы должны обозначить число n как количество свойств на самом высоком уровне и найти способ описать количество свойств на более низких уровнях по отношению к этому n.

Начнем с изложения формулы его практической временной сложности, которая справедлива всегда, далее будет рассмотрен конкретный случай путем введения определенных допущений.

где последующие переменные:

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

Почему это так? По мере увеличения уровней вложенности нашего исходного объекта мы должны опускаться все ниже и ниже в его структуре, копируя все, что мы находим на этих более низких уровнях. Если мы встречаем примитивное свойство на каком-то пути, мы просто копируем хранящееся в нем значение. С непримитивными свойствами все не так просто. Потому что при столкновении с любым мы заботимся не о ссылке, которую он хранит, а обо всем, что там есть, поэтому мы должны создать новую структуру, соответствующую структуре такого свойства, а затем скопировать все, что у нее есть, имея в виду, что любой найденный непримитивность означает, что мы должны идти еще ниже.

Формула практической временной сложности также говорит, что теоретическая сложность имеет полиномиальный класс:

где W(n) зависит от распределения свойств исходного объекта.

Разумеется, справедливы следующие уравнения:

Стоит отметить, что добавление одного дополнительного оператора if-else или чего-то еще, что увеличило бы количество операций, влияет на практическую временную сложность следующим образом:

А теперь давайте поговорим об этом особом случае, о котором мы упоминали ранее. Для этого мы должны сделать одно предположение, что свойства всех объектов в нашем источнике имеют равномерное распределение. Это означает, что любые два не-примитива, расположенные в любом месте, будут иметь примерно одинаковое количество как примитивных, так и непримитивных свойств, тогда:

что означает, что наша исходная формула может быть упрощена до следующего вида:

который после короткого массажа дает:

где k указывает количество вложений.

Теперь перейдем от практической сложности к теоретической, которая заключается в определении класса алгоритма. Мы делаем это, определяя фактор, наиболее сильно влияющий на нашу формулу. Мы видим, что этот фактор:

таким образом, теоретическая вычислительная сложность имеет экспоненциальный класс:

Давайте теперь сравним это с поверхностным копированием, т.е.

а это значит, что при допущении о равномерном распределении свойств глубокое копирование превосходит мелкое на:

поэтому требуется гораздо больше времени, что и было нашей целью увидеть.

Помните, что практическая временная сложность, представленная в начале, применялась всегда, но чтобы что-то сказать о теоретической, нужно знать, как выглядит распределение свойств в исходном объекте. Зная это, вы можете превратить практическую сложность в теоретическую, правильно избавившись от сигма-обозначений из уравнения и правильно задав полином W(n).

Еще одна вещь, которую необходимо учитывать при игре с глубокими копиями, — это то, что мы называем циклической ссылкой. Чтобы объяснить, что это такое, давайте вернемся к нашему дайверу и предположим, что то, что он нашел во время погружения, — это вход, ведущий куда-то. Что ж, все мы знаем, что настоящий дайвер не упустит случая сунуть туда свой нос, и, конечно же, именно наш, так что он просто сделал это. Проходя по этой подводной пещере и любуясь красотами, созданными там природой, он заметил выход, а значит, пещера закончилась, и это его огорчило, так как он хотел провести там еще немного времени, но что он мог сделать? Так что, не имея выбора, он просто вышел, но здесь подождите, потому что, когда он это сделал и огляделся, что-то привлекло его внимание, и улыбка снова была на его лице, так как другой вход как раз рядом. Ну, мы все знаем, что сделал бы настоящий дайвер, так что, даже не думая, он просто вошел. И, честно говоря, именно это решение его просто убило. Так как его охватил прилив радости, он не заметил, что этот вход тот самый, в который он входил некоторое время назад. Это означает, что наш дайвер будет плавать, потому что каждый раз, когда он выходит из пещеры, он также снова входит туда. Следовательно, конец воздуха будет и нашим водолазным концом.

История того, что случилось с отважным ныряльщиком, о котором мы только что говорили, имеет одно общее значение с глубокой копией, а именно: нельзя допустить, чтобы свойство копирования указывало на объект, если какое-либо из его свойств указывает назад куда-то по пути следования. его существование. В противном случае возникнет бесконечный цикл (пока ресурсы не будут исчерпаны). Например, следующий код превысит максимальный размер стека вызовов, если для этого не будет реализована функция глубокого копирования.

И теперь мы можем представить единственный способ, который может использовать JavaScript для создания глубокой копии, не обращаясь ни к одному из его фреймворков.

JSON.parse(JSON.stringify(источник))

Здесь мы используем то, что глобальный объект JSON предоставляет два метода, которые позволяют нам взаимозаменяемо преобразовывать данные между JSON и JavaScript. Вся операция состоит из двух шагов. Сначала мы преобразуем объект или значение JavaScript в строку JSON, что выполняется с помощью метода stringify, затем возвращаемое значение передается в качестве аргумента методу разбора, который создает соответствующий объект или значение JavaScript, зависящее от тип источника.

Теперь пройдемся по всей операции, развернув появившиеся там детали. Прежде всего, строка JSON должна быть создана на основе нашего источника, поэтому она кажется хорошим выбором в качестве отправной точки для нашего обсуждения.

Преобразование значения JavaScript в строку JSON с помощью JSON.stringify(value, replacer, space)

Первое, что нужно понять, это то, что создание строки JSON требует от нас просмотра всех перечисляемых свойств параметра значения. Поэтому для его создания нам нужно проделать столько же операций, сколько требует сама глубокая копия!

Здесь появились две важные вещи. Мы имеем дело только с перечисляемыми свойствами, а это означает, что любое свойство, созданное с помощью Object.defineProperty, чье значение флага перечисления по умолчанию не было изменено, пропускается при создании строки JSON!

Как видите, в результирующей строке JSON отсутствует свойство p. И это происходит независимо от того, являются ли они свойствами самого высокого уровня или нет. Поскольку метод stringify глубоко проходит через весь объект, переданный в параметре value, и этот факт связан со второй вещью, которую мы упомянули, потому что при этом мы можем столкнуться с циклической ссылкой, но если мы это сделаем, метод stringify вернет соответствующая ошибка (TypeError).

Далее мы можем передать любой тип переменной методу stringify, и давайте еще раз подчеркнем слово any. Для каждого из них будет создана соответствующая часть JSON, которая обычно будет находиться во взаимно-однозначном отношении с переданным аргументом, но так будет не всегда. Причина такой ситуации прозаична. А именно, значения JSON не полностью покрывают набор значений JavaScript, то есть, если во время метода stringify будет обнаружен синтаксис JavaScript, который JSON не распознает, он не будет должным образом преобразован!

Итак, типы данных, которые stringify не преобразует должным образом: undefined, Symbol, function. Ну, без первых двух мы могли бы как-то выжить, но не имея возможности копировать функции? Для многих этот факт полностью дискредитирует данную форму копирования.

Теперь давайте посмотрим, как метод stringify преобразует эти типы. Мы рассмотрим три ситуации: при прямой передаче, позже в качестве члена массива и, наконец, в качестве члена объекта.

  • В этом случае он будет неопределенным. Это имеет смысл, потому что JSON не имеет эквивалента этих типов, поэтому в результирующую строку ничего не добавляется! Это похоже на создание переменной, но ничего ей не присваиваемое.
  • Как видно, если какой-либо из этих типов является членом массива, он устанавливается равным null.
  • И последнее, когда они принадлежат объекту, их просто опускают, как будто их вообще не было. И это было бы о параметре stringify value.

Мы можем перейти ко второму параметру, который называется заменителем, и какой из них, если он определен, также может привести к тому, что результирующая строка JSON будет отличаться от ожидаемой. Так что же это? Просто массив или функция, которая вызывает изменения в результирующей строке JSON.

  • Если он определен как массив, его элементы указывают, какие свойства параметра значения станут частью результирующей строки JSON.
  • Если она определена как функция, она должна принимать два параметра, соответствующие парам ключ-значение соответственно. Затем функция, определенная как заменитель, вызывается рекурсивно для каждого свойства, каждый раз возвращая значение, которое она присваивает соответствующему ключу в строке JSON. Конечно, это может привести к изменению этого значения, но с учетом ограничений формата JSON, а точнее того факта, что он не может сохранять некоторые типы JavaScript. Так проходит вся процедура, но, как всегда, есть и загвоздка. Дело в том, что изначально функция заменителя вызывается на объекте, который обернул переданный методу stringify в свойство, ключом которого является пустая строка «». Это означает, что с этого свойства начнется рекурсивное выполнение функции-заменителя. Следующий пример прояснит это.

Последний параметр делает именно то, на что указывает его имя, т. е. форматирует результат, добавляя к началу каждого свойства до 10 определенных в нем символов. Однако если мы хотим использовать строку, возвращенную из метода stringify, в качестве аргумента для метода parse, мы должны избегать букв, цифр и использовать только пробельные символы, иначе мы получим ошибку SyntaxError. .

Итак, на основе всего того, что мы только что обсудили, мы создаем строку JSON, которая становится аргументом метода разбора. Таким образом, все, что нам нужно, это построить скопированный объект. Давайте тогда.

Преобразование строки JSON обратно в значение JavaScript с помощью JSON.parse(текст, оживление)

Метод parse берет строку JSON из текстового параметра и, глубоко обрабатывая ее, возвращает значение или объект JavaScript, в зависимости от типа этой строки. Это означает, что если объект или массив будет закрыт в строке JSON, вы получите его глубокую копию. Если это строка, число, логическое значение или нуль, вместо этого возвращаемое значение будет соответствовать правильному типу. Однако стоит отметить, что при таком подходе нам пришлось удвоить требуемые усилия, потому что метод parse заставляет нас так же, как и stringify, пройтись по всем свойствам. И это все. Здесь нет лишнего подвоха, все так просто, как кажется. По крайней мере, пока не будет определен второй параметр.

Параметр оживления — это функция, которая, если она определена, вызывается непосредственно перед возвратом значения из метода синтаксического анализа, оборачивая его в свойство вновь созданного объекта, ключом которого является пустая строка «». А затем мы рекурсивно проходим по всем завернутым в него свойствам (весь наш исходный объект), каждый раз возвращая значение, которое затем присваивается свойству исходного объекта, ключевое имя которого совпадает с этим конкретный рекурсивный вызов. Просто упомянем, что вся процедура присвоения значений выполняется от самого вложенного к самому корневому элементу, потому что это рекурсивно вызываемый метод.

Таким образом, мы можем использовать нотацию JSON для создания глубокой копии объекта. Однако, учитывая ограничения, которые мы только что обсудили, т. е. потеря возможности копирования некоторых типов (функций!) или, по крайней мере, удвоение времени, необходимого для создания такой копии, не рекомендуется использовать Этот метод.

Если необходимо сделать глубокую копию, лучше сосредоточить свое внимание на методах, реализованных в различных фреймворках, или использовать тот, который доступен сразу с помощью Google.

Вывод

И вот все необходимые знания, необходимые для полного понимания способа «мышления» JavaScript о создании копий объектов.