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

Чтобы продемонстрировать, что я имею в виду, давайте рассмотрим быстрый пример:

let a = 1
let b = a
console.log(a)
console.log(b)
a = 2
console.log(a)
console.log(b)

Скопируйте и запустите приведенный выше код в консоли инструментов разработчика. Как, вероятно, и ожидалось, возвращаемые значения первых console.log(a) и console.log(b) оба были 1. Однако когда вы изменили значение a на 2, зарегистрированные значения были 2 и 1 соответственно. Причина этого в том, что значения, присвоенные выше, известны как «примитивы». В JavaScript существует шесть типов примитивных значений: undefined, null, boolean, string, symbol и номер. Примитивы — это типы данных, которые не являются объектами, не имеют методов и не могут быть изменены. Когда мы присваивали a =1, мы связывали нашу переменную a с местом, где примитив 1находился в памяти. Установка b = a также установила эту связь, связав переменную b с примитивом местоположения 1, находящимся в памяти. Помимо первоначального объявления, нет никакой другой связи, связывающей b с a. Вот почему, когда мы меняем примитив, назначенный a, с 1 на 2, b не изменяется.

Давайте рассмотрим другой пример:

let a = [1, 2, 3]
let b = a
console.log(a)
console.log(b)
a.pop()
console.log(a)
console.log(b)

Скопируйте и вставьте приведенный выше код, чтобы зарегистрировать ответ. Заметили что-нибудь интересное? Когда мы установили b = a и выполнили операцию над a, это также повлияло на b. Причина этого в том, что a назначается ссылочному типу данных. Когда переменной присваивается непримитивное значение (объект), ей дается ссылка на это значение. На самом деле переменная не содержит значения, а указывает на расположение объекта в памяти. Когда мы вызываем a.pop(), мы выполняем эту операцию не над a, а над значением, находящимся в месте, на которое ссылается a. Поскольку b также ссылается на то же место, мы видим, что на него также влияет операция a.pop().

Хорошо, давайте углубимся в суть этого сообщения в блоге с другим примером кода:

let a = [1, 2, 3]
let b = a.slice()
console.log(a)
console.log(b)
a.pop()
console.log(a)
console.log(b)

Когда приведенный выше код запускается, мы видим, что результаты очень похожи на наш пример с примитивным типом данных. Но как это может быть, если a и b присвоены эталонному значению? Причина этого в том, что мы фактически не устанавливаем b равным ссылочному местоположению a. Через slice() мы создаем копию объекта, на который ссылается a, и устанавливаем b равным его ссылочному местоположению. Объект, на который ссылается b, является новым, отдельным объектом, отличным от объекта, на который ссылается a.

Продолжим с другим примером:

let a = [[1, 2], [3, 4]]
let b = a.slice()
console.log(a)
console.log(b)
a[1].pop()
console.log(a)
console.log(b)

При запуске этого кода мы не получаем тех же результатов, что и в предыдущем примере. Причина этого в том, что мы делаем копию массива, элементы которого являются ссылочными значениями. Копирование массива с помощью slice() создает новый объект, но элементы этой копии по-прежнему указывают на одни и те же ссылочные значения. Использование slice() таким образом называется созданием поверхностной копии, поскольку уникальным образом создается только объект верхнего уровня. Чтобы создать новую, полностью уникальную копию, нам нужно создать так называемую глубокую копию, где каждый из вложенных элементов является либо примитивными значениями, либо новыми ссылочными объектами.

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

let a = [[1, 2], [3, 4]]
let b = JSON.parse(JSON.stringify(a))
console.log(a)
console.log(b)
a[1].pop()
console.log(a)
console.log(b)

Если вы чем-то похожи на меня, вы, вероятно, провели бесчисленное количество часов, ломая голову над вопросом, почему ваши данные манипулируют, когда деструктивные методы вызываются для разных переменных. Надеюсь, после прочтения этого поста вы поняли, что происходит под капотом, и почувствуете себя более уверенно при работе с типами данных JavaScript.