В JavaScript нет классов в смысле Java: наследование устроено через прототипы, а class это удобный синтаксис поверх них. Чтобы отвечать на вопросы уверенно, нужно понимать, что происходит под капотом.
Прототип и цепочка
У каждого объекта есть скрытая ссылка [[Prototype]] на другой объект или null. Когда вы читаете свойство, движок ищет его в самом объекте, затем в прототипе, затем в прототипе прототипа и так до null.
const animal = { eats: true };
const rabbit = Object.create(animal); // прототип rabbit это animal
rabbit.jumps = true;
rabbit.jumps; // true, своё свойство
rabbit.eats; // true, найдено в прототипе
Object.getPrototypeOf(rabbit) === animal; // true
Запись работает иначе: rabbit.eats = false создаст собственное свойство в rabbit, прототип не изменится. Исключение: если в прототипе есть сеттер, вызовется он.
Доступ к прототипу: Object.getPrototypeOf(obj) и Object.setPrototypeOf(obj, proto). Свойство __proto__ устаревшее, но работает. Менять прототип у уже созданного объекта медленно, лучше задавать его при создании через Object.create.
prototype у функций
Не путайте две вещи:
obj.[[Prototype]](черезObject.getPrototypeOf) есть у любого объекта;F.prototypeэто обычное свойство функций-конструкторов. Оно становится прототипом объектов, созданных черезnew F().
function User(name) { this.name = name; }
User.prototype.hi = function () { return 'Привет, ' + this.name; };
const u = new User('Аня');
u.hi(); // метод найден в User.prototype
Object.getPrototypeOf(u) === User.prototype; // true
User.prototype.constructor === User; // true
Методы кладут в прототип, чтобы они были одни на все экземпляры, а не копировались в каждый.
Что делает new
new F(args) выполняет четыре шага:
- Создаёт пустой объект.
- Ставит ему прототип
F.prototype. - Вызывает
Fсthis, равным этому объекту. - Если
Fвернула объект, результатом будет он; иначе новый объект.
Классы
class Animal {
#secret = 'приватно'; // приватное поле
static count = 0; // статическое поле, живёт на самом классе
name; // публичное поле экземпляра
constructor(name) {
this.name = name;
Animal.count++;
}
speak() { return `${this.name} издаёт звук`; } // в Animal.prototype
get title() { return this.name.toUpperCase(); } // геттер
static create(name) { return new Animal(name); }
}
class Dog extends Animal {
constructor(name) {
super(name); // обязателен до обращения к this
this.tricks = [];
}
speak() { return super.speak() + ': гав'; }
}
Что важно знать про классы:
typeof Animal === 'function', методы лежат вAnimal.prototype, как в ручной версии.- Класс нельзя вызвать без
new: TypeError. - Тело класса всегда в строгом режиме.
- Объявление класса поднимается, но находится в TDZ, как
let. - Методы класса неперечисляемые:
for...inпо экземпляру их не покажет. - В конструкторе наследника нельзя трогать
thisдоsuper(): ReferenceError. extendsнастраивает две цепочки:Dog.prototypeнаследуетAnimal.prototype, а самDogнаследуетAnimal(поэтому статические методы тоже наследуются).
Приватные поля
#field это настоящая приватность на уровне языка, а не соглашение с подчёркиванием. Снаружи обращение obj.#field это SyntaxError ещё при разборе. Проверить наличие можно через #field in obj. Приватные поля не наследуются в смысле доступа: метод наследника не видит #secret родителя.
Порядок инициализации
В базовом классе поля инициализируются в начале конструктора. В наследнике поля создаются сразу после возврата из super(). Поэтому, если конструктор родителя вызывает переопределённый метод, а тот читает поле наследника, поле ещё undefined.
instanceof и проверки
obj instanceof F проверяет, есть ли F.prototype где-то в цепочке прототипов obj. Это не проверка «создан конструктором F»: если подменить F.prototype, результат изменится.
Object.hasOwn(obj, 'key') (или obj.hasOwnProperty('key')) проверяет только собственные свойства, оператор in ищет по всей цепочке.
Object.create(null)
Создаёт объект без прототипа: у него нет toString, hasOwnProperty и прочего. Используют как «чистый словарь», куда безопасно класть ключи вроде __proto__. Сейчас чаще берут Map.
Частые ошибки
- Путать
__proto__объекта иprototypeфункции. - Класть массивы и объекты в прототип: они станут общими для всех экземпляров.
- Обращаться к
thisв наследнике доsuper(). - Думать, что
classвводит новую модель наследования.
Что спросят на собесе
- Что такое прототипное наследование, как ищется свойство.
- Чем
__proto__отличается отprototype. - Что делает оператор
newпошагово. - Чем класс отличается от функции-конструктора, что делает
super. - Как работают приватные поля и
instanceof.