Одно из фундаментальных отличий объектов от примитивов заключается в том, что объекты хранятся и копируются «по ссылке», тогда как примитивные значения: строки, числа, логические значения и т. д. — всегда копируются «как целое значение».
Это легко понять, если мы немного заглянем внутрь того, что происходит, когда мы копируем значение.
Начнем с примитива, такого как строка.
Здесь мы помещаем копию message
в phrase
:
let message = "Привет!"; пусть фраза = сообщение;
В результате у нас есть две независимые переменные, каждая из которых хранит строку "Hello!"
.
Вполне очевидный результат, не так ли?
Объекты не такие.
Переменная, присвоенная объекту, хранит не сам объект, а его «адрес в памяти» — другими словами, «ссылку» на него.
Давайте посмотрим на пример такой переменной:
пусть пользователь = { имя: «Джон» };
И вот как это на самом деле хранится в памяти:
Объект хранится где-то в памяти (справа на картинке), а user
переменная (слева) имеет на него «ссылку».
Мы можем думать об объектной переменной, такой как user
, как о листе бумаги с адресом объекта.
Когда мы выполняем действия с объектом, например, берем свойство user.name
, движок JavaScript смотрит, что находится по этому адресу, и выполняет операцию над реальным объектом.
Теперь вот почему это важно.
При копировании объектной переменной ссылка копируется, но сам объект не дублируется.
Например:
let user = { name: "Джон" }; пусть администратор = пользователь; // копируем ссылку
Теперь у нас есть две переменные, каждая из которых хранит ссылку на один и тот же объект:
Как видите, есть еще один объект, но теперь с двумя переменными, которые на него ссылаются.
Мы можем использовать любую переменную для доступа к объекту и изменения его содержимого:
пусть пользователь = {имя: 'Джон'}; пусть администратор = пользователь; admin.name = 'Пит'; // изменено ссылкой "admin" оповещение(имя_пользователя); // 'Пит', изменения видны по ссылке "пользователь"
Это как если бы у нас был кабинет с двумя ключами и мы использовали один из них ( admin
), чтобы проникнуть в него и внести изменения. Затем, если мы позже воспользуемся другим ключом ( user
), мы все равно откроем тот же шкаф и сможем получить доступ к измененному содержимому.
Два объекта равны, только если они являются одним и тем же объектом.
Например, здесь a
и b
ссылаются на один и тот же объект, поэтому они равны:
пусть а = {}; пусть б = а; // копируем ссылку Предупреждение (а == б); // правда, обе переменные ссылаются на один и тот же объект Предупреждение (а === б); // истинный
И здесь два независимых объекта не равны, хоть и похожи друг на друга (оба пусты):
пусть а = {}; пусть б = {}; // два независимых объекта Предупреждение (а == б); // ЛОЖЬ
Для сравнений типа obj1 > obj2
или сравнения с примитивом obj == 5
объекты преобразуются в примитивы. Совсем скоро мы изучим, как работают преобразования объектов, но, честно говоря, такие сравнения нужны очень редко – обычно они возникают в результате программной ошибки.
Константные объекты могут быть изменены
Важным побочным эффектом хранения объектов в качестве ссылок является то, что объект, объявленный как const
может быть изменен.
Например:
константный пользователь = { имя: «Джон» }; user.name = "Пит"; // (*) оповещение(имя_пользователя); // Пит
Может показаться, что строка (*)
вызовет ошибку, но это не так. Значение user
является постоянным, оно всегда должно ссылаться на один и тот же объект, но свойства этого объекта могут изменяться.
Другими словами, const user
выдает ошибку только в том случае, если мы попытаемся установить user=...
в целом.
Тем не менее, если нам действительно нужно создать постоянные свойства объекта, это тоже возможно, но с использованием совершенно других методов. Мы упомянем об этом в главе «Флаги и дескрипторы свойств».
Таким образом, копирование объектной переменной создает еще одну ссылку на тот же объект.
Но что, если нам нужно дублировать объект?
Мы можем создать новый объект и реплицировать структуру существующего, перебирая его свойства и копируя их на примитивном уровне.
Так:
пусть пользователь = { имя: «Джон», возраст: 30 }; пусть клон = {}; // новый пустой объект // скопируем в него все пользовательские свойства for (введите пользователя) { клон[ключ] = пользователь[ключ]; } // теперь клон — полностью независимый объект с тем же содержимым clone.name = "Пит"; // изменили в нем данные оповещение(имя_пользователя); // все еще Джон в исходном объекте
Мы также можем использовать метод Object.assign.
Синтаксис:
Object.assign(назначение, ... источники)
Первый аргумент dest
— это целевой объект.
Дальнейшие аргументы — это список исходных объектов.
Он копирует свойства всех исходных объектов в целевой dest
, а затем возвращает их как результат.
Например, у нас есть объект user
, добавим ему пару разрешений:
let user = { name: "Джон" }; пусть разрешения1 = {canView: true}; пусть разрешения2 = {canEdit: true}; // копируем все свойства из разрешений1 и разрешений2 в пользователя Object.assign(пользователь, разрешения1, разрешения2); // теперь user = { name: "Джон", canView: true, canEdit: true } оповещение(имя_пользователя); // Джон оповещение (user.canView); // истинный предупреждение (user.canEdit); // истинный
Если скопированное имя свойства уже существует, оно перезаписывается:
let user = { name: "Джон" }; Object.assign(user, { name: "Пит" }); оповещение(имя_пользователя); // теперь user = { name: "Пит" }
Мы также можем использовать Object.assign
для выполнения простого клонирования объекта:
пусть пользователь = { имя: «Джон», возраст: 30 }; пусть клон = Object.assign({}, пользователь); оповещение(клон.имя); // Джон оповещение(клон.возраст); // 30
Здесь он копирует все свойства user
в пустой объект и возвращает его.
Существуют также другие методы клонирования объекта, например, использование расширенного синтаксиса clone = {...user}
, который рассматривается позже в этом руководстве.
До сих пор мы предполагали, что все свойства user
примитивны. Но свойства могут быть ссылками на другие объекты.
Так:
пусть пользователь = { имя: «Джон», размеры: { рост: 182, ширина: 50 } }; оповещение(user.sizes.height); // 182
Теперь недостаточно скопировать clone.sizes = user.sizes
, поскольку user.sizes
является объектом и будет скопирован по ссылке, поэтому clone
и user
будут иметь одинаковые размеры:
пусть пользователь = { имя: «Джон», размеры: { рост: 182, ширина: 50 } }; пусть клон = Object.assign({}, пользователь); предупреждение(user.sizes === clone.sizes); // правда, тот же объект // размеры общего ресурса пользователя и клона user.sizes.width = 60; // изменяем свойство из одного места оповещение(clone.sizes.width); // 60, получаем результат от другого
Чтобы исправить это и сделать user
и clone
действительно отдельными объектами, мы должны использовать цикл клонирования, который проверяет каждое значение user[key]
и, если это объект, затем также копирует его структуру. Это называется «глубоким клонированием» или «структурированным клонированием». Существует метод StructuredClone, реализующий глубокое клонирование.
Вызов structuredClone(object)
клонирует object
со всеми вложенными свойствами.
Вот как мы можем использовать его в нашем примере:
пусть пользователь = { имя: «Джон», размеры: { рост: 182, ширина: 50 } }; пусть клон = структурированныйClone (пользователь); предупреждение(user.sizes === clone.sizes); // ложь, разные объекты // пользователь и клон теперь совершенно не связаны user.sizes.width = 60; // изменяем свойство из одного места оповещение(clone.sizes.width); // 50, не связаны
Метод structuredClone
может клонировать большинство типов данных, таких как объекты, массивы, примитивные значения.
Он также поддерживает циклические ссылки, когда свойство объекта ссылается на сам объект (напрямую или через цепочку или ссылки).
Например:
пусть пользователь = {}; // давайте создадим циклическую ссылку: // user.me ссылается на самого пользователя user.me = пользователь; пусть клон = структурированныйClone (пользователь); оповещение(clone.me === клон); // истинный
Как видите, clone.me
ссылается на clone
, а не user
! Таким образом, циклическая ссылка также была клонирована правильно.
Хотя бывают случаи, когда structuredClone
дает сбой.
Например, когда объект имеет свойство функции:
// ошибка структурированныйКлон({ е: функция() {} });
Свойства функции не поддерживаются.
Чтобы справиться с такими сложными случаями, нам может потребоваться использовать комбинацию методов клонирования, написать собственный код или, чтобы не изобретать велосипед, взять существующую реализацию, например _.cloneDeep(obj) из библиотеки JavaScript lodash.
Объекты назначаются и копируются по ссылке. Другими словами, переменная хранит не «значение объекта», а «ссылку» (адрес в памяти) на это значение. Поэтому копирование такой переменной или передача ее в качестве аргумента функции копирует эту ссылку, а не сам объект.
Все операции через скопированные ссылки (например, добавление/удаление свойств) выполняются над одним и тем же объектом.
Чтобы создать «настоящую копию» (клон), мы можем использовать Object.assign
для так называемой «мелкой копии» (вложенные объекты копируются по ссылке) или функцию «глубокого клонирования» structuredClone
, или использовать пользовательскую реализацию клонирования, например как _.cloneDeep(obj).