Ссылки и сравнение объектов

С числами, строками и другими простыми значениями присваивание ведёт себя привычно: значение можно положить во вторую переменную, а затем менять обе независимо:

numbers.jsjs
let a = 5;let b = a;b = 6;console.log(a);console.log(b);
Консоль
5
6

Когда выполнилось let b = a, значение 5 скопировалось в b. После этого у каждой переменной своё значение: в a лежит 5, в b – 6. С объектами похожая запись приводит к неожиданному результату:

two-names.jsjs
const user = {  name: 'Ира',};const admin = user;admin.name = 'Макс';console.log(user.name);
Консоль
Макс

Мы меняли admin.name. Почему же изменился user.name?

Объект не копируется при присваивании

Когда выполняется литерал { name: 'Ира' }, JavaScript создаёт один объект. Для понимания удобно представлять, что переменная user хранит ссылку на него – возможность обратиться именно к этому объекту.

Теперь выполним const admin = user. Новый объект здесь не создаётся: в admin попадает ссылка на тот же самый объект. Поэтому строка admin.name = 'Макс' не изменяет какой-то отдельный объект admin – она идёт по ссылке к общему объекту и меняет его. После этого и user.name, и admin.name читают одно и то же свойство одного объекта.

Две переменные не означают два объекта.

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

Два имени – один объект
const user = { name: 'Ира' };
Литерал { ... } создаёт объект. user получает ссылку на него.
const admin = user;
admin.name = 'Макс';
console.log(user.name);
Консоль
Нет вывода в консоль.

Имён может быть сколько угодно, а объект по-прежнему один:

three-names.jsjs
const user = { name: 'Ира' };const admin = user;const owner = user;owner.name = 'Лена';console.log(user.name);console.log(admin.name);
Консоль
Лена
Лена
user admin owner { name: 'Лена' }
Это не три копии пользователя, а три пути к одному объекту.

Изменить объект и сменить ссылку – разные действия

Возьмём let, чтобы переменную можно было переназначить. Сначала обе переменные ведут к одному объекту, и изменение свойства через admin меняет общий объект. Но совсем другое происходит, когда переменной admin присваивают новый литерал:

reassign.jsjs
let user = {  name: 'Ира',};let admin = user;// меняется общий объектadmin.name = 'Макс';// новый объект: admin теперь указывает на негоadmin = {  name: 'Лена',};console.log(user.name);console.log(admin.name);
Консоль
Макс
Лена

Литерал { name: 'Лена' } создаёт новый объект, а admin начинает указывать уже на него. Старый объект никуда не переместился – изменилась только связь между переменной admin и объектом:

Первая запись меняет объект, вторая – только связь admin с объектом.

Отсюда важное различие: admin.name = 'Макс' меняет объект, на который указывает admin, а admin = { name: 'Макс' } меняет значение самой переменной admin – теперь она указывает на другой объект.

Новый литерал – новый объект

two-literals.jsjs
const a = { name: 'Ира' };const b = { name: 'Ира' };

Каждое выполнение литерала { ... } создаёт отдельный объект. Хотя содержимое выглядит одинаково, объектов здесь два. Одинаковые свойства ещё не делают их одним объектом – и именно это важно при сравнении.

Сравнение объектов

Для чисел и строк === отвечает на привычный вопрос: одинаковы ли значения.

primitives-equal.jsjs
console.log(5 === 5);console.log('Ира' === 'Ира');
Консоль
true
true

С объектами вопрос другой:

objects-equal.jsjs
const a = { name: 'Ира' };const b = { name: 'Ира' };console.log(a === b);
Консоль
false

Почему false, хотя объекты выглядят одинаково? Потому что литерал { ... } выполнился дважды и создал два разных объекта. При сравнении объектов === проверяет: это один и тот же объект или два разных? Теперь другая ситуация:

same-object.jsjs
const a = { name: 'Ира' };const c = a;console.log(a === c);
Консоль
true

Здесь обе переменные ведут к одному объекту, поэтому результат true:

a { name: 'Ира' } b { name: 'Ира' } a === b → false a c { name: 'Ира' } a === c → true
Два литерала – два объекта; вторая ссылка – тот же объект.

=== не сравнивает свойства объекта

Выражение { name: 'Ира' } === { name: 'Ира' } даёт false не потому, что JavaScript «не заметил» одинаковые свойства. Просто === вообще не занимается таким сравнением. Для объектов он отвечает на другой вопрос: слева и справа тот же самый объект? Если нужно узнать, одинаковы ли данные внутри двух разных объектов, свойства сравнивают отдельно:

compare-props.jsjs
const a = { name: 'Ира' };const b = { name: 'Ира' };console.log(a.name === b.name);
Консоль
true

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

Примитив копируется, объект – нет

Теперь можно вернуться к примеру из начала урока. После let b = a у нас два независимых значения: 5 в a и ещё одно 5 в b. Если поменять b, a не изменится. После const admin = user второй объект не создаётся: обе переменные ведут к одному.

примитив: два независимых значения
let a = 5;let b = a;// два значения: 5 и 5
объект: один общий объект
const user = { name: 'Ира' };const admin = user;// один объект, две ссылки

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

Почему язык устроен именно так

Число, строка или булево значение – маленькие значения фиксированного размера, и меняться после создания они не могут. Скопировать такое значение при присваивании дёшево, а копия ничем не отличается от оригинала – поэтому язык просто копирует его.

Объект – другое дело. Он может быть большим: десятки свойств, вложенные объекты, а внутри них ещё объекты. И он меняется по ходу работы программы. Если бы каждое присваивание создавало полную копию, программа тратила бы память и время на копирование при каждом =, а разные части кода не видели бы изменений друг друга: одна часть поменяла бы свою копию, другая продолжала бы работать со своей.

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

// теперь вы знаете

  1. При const admin = user новый объект не создаётся: обе переменные указывают на один объект
  2. Изменение свойства через одну ссылку видно через остальные ссылки на тот же объект
  3. admin.name = … изменяет объект, а admin = { … } перенаправляет переменную на другой объект
  4. Каждый новый литерал { … } создаёт новый объект
  5. Для объектов === проверяет, один ли это объект: одинаковые свойства сами по себе равенства не дают

Проверьте себя

Что выведет программа?

const player = {
score: 10,
};
const copy = player;
copy.score = 20;
console.log(player.score);

Что появится в консоли?

let cart = { items: 1 };
const saved = cart;
cart = { items: 1 };
console.log(cart === saved);
saved.items = 2;
console.log(cart.items);

Сколько объектов создаётся при выполнении этого кода?

const start = { count: 0 };
let last = start;
for (let i = 1; i <= 3; i++) {
last = { count: i };
}
const copy = last;

В объекте catalog тысяча свойств. Верно ли, что const copy = catalog; выполняется дольше, чем такое же присваивание объекта с одним свойством?

Что выведут два сравнения?

const player = { score: 10 };
const copy = player;
const other = { score: 10 };
console.log(player === copy);
console.log(player === other);