Базовый CS с нуля
Ссылки и значения
Ты видел, что присваивание записывает значение в ячейку. Но что именно является
«значением», которое записывается? Для числа вроде 42 ответ очевиден: паттерн битов
для 42. Скопируй его из одной ячейки в другую — каждая ячейка независимо хранит 42.
Теперь рассмотри объект: { x: 1, y: 2 }. Объект может быть большим — много полей,
вложенная структура. Среда выполнения не копирует всё это в одну ячейку при каждом
присваивании. Вместо этого она хранит компактную ссылку — по сути, адрес,
указывающий на то место, где живёт объект. Когда ты присваиваешь эту переменную другой,
обе переменные оказываются хранящими одну и ту же ссылку, указывающую на один и тот же
объект. Это называется алиасингом.
Следствие: мутация объекта через одно имя переменной немедленно видна через другое. Одна строка присваивания может вызвать неожиданное поведение, если не понимать, что два имени разделяют одну вещь.
Это различие — примитивные значения копируются, объекты передаются по ссылке — одно из важнейших знаний о том, как работают JavaScript и TypeScript.
После этого урока ты сможешь объяснить, почему присваивание примитива копирует значение, а присваивание объекта копирует только ссылку, предсказать результат алиасинга переменной на объект и последующей мутации через псевдоним, а также проследить листинг, смешивающий присваивания примитивов и объектов.
Что хранится в ячейке: значение или ссылка?
Ты уже знаешь, что переменная — это именованная ячейка памяти. Ячейка хранит некий паттерн битов. Для JavaScript/TypeScript ключевой вопрос: какой паттерн битов хранит ячейка при присваивании примитива и при присваивании объекта?
Объект — это составное значение, состоящее из именованных полей (свойств): например,
{ x: 1, y: 2 } — объект с двумя полями, x и y, каждое из которых хранит своё
значение. В отличие от примитива (например, числа), объект может быть сколь угодно
большим и никогда не умещается в одну ячейку памяти.
Примитивные значения — семантика копирования. Примитивное значение (число, строка,
булево, null, undefined) достаточно мало, чтобы поместиться непосредственно в ячейку.
При присваивании примитива переменной среда выполнения записывает фактические биты
значения в ячейку. Когда ты затем копируешь эту переменную в другую (let b = a),
среда выполнения копирует паттерн битов из одной ячейки в другую. Обе ячейки теперь
хранят независимые копии значения. Изменение одной не влияет на другую.
let a = 42;
let b = a; // ячейка b получает копию 42
a = 99; // ячейка a меняется; ячейка b всё ещё хранит 42Объекты и массивы — семантика ссылок. Объект ({ x: 1 }) или массив ([1, 2, 3])
— составная структура, живущая по некоторому адресу в отдельной области памяти,
называемой кучей (heap). Она может быть большой и не вмещается в одну ячейку.
Вместо этого ячейка переменной хранит ссылку — число, являющееся адресом объекта
в куче. В языках более низкого уровня это иногда называют «указателем».
При присваивании объекта второй переменной (let b = a) копируется ссылка (число-адрес)
— не сам объект. Теперь в ячейках a и b хранится один и тот же адрес. Они оба
указывают на один объект в куче. Это называется алиасингом: два имени для одной
вещи.
Алиасинг означает: мутируй через одно имя — увидишь через другое. Поскольку a
и b указывают на один объект, добавление поля через a.z = 5 означает, что b.z
тоже равен 5 — объект один.
const и ссылки на объекты. const obj = { x: 1 } означает, что переменная
obj не может быть перепривязана — нельзя написать obj = другойОбъект. Но объект,
на который указывает obj, не заморожен: obj.x = 99 — вполне допустимо. const
защищает привязку (какую ссылку хранит ячейка), а не содержимое объекта по этой ссылке.
1
// --- ПРИМИТИВ: семантика копирования ---
2
let a = 42;
3
let b = a; // ячейка b получает копию паттерна битов 42
4
a = 99; // ячейка a теперь хранит 99; ячейка b всё ещё хранит 42
5
console.log(b); // 42 — независимая копия, изменение a не влияет на b
6
7
// --- ОБЪЕКТ: семантика ссылок ---
8
let p = { x: 1, y: 2 }; // объект живёт в куче по адресу (допустим) 5000
9
let q = p; // ячейка q получает ту же ссылку: адрес 5000
10
// p и q — ПСЕВДОНИМЫ одного объекта
11
q.x = 99; // мутируем объект через q
12
console.log(p.x); // 99 — тот же объект, мутация видна через p
13
14
// --- const и ссылка ---
15
const obj = { val: 10 }; // ячейка obj хранит ссылку; привязка запечатана
16
// obj = { val: 20 }; // TypeError: нельзя перепривязать имя
17
obj.val = 20; // OK: сам объект не заморожен
18
console.log(obj.val); // 20
- L2 Ячейка a: паттерн битов для 42 (примитив)
- L3 Ячейка b получает копию этих битов. Две независимые ячейки, обе хранят 42.
- L4 Ячейка a меняется на 99. Ячейка b не затронута — у неё своя копия.
- L8 Объект выделен в куче. Ячейка p хранит адрес кучи (ссылку).
- L9 Ячейка q получает копию ссылки (тот же адрес). Оба указывают на один объект.
- L11 Мутация через q: изменяет объект по адресу 5000. p и q по-прежнему указывают туда.
- L12 p.x равен 99 — тот же объект был мутирован. Алиасинг сделал изменение видимым через p.
- L15 const запечатывает привязку: ячейка obj всегда хранит одну и ту же ссылку.
- L16 Перепривязка не удаётся: ячейка не может быть перезаписана другой ссылкой.
- L17 Мутация свойства объекта допустима: const охраняет только ссылку в ячейке, не содержимое объекта.
Проследим алиасинг: p и q разделяют один объект. Наблюдаем, как мутация через q
появляется через p.
1
let p = { x: 1 };
2
let q = p;
3
q.x = 99;
4
console.log(p.x);
Частая ошибка
Очень распространённая ошибка: предполагать, что let q = p создаёт копию объекта.
Это не так. Создаётся копия ссылки — копия числа-адреса. После этого и p, и q
хранят одинаковый адрес, и в куче по-прежнему существует только один объект. Чтобы
создать независимую копию объекта, нужно явно скопировать его:
let q = { ...p } (синтаксис spread создаёт поверхностную копию). Даже тогда
вложенные объекты внутри всё ещё будут разделяться — это разница между поверхностным
и глубоким копированием, тема для следующего урока.
Граничные случаи
Строки являются примитивами в JavaScript (это не объекты), но они также не фиксированного размера, как числа. Строки неизменяемы (immutable): каждая «мутация» строки на самом деле создаёт новое строковое значение, а переменная обновляется так, чтобы хранить новое строковое значение. Нельзя изменить символ внутри строки на месте так, как можно изменить поле внутри объекта. Вот почему строки ведут себя как примитивы (каждая переменная получает независимую копию при присваивании), хотя внутренне они хранятся как структурированные значения. Спецификация TC39 классифицирует string как примитивный тип.
let a = 10; let b = a; a = 99; Чему равно b?
let x = { n: 5 }; let y = x; y.n = 42; Чему равно x.n?
let p = { v: 1 }; let q = { v: 1 }; q.v = 7; Чему равно p.v?
let a = [1, 2, 3]; let b = a; b[0] = 99; Чему равно a[0]?
const c = { x: 0 }; c.x = 5; Чему равно c.x?
Почему мутация объекта через одно имя переменной также меняет то, что видит другая переменная?
При присваивании примитивного значения (число, строка, булево и т.д.) фактический
паттерн битов копируется в ячейку-получатель. Каждая переменная хранит независимую
копию; изменение одной не влияет на другую. При присваивании объекта или массива
копируется ссылка — адрес объекта в куче, — а не сам объект. Две переменные,
хранящие одну и ту же ссылку, являются псевдонимами (aliases): они разделяют один
объект. Мутация этого объекта через любое из имён немедленно видна через другое.
const запечатывает привязку — ссылка в ячейке не может быть заменена — но не
замораживает содержимое объекта. Строки являются примитивами и ведут себя по семантике
копирования, даже несмотря на то, что не являются значениями фиксированного размера.