Урок по треку JavaScript 8 мин

Прототипы и классы за 8 минут

Цепочка прототипов, new, class, extends, super и приватные поля.

В 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) выполняет четыре шага:

  1. Создаёт пустой объект.
  2. Ставит ему прототип F.prototype.
  3. Вызывает F с this, равным этому объекту.
  4. Если 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.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.