В 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: контекст вызова — вторая половина головоломки «методы и объекты».
- Объекты: ссылки, копирование и неизменяемость — следующая тема цикла.
- Замыкания — альтернативный прототипам способ разделять поведение и состояние.