Что такое прототипное наследование в JavaScript?
Суть прототипного наследования
В JavaScript нет классического наследования через классы, как в C++ или Java. Вместо этого каждый объект имеет внутреннюю ссылку [[Prototype]] (доступную через __proto__ или Object.getPrototypeOf()), которая указывает на другой объект — его прототип. Когда вы обращаетесь к свойству или методу объекта, движок сначала ищет его в самом объекте, а если не находит — поднимается по цепочке прототипов вверх.
Цепочка прототипов заканчивается на Object.prototype, чей прототип равен null. Если свойство не найдено нигде в цепочке, возвращается undefined.
Способы создания наследования
Через Object.create()
Самый прямой способ установить прототип:
const animal = {
eat() {
console.log(`${this.name} ест`);
}
};
// dog наследует от animal
const dog = Object.create(animal);
dog.name = 'Рекс';
dog.eat(); // "Рекс ест"
Через функции-конструкторы
При вызове функции с new создаётся объект, чей прототип — это Constructor.prototype:
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} ест`);
};
const cat = new Animal('Мурзик');
cat.eat(); // "Мурзик ест"
// Прототип cat указывает на Animal.prototype
console.log(Object.getPrototypeOf(cat) === Animal.prototype); // true
Через классы (синтаксический сахар)
Классы ES2015 — это обёртка над прототипным наследованием:
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} ест`);
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name} лает`);
}
}
const rex = new Dog('Рекс');
rex.eat(); // унаследовано от Animal
rex.bark(); // собственный метод Dog
Как работает поиск свойств
Допустим, у нас есть цепочка: obj -> A.prototype -> Object.prototype -> null. При вызове obj.toString():
- Движок ищет
toStringв самомobj— не находит. - Идёт в
A.prototype— не находит. - Идёт в
Object.prototype— находит и вызывает.
Особенности и подводные камни
- Запись свойств не идёт по цепочке: присваивание
obj.x = 1всегда создаёт собственное свойство вobj, а не модифицирует прототип. - Методы перекрывают прототипные: если в объекте есть своё свойство с тем же именем, оно "затеняет" прототипное (shadowing).
- Изменение прототипа на лету (
Object.setPrototypeOf) — медленная операция и ломает оптимизации движка. - Все объекты в JS наследуют от
Object.prototype, кроме созданных черезObject.create(null).
Проверка наследования
class A {}
class B extends A {}
const b = new B();
console.log(b instanceof B); // true
console.log(b instanceof A); // true
console.log(A.prototype.isPrototypeOf(b)); // true
Что хочет услышать интервьюер
Понимание разницы между [[Prototype]], __proto__ и Constructor.prototype
Знание, что класс — это синтаксический сахар над прототипами
Объяснение поиска свойств вверх по цепочке прототипов до null
Знание способов создания наследования: Object.create, new, class extends
Понимание, что запись свойств не идёт по цепочке, а создаёт собственное свойство
Пример: Цепочка прототипов через Object.create
// Базовый объект
const vehicle = {
start() {
console.log(`${this.brand} заводится`);
}
};
// car наследует от vehicle
const car = Object.create(vehicle);
car.drive = function() {
console.log(`${this.brand} едет`);
};
// sportsCar наследует от car
const sportsCar = Object.create(car);
sportsCar.brand = 'Ferrari';
sportsCar.turbo = function() {
console.log(`${this.brand} включает турбо`);
};
sportsCar.turbo(); // собственный метод
sportsCar.drive(); // унаследован от car
sportsCar.start(); // унаследован от vehicle
// Проверка цепочки
console.log(Object.getPrototypeOf(sportsCar) === car); // true
console.log(Object.getPrototypeOf(car) === vehicle); // true
Пример: Наследование через функцию-конструктор
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
return `${this.name} ест`;
};
function Dog(name, breed) {
// Вызываем родительский конструктор
Animal.call(this, name);
this.breed = breed;
}
// Устанавливаем цепочку прототипов
Dog.prototype = Object.create(Animal.prototype);
// Восстанавливаем правильный constructor
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
return `${this.name} лает`;
};
const rex = new Dog('Рекс', 'Лабрадор');
console.log(rex.eat()); // "Рекс ест"
console.log(rex.bark()); // "Рекс лает"
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
Пример: Современный синтаксис через class
class Shape {
constructor(color) {
this.color = color;
}
describe() {
return `Фигура цвета ${this.color}`;
}
}
class Circle extends Shape {
constructor(color, radius) {
// Обязательный вызов родительского конструктора
super(color);
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
// Переопределение метода с вызовом родительского
describe() {
return `${super.describe()}, радиус ${this.radius}`;
}
}
const c = new Circle('красный', 5);
console.log(c.describe()); // "Фигура цвета красный, радиус 5"
console.log(c.area().toFixed(2)); // "78.54"
Типичные ошибки
Путать __proto__ (свойство экземпляра) и prototype (свойство функции-конструктора)
Считать, что классы в JS реализуют классическое наследование, а не прототипное
Думать, что изменение прототипа объекта затронет только этот объект, а не все объекты с тем же прототипом
Использовать Object.setPrototypeOf в горячем коде, не зная о потере производительности
Забывать вызывать super() в конструкторе наследника при использовании class extends


