Прототипы в JavaScript: наследование под капотом
Содержание
О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».
Прототипное наследование — то, что делает объектную модель JavaScript непохожей на Java и C++. Понять его один раз — значит перестать удивляться, откуда у объектов методы, которых вы не писали.
Главная идея: заимствование по цепочке
Прототип — это объект, у которого другой объект берёт то, чего нет у него самого.
const animal = {
eat() { return 'ем'; },
};
const dog = Object.create(animal); // прототип dog — это animal
dog.bark = function () { return 'гав'; };
dog.bark(); // "гав" — своё
dog.eat(); // "ем" — взято из прототипа animal!
У dog нет метода eat, но вызов работает: не найдя eat у себя, JavaScript ищет его в прототипе — в animal — и находит. Это и есть наследование в языке: не копирование, а заимствование по ссылке.
Цепочка прототипов
Поиск не останавливается на первом прототипе — он идёт дальше:
dog.toString(); // работает, хотя мы не писали toString ни у dog, ни у animal
dog → animal → Object.prototype → null
-
1
Есть у самого объекта?Нашли → отдаём.
dog.bark -
2
Нет — смотрим прототип
dog.eat→ нашли вanimal -
3
Нет — прототип прототипа
dog.toString→ нашли вObject.prototype -
4
Дошли до nullНе нашли нигде →
undefined
Любой объект в конце цепочки упирается в Object.prototype (откуда toString, hasOwnProperty), а тот — в null. Отсюда объекты «умеют» методы, которых вы им не давали: они унаследованы от Object.prototype.
prototype против proto
Два похожих слова, которые постоянно путают.
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function () { return 'гав'; }; // метод для всех собак
const rex = new Dog('Рекс');
rex.bark(); // "гав"
rex.__proto__ === Dog.prototype; // true
prototype — свойство функции-конструктора. Это объект, который станет прототипом всех экземпляров, созданных через new. Туда кладут общие методы.
__proto__ — свойство самого объекта, ссылка на его прототип. У rex нет метода bark, но rex.__proto__ указывает на Dog.prototype, где bark есть.
Проще запомнить так: prototype настраивают на конструкторе (заготовка), __proto__ читают у готового объекта (ссылка). В современном коде вместо __proto__ используют Object.getPrototypeOf(obj).
Методы общие, а не скопированы
Важное следствие для понимания и памяти:
const a = new Dog('А');
const b = new Dog('Б');
a.bark === b.bark; // true — ОДИН И ТОТ ЖЕ метод!
Метод bark не копируется в каждый экземпляр — он один, лежит в Dog.prototype, и все собаки на него ссылаются. Тысяча объектов — одна функция bark, а не тысяча копий. Это экономит память и объясняет, почему методы кладут в прототип, а не создают в конструкторе.
Классы — это сахар
Современный код пишут на классах, и они выглядят как в Java:
class Dog {
constructor(name) {
this.name = name;
}
bark() { return 'гав'; }
}
Но это ровно то же прототипное наследование, просто с человеческим синтаксисом. Под капотом class Dog создаёт функцию-конструктор Dog, а метод bark кладёт в Dog.prototype — в точности как в примере выше вручную. Никакого нового механизма классы не принесли: они спрятали прототипы за привычной записью.
Поэтому знать прототипы полезно даже при работе с классами: instanceof, наследование через extends, super — всё это операции над цепочкой прототипов. А ещё вы встретите код без классов, где прототипы видны напрямую.
Где это всплывает на практике
Прототипы — не только теория:
instanceofпроверяет, есть лиConstructor.prototypeв цепочке объекта;- расширение встроенных типов (
Array.prototype.myMethod = ...) добавляет метод всем массивам — мощно и опасно, так делать не стоит; - «чистый» объект без прототипа —
Object.create(null)создаёт объект безObject.prototype, безtoStringи прочего: безопаснее как словарь-хранилище; - отладка — когда
obj.someMethod is not a function, вопрос в том, есть ли метод в цепочке прототипов.
Своё vs унаследованное
Иногда нужно отличить собственные свойства объекта от унаследованных:
const dog = Object.create({ inherited: 1 });
dog.own = 2;
dog.hasOwnProperty('own'); // true — своё
dog.hasOwnProperty('inherited'); // false — из прототипа
Object.keys(dog); // ['own'] — только свои
'inherited' in dog; // true — есть в цепочке
hasOwnProperty и Object.keys смотрят только на собственные свойства, а оператор in и for...in — по всей цепочке. Путаница между ними — источник багов при переборе объектов. Подробнее — про методы объектов.
Смежные темы: классы — сахар над этим; объекты — создание и работа; this и контекст. Полный список — в уроках JavaScript.