Перейти к содержанию
Educora
Продвинутый20 мин16 / 27

Прототипы и наследование

Посмотри, что на самом деле скрывается за `class`: цепочка прототипов, `Object.create`, функции-конструкторы и почему классы называют «синтаксическим сахаром».

Проверь себя
В этом уроке ты узнаешь
  • Объяснять, как поиск свойства идёт по цепочке прототипов до Object.prototype и null
  • Создавать объекты через Object.create и функции-конструкторы, вызванные с new
  • Показывать, что class построен на прототипах, и отличать собственные свойства от унаследованных

У каждого массива есть метод map, у каждой строки — toUpperCase, хотя ты никогда не записывал эти методы в свои массивы. Откуда они берутся? В JavaScript у каждого объекта есть скрытая ссылка на другой объект — его прототип. Если свойства нет в самом объекте, JavaScript ищет его в прототипе. Встроенные методы, наследование и классы построены именно на этом простом механизме.

Цепочка прототипов

Прототип объекта читают через Object.getPrototypeOf(obj) (старая запись __proto__ тоже работает, но не рекомендуется). При чтении obj.x JavaScript сначала проверяет собственные свойства объекта, затем прототип, затем прототип прототипа и так далее — пока не дойдёт до null. Если свойства нигде нет, результат — undefined. Запись работает иначе: obj.x = ... всегда создаёт или меняет собственное свойство и не трогает прототип.

JavaScript
const animal = {
  eats: true,
  describe() {
    return `${this.name} eats: ${this.eats}`;
  },
};

const cat = Object.create(animal);
cat.name = 'Tom';

console.log(cat.describe());
console.log(Object.getPrototypeOf(cat) === animal);
console.log(Object.hasOwn(cat, 'name'), Object.hasOwn(cat, 'eats'));

cat.eats = false;
console.log(cat.eats, animal.eats);

console.log(Object.getPrototypeOf(animal) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype));
▸ Ожидаемый результат
Tom eats: true
true
true false
false true
true
null
cat.eats = false создало собственное свойство, которое «затеняет» унаследованное, — animal не изменился. Цепочка: cat → animal → Object.prototype → null.

Встроенные объекты устроены так же. Цепочка массива: массив → Array.prototype → Object.prototype → null. map находится не в самом массиве, а в Array.prototype. Оператор in проверяет всю цепочку, а Object.hasOwn — только собственные свойства. У Array.prototype есть свой toString, который затеняет Object.prototype.toString.

JavaScript
const numbers = [1, 2, 3];

console.log(Object.getPrototypeOf(numbers) === Array.prototype);
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype);
console.log(Object.hasOwn(numbers, 'map'), 'map' in numbers);
console.log(numbers.toString === Object.prototype.toString);
console.log(numbers.toString());
▸ Ожидаемый результат
true
true
false true
false
1,2,3

Функции-конструкторы и new

До появления class объекты одинаковой структуры создавали с помощью функций-конструкторов. Когда обычную функцию вызывают с new, JavaScript делает четыре шага:

  1. создаёт пустой объект;
  2. делает его прототипом Fn.prototype;
  3. выполняет функцию, где this — новый объект;
  4. возвращает новый объект, если функция не вернула другой объект.

Методы записывают в Fn.prototype: тогда все экземпляры делят одну копию метода, а не носят каждый свою. В программе с тысячей объектов это заметная экономия памяти.

JavaScript
function Student(name, grade) {
  this.name = name;
  this.grade = grade;
}

Student.prototype.introduce = function () {
  return `${this.name}, grade ${this.grade}`;
};

const leyla = new Student('Leyla', 9);
const elvin = new Student('Elvin', 10);

console.log(leyla.introduce(), '|', elvin.introduce());
console.log(leyla.introduce === elvin.introduce);
console.log(Object.getPrototypeOf(leyla) === Student.prototype);
console.log(leyla);
▸ Ожидаемый результат
Leyla, grade 9 | Elvin, grade 10
true
true
Student { name: 'Leyla', grade: 9 }

Классы — синтаксический сахар

class (ES2015) позволяет записать ту же структуру понятнее: класс на самом деле — функция, его методы попадают в ClassName.prototype, а extends связывает прототипы между собой. Есть и отличия: код класса всегда в строгом режиме, класс нельзя вызвать без new, методы не видны в for...in, а #приватные поля существуют только в классах.

JavaScript
class Shape {
  constructor(name) { this.name = name; }
  describe() { return `I am a ${this.name}`; }
}

class Circle extends Shape {
  constructor(radius) {
    super('circle');
    this.radius = radius;
  }
  area() { return +(Math.PI * this.radius ** 2).toFixed(2); }
}

const c = new Circle(2);
console.log(typeof Shape, c.describe(), c.area());
console.log(Object.getPrototypeOf(Circle.prototype) === Shape.prototype);
console.log(Object.getOwnPropertyNames(Circle.prototype));
console.log(Object.hasOwn(c, 'area'), 'area' in c);

try {
  Shape('square');
} catch (error) {
  console.log(error.name);
}
▸ Ожидаемый результат
function I am a circle 12.57
true
[ 'constructor', 'area' ]
false true
TypeError
Запись с классомЧто происходит «под капотом»
class A { m() {} }function A() {} + A.prototype.m = function () {}
class B extends A {}Object.setPrototypeOf(B.prototype, A.prototype)
super.m()A.prototype.m.call(this)
static s() {}A.s = function () {}
Таблица — упрощённая схема: extends ещё и связывает B с A, чтобы наследовались статические методы.
Задание

С помощью Object.create(vehicle) создай два объекта: car (brand: 'Toyota') и bike (brand: 'BMX' и собственное свойство wheels: 2). Метод describe() у обоих должен браться из прототипа. Строки ниже выведут результат.

Задание · JavaScript
const vehicle = {
  wheels: 4,
  describe() {
    return `${this.brand} has ${this.wheels} wheels`;
  },
};

// create car and bike here

console.log(car.describe());
console.log(bike.describe());
console.log(Object.hasOwn(car, 'wheels'), Object.hasOwn(bike, 'wheels'));
▸ Ожидаемый результат
Toyota has 4 wheels
BMX has 2 wheels
false true
Задание

Добавь в Counter.prototype метод increment(): он увеличивает value на 1 и возвращает this, чтобы вызовы можно было соединять в цепочку. Метод должен быть общим для всех экземпляров, а не собственным свойством каждого объекта.

Задание · JavaScript
function Counter(start) {
  this.value = start;
}

// add increment() to Counter.prototype

const a = new Counter(5);
const b = new Counter(0);
a.increment().increment();

console.log(a.value, b.value);
console.log(a.increment === b.increment);
console.log(Object.hasOwn(a, 'increment'));
▸ Ожидаемый результат
7 0
true
false

Главное

  • У каждого объекта есть скрытая ссылка на прототип; отсутствующее свойство ищется по цепочке до Object.prototype, а затем null.
  • Запись свойства всегда создаёт собственное свойство, которое затеняет унаследованное; прототип не меняется.
  • Object.create(proto) создаёт объект с заданным прототипом; new Fn() связывает новый объект с Fn.prototype.
  • Методы в прототипе общие для всех экземпляров; Object.hasOwn проверяет собственные свойства, in — всю цепочку.
  • class построен на прототипах и добавляет удобства: строгий режим, обязательный new, приватные поля.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
После const a = { x: 1 }; const b = Object.create(a); b.x = 2; чему равно a.x?