← назад к разделу

В JavaScript нет классического наследования, как в Java или C#, — есть прототипы. Синтаксис class появился лишь в 2015-м и под капотом разворачивается в тот же прототипный механизм. Пока вы пишете простые классы, этого можно не знать; как только видите hasOwnProperty, метод «из ниоткуда» или странности this — без прототипов не разобраться.

Цепочка: где ищутся свойства

У каждого объекта есть скрытая ссылка на другой объект — его прототип. Чтение свойства идёт по цепочке: нет у самого объекта — смотрим в прототип, потом в прототип прототипа, и так до null.

const animal = { eats: true };
const rabbit = Object.create(animal); // прототип rabbit — animal
rabbit.jumps = true;

rabbit.jumps; // true  — своё свойство
rabbit.eats;  // true  — найдено В ПРОТОТИПЕ
rabbit.toString(); // работает — найдено в Object.prototype, конце цепочки

Ключевое слово — делегирование: rabbit не скопировал eats, он делегирует поиск наверх. Поменяете animal.eats — изменится и то, что видит rabbit. Свойств у объекта по-прежнему одно (jumps), что видно инструментам:

Object.keys(rabbit);                 // ["jumps"] — только свои
"eats" in rabbit;                    // true — in смотрит всю цепочку
Object.hasOwn(rabbit, "eats");       // false — а это только свои

Отсюда практика: перебирая чужие объекты, отличайте «своё» от «унаследованного» — Object.keys/Object.entries дают только собственные свойства, оператор in и цикл for..in заглядывают в цепочку.

Запись работает иначе, чем чтение: присваивание rabbit.eats = false не изменит animal — оно создаст собственное свойство eats у rabbit, которое затенит прототипное. Цепочка — только для чтения.

prototype и proto: два разных слова

Главная терминологическая путаница языка:

  • __proto__ (официально — [[Prototype]], читается через Object.getPrototypeOf(obj)) — та самая скрытая ссылка любого объекта на его прототип.
  • prototype — обычное свойство, которое есть только у функций: объект, который станет прототипом экземпляров, созданных через new.
function User(name) { this.name = name; }
User.prototype.greet = function () { return `Привет, ${this.name}`; };

const u = new User("Аня");
Object.getPrototypeOf(u) === User.prototype; // true — вот связь
u.greet(); // "Привет, Аня" — метод найден в прототипе

new делает четыре вещи: создаёт пустой объект, ставит ему прототипом Fn.prototype, вызывает функцию с this = новый объект, возвращает его. Методы при этом существуют в одном экземпляре на всех — в прототипе, а не копируются в каждый объект. Это и есть экономия, ради которой всё затевалось.

class — это прототипы в красивой одежде

class User {
  constructor(name) { this.name = name; }
  greet() { return `Привет, ${this.name}`; }
}

Этот код делает то же самое, что пример с функцией выше: greet лежит в User.prototype, экземпляры делегируют к нему. extends выстраивает цепочку из двух прототипов. Проверить легко:

typeof User;                        // "function" — класс это функция
Object.hasOwn(new User("x"), "greet"); // false — метод не в экземпляре

Что class добавляет по-настоящему: обязательность new (вызов без него — ошибка), строгий режим внутри, приватные поля #field, аккуратный синтаксис super. Чего не добавляет: никакого «настоящего классового» наследования — под капотом всё то же делегирование. Практическое следствие: метод класса, переданный как колбэк, теряет this точно так же, как любая функция, — класс от этого не спасает.

Когда это стреляет на практике

  • Метод «из ниоткуда»: [].at, "".padStart — всё из прототипов (Array.prototype, String.prototype). Поэтому же патчить встроенные прототипы нельзя: добавив своё в Array.prototype, вы меняете поведение всех массивов приложения и чужих библиотек.
  • for..in по массиву перебирает и унаследованное, и индексы как строки — для массивов только for..of или методы массива.
  • JSON и прототипы: JSON.parse создаёт объекты с обычным прототипом, а вот Object.create(null) даёт объект вообще без прототипа — «чистый словарь» без toString, идеален для карт «ключ-значение», где ключи приходят снаружи.
  • Проверка типа: instanceof идёт по цепочке прототипов, поэтому «сломается», если объект пришёл из другого окна/iframe — там свои встроенные прототипы.

Коротко

  • Чтение свойства идёт по цепочке прототипов до null; запись всегда создаёт собственное свойство. Это делегирование, не копирование.
  • __proto__/[[Prototype]] — ссылка любого объекта на прототип; prototype — свойство функции, из которого new берёт прототип для экземпляров.
  • Методы живут в прототипе в одном экземпляре на всех — потому Object.keys их не видит, а in видит.
  • class — синтаксис поверх того же механизма: методы в prototype, extends — цепочка. Потеря this у метода-колбэка никуда не девается.
  • Не патчить встроенные прототипы; для «чистых словарей» — Object.create(null) или Map.

Что почитать дальше

  • this: контекст вызова — вторая половина головоломки «методы и объекты».
  • Объекты: ссылки, копирование и неизменяемость — следующая тема цикла.
  • Замыкания — альтернативный прототипам способ разделять поведение и состояние.