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

Возьмите этот пример:

let apple = "apple";
let pear = apple
pear = "pear"
console.log("apple: ", apple);
console.log("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" // memory address: 0x001
const variable2 = 100     // memory address: 0x002

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

const variable1 = "Hello" // points to memory address: 0x001
const variable2 = 100     // points to memory address: 0x002
console.log(variable1)

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

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

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

const name = "Jesse"
const age = 30

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

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

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

// primitive value
const name = "Jesse"               // memory address: 0x001
// reference value               
const person = { name: "Jesse" }   // memory address: 0x002
// primitive copy points to new memory address
const nameCopy = name              // memory address: 0x003
// reference copy points to orginial memory address
const personCopy = person          // memory address: 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 = {               // memory address: 0x001
 apples: 3
}
fruitStandCopy = fruitStand // memory address: 0x001
fruitStandCopy.apples = 7

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

В том, что мы сделали, нет ничего плохого. Вот как JavaScript обрабатывает копирование объектов.

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

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

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

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

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

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

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

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. Может быть странно видеть поведение, разложенное перед вами. Со временем это станет для вас второй натурой, и вы сможете избегать коварных ошибок, которые поверхностные и глубокие копии могут внести в кодовую базу. Я знаю по собственному опыту, что это может быть одним из самых сложных для отладки, и чем быстрее вы зафиксируете их в памяти, тем лучше вы будете в долгосрочной перспективе.

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Присоединяйтесь к нашему сообществу Discord.