Разбор прототипного наследования, функций-конструкторов, классов ES6 и связи между ними в JavaScript.
Прототипы и классы в JavaScript
JavaScript — это язык с прототипным наследованием, в котором ключевое слово class появилось лишь как синтаксический сахар над уже существовавшим механизмом.
В отличие от языков с классическим наследованием (Java, C++, C#), где объекты создаются по описанию класса, JavaScript изначально строился на другом принципе: один объект может служить образцом для другого. Этот образец называется прототипом (prototype). Понимание прототипов важно не только для чтения чужого кода, но и для осознанного выбора между функциями-конструкторами, Object.create и классами ES6.
Синтаксис class, добавленный в стандарте ES2015, не отменил прототипы — он лишь сделал работу с ними привычнее для разработчиков, пришедших из других языков. Внутри класса по-прежнему лежит цепочка прототипов, а методы живут в свойстве prototype. Поэтому разговор о классах без разговора о прототипах был бы неполным.
Ниже последовательно разбираются: что такое прототип, как работает цепочка прототипов, чем отличаются разные способы создания объектов и где синтаксис классов расходится с обычными функциями.
Что такое прототип
Прототип — это обычный объект, на который ссылается другой объект через внутреннее свойство [[Prototype]]. Когда у объекта запрашивают свойство, которого у него нет, движок ищет его в прототипе, затем в прототипе прототипа и так далее, пока цепочка не оборвётся на null.
Ссылку на прототип можно получить через Object.getPrototypeOf и установить через Object.setPrototypeOf. Исторически для этого использовалось свойство __proto__, но оно считается устаревшим и не рекомендуется в новом коде.
const animal = { eats: true };
const rabbit = { jumps: true };
Object.setPrototypeOf(rabbit, animal);
console.log(rabbit.jumps); // true — своё свойство
console.log(rabbit.eats); // true — найдено в прототипе
console.log(rabbit.flies); // undefined — нет нигде в цепочке
Важно различать свойство prototype у функций и внутреннее [[Prototype]] у объектов. Это разные вещи, и их часто путают.
Цепочка прототипов
Поиск свойства по цепочке — основной механизм повторного использования кода в JavaScript. Цепочка заканчивается на Object.prototype, чей прототип равен null. Отсюда у любого объекта доступны методы вроде toString и hasOwnProperty.
Проверить, лежит ли свойство в самом объекте, а не в прототипе, помогает hasOwnProperty или Object.hasOwn.
const base = { greet() { return "hi"; } };
const obj = Object.create(base);
obj.name = "test";
console.log(obj.greet()); // "hi" — из прототипа
console.log(Object.hasOwn(obj, "name")); // true
console.log(Object.hasOwn(obj, "greet")); // false
Обращение к свойству по цепочке — операция чтения. Запись всегда создаёт или изменяет свойство в самом объекте, даже если такое же имя есть в прототипе.
Функции-конструкторы
До появления классов объекты одного «типа» создавали через функции-конструкторы, вызываемые с оператором new. У каждой такой функции есть свойство prototype — объект, который станет прототипом для создаваемых экземпляров.
Оператор new выполняет четыре действия: создаёт новый объект, связывает его с prototype функции, вызывает функцию с этим объектом в роли this и возвращает результат, если функция не вернула объект явно.
function User(name) {
this.name = name;
}
User.prototype.sayHi = function () {
return "Hi, " + this.name;
};
const u = new User("Anna");
console.log(u.sayHi()); // "Hi, Anna"
console.log(u instanceof User); // true
Методы намеренно размещают на prototype, а не внутри конструктора: тогда все экземпляры делят одну функцию, а не получают копию при каждом создании.
Классы ES6
Синтаксис class даёт более компактную запись того же механизма. Методы, объявленные в теле класса, попадают в prototype автоматически, а конструктор вызывается при new.
class User {
constructor(name) {
this.name = name;
}
sayHi() {
return "Hi, " + this.name;
}
}
const u = new User("Anna");
console.log(u.sayHi()); // "Hi, Anna"
console.log(typeof User); // "function"
Несмотря на внешнее сходство с классами других языков, здесь нет статической типизации и нет отдельной сущности «класс» во время выполнения. Класс — это функция, а его методы — свойства prototype.
Наследование через extends и super
Ключевое слово extends связывает прототипы двух классов, а super даёт доступ к родительскому конструктору и методам. Это та же цепочка прототипов, только записанная декларативно.
class Animal {
constructor(name) { this.name = name; }
speak() { return this.name + " makes a sound"; }
}
class Dog extends Animal {
speak() { return this.name + " barks"; }
parentSpeak() { return super.speak(); }
}
const d = new Dog("Rex");
console.log(d.speak()); // "Rex barks"
console.log(d.parentSpeak()); // "Rex makes a sound"
При extends прототип Dog.prototype ссылается на Animal.prototype. Это позволяет переопределять методы и при необходимости вызывать родительскую версию через super.
Различия классов и функций-конструкторов
Синтаксис классов не полностью эквивалентен функциям-конструкторам. Есть несколько заметных отличий, которые важно учитывать.
- Классы нельзя вызывать без
new— это выбросит ошибку. - Тело класса всегда выполняется в строгом режиме.
- Методы класса неперечисляемы (non-enumerable), в отличие от присвоенных вручную.
- Классы не поднимаются (no hoisting) так, как функции: обратиться к ним до объявления нельзя.
Эти ограничения делают поведение классов более предсказуемым, но одновременно менее гибким по сравнению с ручным управлением прототипами.
Когда что использовать
Выбор между классами и прототипами — предмет споров. Одни считают, что классы снижают порог входа и делают код единообразным. Другие указывают, что они скрывают реальную модель языка и провоцируют перенос привычек из классических ООП-языков.
Практически: для создания множества однотипных объектов с общими методами удобны классы. Для делегирования поведения между отдельными объектами и композиции без иерархий лучше подходит Object.create и прямое связывание прототипов. Оба подхода опираются на один и тот же механизм.
Что запомнить
- В основе JavaScript лежит прототипное наследование, а не классическое.
- Свойство
prototypeу функции и внутреннее[[Prototype]]у объекта — разные вещи. - Поиск свойства идёт по цепочке прототипов до
null. - Оператор
newсвязывает создаваемый объект сprototypeфункции. - Классы ES6 — синтаксический сахар над прототипами, а не новая модель.
extendsиsuperработают через ту же цепочку прототипов.- Классы нельзя вызывать без
newи они выполняются в строгом режиме. - Методы на
prototypeобщие для всех экземпляров, что экономит память. - Для композиции и делегирования уместны
Object.createи ручное связывание прототипов.