Прототипное наследование

прототипы≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

В JavaScript объект может «подсматривать» свойства у другого объекта: если своего свойства нет, поиск уходит выше — по скрытой ссылке [[Prototype]]. Это и есть прототипное наследование: не копирование, а делегирование.

Разделы темы:·

Прочитать прототип — Object.getPrototypeOf(obj), задать существующему объекту — Object.setPrototypeOf(obj, proto). А если объект только создаётся, удобнее сразу родить его с нужным прототипом: Object.create(proto).

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Вы часто встретите в старом коде obj.__proto__ — это исторический геттер-сеттер для того же самого поля. Он работает в браузерах и никуда не денется ради совместимости, но в спецификации помечен как устаревший. В новом коде пишите Object.getPrototypeOf и Object.create.

__proto__ — обычное на вид имя свойства, и это его беда. Если вы складываете в объект данные, пришедшие извне (например, ключи из формы или JSON), то ключ "__proto__" вместо записи данных попытается сменить объекту прототип. Отсюда целый класс уязвимостей — «загрязнение прототипа».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Прототип сам может иметь прототип — получается цепочка. Движок идёт по ней снизу вверх и останавливается на первом объекте, где нужное имя нашлось. Если цепочка кончилась, а свойства нет — результат undefined, без всякой ошибки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Отличить «своё» от «унаследованного» помогает hasOwnProperty или более современный Object.hasOwn(obj, key). Оператор in, наоборот, смотрит всю цепочку целиком — это разные вопросы и разные ответы.

Object.keys вернёт только собственные ключи, а for...in переберёт ещё и унаследованные перечислимые. Свойства из Object.prototype в перебор не попадают: они помечены как неперечислимые.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Из того же правила следует поведение this в унаследованных методах. Метод может лежать сколь угодно высоко в цепочке, но this внутри него — это объект перед точкой, тот, у которого метод вызвали. Поэтому один метод в прототипе спокойно обслуживает сотни объектов, работая с данными каждого.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Объектный литерал {} выглядит пустым, но пустым не является: его прототипом автоматически становится Object.prototype. Оттуда берутся toString, hasOwnProperty, valueOf и прочая базовая обвязка. А прототип самого Object.prototype — это null, то есть конец цепочки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Так же устроены и остальные встроенные типы: у массива в цепочке стоит Array.prototype (с map, push и остальными), а уже за ним — Object.prototype. Отсюда правило: чем длиннее цепочка, тем дальше идти движку, но искать он будет ровно до первого совпадения.

ЗначениеЦепочка прототипов
{}Object.prototype → null
[]Array.prototype → Object.prototype → null
function f(){}Function.prototype → Object.prototype → null
Object.create(null)null

Object.create(null) создаёт объект, у которого [[Prototype]] сразу равен null. У него нет ни toString, ни hasOwnProperty — вообще ничего унаследованного. Такой объект нельзя вывести через alert и склеить со строкой, зато он идеален как словарь: любые ключи от пользователя, никаких сюрпризов.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Если чистый словарь нужен часто, есть готовый инструмент — Map. Он и ключи любого типа держит, и порядок вставки сохраняет. А Object.create(null) остаётся тогда, когда нужен именно обычный объект, но без наследства.

  • У каждого объекта есть скрытая ссылка [[Prototype]] — на другой объект или на null.
  • Читать прототип — Object.getPrototypeOf, создавать с прототипом — Object.create; __proto__ устарел и опасен при работе с внешними данными.
  • Чтение свойства идёт вверх по цепочке до первого совпадения, иначе undefined; запись всегда попадает в сам объект.
  • this в унаследованном методе — это объект перед точкой, а не тот, где метод лежит.
  • Цепочка обычного объекта заканчивается на Object.prototype, а Object.create(null) даёт чистый словарь вообще без наследства.

1/3Что выведет код? const a = { x: 1 }; const b = Object.create(a); b.x = 2; console.log(a.x);

2/3В прототипе объекта есть свойство size. Что вернёт obj.hasOwnProperty("size")?

3/3Чему равен Object.getPrototypeOf(Object.prototype)?

Счётчик на общем прототипе

Два объекта наследуют от одного прототипа со счётчиком и методом inc. Не запуская код, скажите, что выведет последняя строка — и объясните, почему proto.count оказался именно таким.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Выведет 2 1 0.

Строка this.count++ — это на самом деле this.count = this.count + 1: сначала чтение, потом запись. Чтение при первом вызове уходит по цепочке в proto и возвращает 0, а запись, как всегда, происходит в сам объект — у a появляется собственное count со значением 1. Второй вызов уже читает и пишет только своё свойство, поэтому у a получается 2, у b — 1.

Прототип не менялся ни разу, его count так и остался 0. Общими у объектов оказались методы, а данные — у каждого свои. Если бы счётчик правда должен был быть общим, менять пришлось бы явно: proto.count++.

Длина цепочки прототипов

Напишите функцию chainLength(obj), которая возвращает число звеньев прототипов до null. Для обычного объекта это 1 (только Object.prototype), для объекта без прототипа — 0.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Достаточно идти вверх по цепочке, пока Object.getPrototypeOf не вернёт null, и считать шаги.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

У массива звеньев два: Array.prototype и за ним Object.prototype. Проверять условие удобно именно через !== null, а не на «ложность»: сравнение с null точно описывает конец цепочки и не спутает его ни с чем другим.