Поверхностное и глубокое копирование в JavaScript

Не все копии в JavaScript одинаковы. Дело в том, что скопированная переменная в этом языке классифицируется как поверхностная и глубокая копия.

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

Для начала рассмотрим несколько типичных примеров копирования переменных.

Возьмем такой пример:

let apple = "apple";
let pear = apple

pear = "pear"

console.log("apple: ", apple);
console.log("pear: ", pear);

Если вы предположили, что вывод для apple  —  это “apple”, а для pear  —  “pear”, то вы правы. Теперь рассмотрим другой вариант:

const fruitStand = {
 apples: 3
}

const fruitStandCopy = fruitStand;

fruitStandCopy.apples = 7

console.log("Fruit Stand: ", fruitStand.apples);
console.log("Fruit Stand Copy: ", fruitStandCopy.apples);

Можно подумать, что выводом для fruitStand.apples будет 3, а для fruitStandCopy.apples  —  7. Однако и для fruitStand.apples, и для fruitStandCopy.apples вывод будет один и тот же  —  7. Подобные странности в поведении JavaScript  —  пример того, как в этом языке обрабатываются глубокие и поверхностные копии.

Память

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

const variable1 = "Hello" // адрес памяти: 0x001
const variable2 = 100 // адрес памяти: 0x002

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

const variable1 = "Hello" // указывает на адрес памяти: 0x001
const variable2 = 100     // указывает на адрес памяти: 0x002

console.log(variable1)

В приведенном выше примере при ссылке на variable1 в console.log JavaScript не поднимается на 3 строки вверх к месту создания variable1 и не проверяет ее значение. Он обращается к адресу памяти, связанному с variable1.

Примитивы и ссылки

С точки зрения глубокого и поверхностного копирования можно создавать два вида переменных. Первый  —  это примитив. Его основные типы: строка, число, булево значение, неопределенное значение и null.

const name = "Jesse"
const age = 30

Второй вид  —  переменная-ссылка, известная как объект.

const person = {
name: "Jesse",
age: 30
}

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

// примитивное значение
const name = "Jesse"               // адрес памяти: 0x001

// значение ссылочной переменной            
const person = { name: "Jesse" }   // адрес памяти: 0x002

// примитивная копия указывает на новый адрес памяти
const nameCopy = name              // адрес памяти: 0x003

// ссылочная копия указывает на первоначальный адрес памяти
const personCopy = person          // адрес памяти: 0x002

Поверхностные копии

Вернемся к этому примеру:

const fruitStand = {
 apples: 3
}

const fruitStandCopy = fruitStand;

fruitStandCopy.apples = 7

console.log("Fruit Stand: ", fruitStand.apples);
console.log("Fruit Stand Copy: ", fruitStandCopy.apples);

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

const fruitStand = {               // адрес памяти: 0x001
 apples: 3
}

fruitStandCopy = fruitStand // адрес памяти: 0x001

fruitStandCopy.apples = 7

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

Именно так JavaScript работает с копированием объектов.

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

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

Как следует из предыдущих примеров, примитивные переменные получают новые адреса памяти при копировании. Технически, это пример глубокого копирования, однако термин “глубокое копирование” обычно используется для копирования ссылочных переменных.

К сожалению, нельзя получить такое поведение автоматически при копировании ссылочной переменной. Дело в том, что размер объекта неизвестен, и в JavaScript считается более эффективным по умолчанию выполнять поверхностное копирование объектов.

Создание глубоких копий

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

Если объект, который нужно скопировать, находится на глубине только одного уровня, для создания глубокой копии рекомендуется использовать оператор spread.

const fruitStand = {
 apples: 3
}
const fruitStandCopy = {...fruitStand};

fruitStandCopy.apples = 7

console.log(fruitStand.apples)      // 3
console.log(fruitStandCopy.apples)  // 7

Для более сложных объектов стоит применять комбинацию JSON.parse() и JSON.stringify():

const fruitStand = {
 apples: 3
}

const fruitStandCopy = JSON.parse(JSON.stringify(fruitStand))

fruitStandCopy.apples = 7

console.log(fruitStand.apples)      // 3
console.log(fruitStandCopy.apples)  // 7

Однако эта техника рискованна, поскольку функции внутри исходного объекта не будут скопированы.

Для создания глубоких копий также можно использовать сторонние библиотеки, такие как lodash. Если вы работаете над большим приложением, то, вероятно, они уже установлены для других методов.

Подведение итогов

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

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Jesse Langford: Understand Shallow and Deep Copies in JavaScript

Предыдущая статьяКак использовать GitLab в качестве реестра Helm-чартов
Следующая статья3 распространенные ошибки при поиске работы в области науки о данных в 2022 году