Прототипы в 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. 1 Есть у самого объекта?Нашли → отдаём. dog.bark
  2. 2 Нет — смотрим прототипdog.eat → нашли в animal
  3. 3 Нет — прототип прототипаdog.toString → нашли в Object.prototype
  4. 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.

Частые вопросы

Что такое прототип простыми словами?
Объект, у которого другой объект заимствует свойства и методы. Если у объекта нет нужного свойства, JavaScript ищет его в прототипе, потом в прототипе прототипа — по цепочке. Так работает наследование в языке.
Чем __proto__ отличается от prototype?
prototype — свойство функции-конструктора: объект, который станет прототипом создаваемых экземпляров. proto — ссылка самого объекта на его прототип. Проще: prototype настраивают на конструкторе, proto есть у готового объекта.
Классы в JavaScript — это настоящие классы?
Нет, это синтаксический сахар над прототипами. class выглядит как в Java, но под капотом создаёт функцию-конструктор и кладёт методы в prototype. Нового механизма наследования не появилось.
Зачем знать про прототипы, если есть классы?
Чтобы понимать, что происходит на самом деле: почему методы общие для всех экземпляров, как работает instanceof, откуда у объекта методы, которых вы не писали. И чтобы читать код без классов.
Что такое цепочка прототипов?
Последовательность объектов, по которой идёт поиск свойства: сам объект → его прототип → прототип прототипа → … → null. Поиск останавливается на первом совпадении или доходит до конца цепочки.