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

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

Прототип — скрытая ссылка, по которой движок ищет свойства и методы, если их нет на самом объекте. Наследование в JavaScript прототипное: объекты делегируют доступ к другому объекту-прототипу, а не копируют поля класса как в классических ООП-языках.


[[Prototype]] и поиск свойств #

У каждого объекта есть внутренняя ссылка [[Prototype]] (в консоли часто видна как __proto__, но в коде так не пишут):

const lesson = {
  title: 'Прототипы',
  level: 'middle',
}

lesson.toString() // "[object Object]"

Метода toString на lesson нет — движок поднимается по цепочке к Object.prototype и находит его там.

Порядок поиска:

  1. собственное свойство объекта;
  2. свойство на прототипе;
  3. прототип прототипа — и так далее, пока не null.

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

До class объекты часто создавали через функцию с new:

function Lesson(title, minutes) {
  this.title = title
  this.minutes = minutes
}

Lesson.prototype.describe = function () {
  return `${this.title} (~${this.minutes} мин)`
}

const intro = new Lesson('Введение', 12)
intro.describe() // "Введение (~12 мин)"

new создаёт объект, связывает его [[Prototype]] с Lesson.prototype и вызывает функцию с this, указывающим на новый объект. Методы на prototype общие для всех экземпляров — экономия памяти.


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

const quiz = new Lesson('Контрольные вопросы', 8)

quiz.hasOwnProperty('title') // true — собственное свойство
quiz.hasOwnProperty('describe') // false — унаследовано с prototype

Lesson.prototype сам является объектом и наследует от Object.prototype. Цепочка:

quiz → Lesson.prototype → Object.prototype → null

Если свойство нигде не найдено (дошли до null), результат — undefined (для чтения) или возможность записи на сам объект.


Object.create #

Явное задание прототипа без конструктора:

const lessonBehavior = {
  markComplete() {
    this.done = true
  },
}

const chapter = Object.create(lessonBehavior)
chapter.title = 'Глава 3'
chapter.markComplete()

chapter.done // true
Object.getPrototypeOf(chapter) === lessonBehavior // true

Object.create(proto) создаёт объект с [[Prototype]] === proto. Удобно для композиции поведения без new.


Собственные vs унаследованные #

for (const key in quiz) {
  console.log(key)
}
// перечислит и title, и describe — цикл идёт по цепочке

Только собственные ключи:

Object.keys(quiz) // ['title', 'minutes']
Object.hasOwn(quiz, 'describe') // false (ES2022+)

При расширении объектов проверяйте hasOwnProperty или Object.hasOwn, чтобы не трогать унаследованные поля.


Изменение прототипа #

Прототип можно менять у функции-конструктора (влияет на будущие экземпляры) или у одного объекта:

const track = { title: 'Async' }

const proto = {
  getLabel() {
    return `[${this.level}] ${this.title}`
  },
}

Object.setPrototypeOf(track, proto)
track.level = 'advanced'
track.getLabel() // "[advanced] Async"

Мутация Lesson.prototype после создания экземпляров добавляет методы всем уже существующим объектам этой линии — мощно, но рискованно в библиотечном коде.


class — синтаксический сахар #

class из ES2015 не отменяет прототипы — под капотом те же механизмы:

class Lesson {
  constructor(title, minutes) {
    this.title = title
    this.minutes = minutes
  }
  describe() {
    return `${this.title} (~${this.minutes} мин)`
  }
}

typeof Lesson // "function"
Lesson.prototype.describe // функция на прототипе

Понимание прототипов помогает отлаживать this, наследование и instanceof.


Частые ошибки #

  1. Путаница __proto__ и prototype — у функции есть свойство .prototype; у экземпляра — ссылка [[Prototype]] на него.
  2. Методы в конструкторе вместо prototype — каждый экземпляр получает копию функции, лишняя память.
  3. Перезапись Constructor.prototype целиком — ломается связь с уже созданными объектами; дополняйте, а не заменяйте объект.
  4. Ожидание «классического» копирования полей — изменение на прототипе видно всем экземплярам.
Lesson.prototype.badge = 'JSFlow'
const a = new Lesson('A', 5)
const b = new Lesson('B', 10)
a.badge // "JSFlow" — общее через прототип

Итог #

  • Объекты наследуют через цепочку прототипов, а не копирование класса.
  • Функция-конструктор + prototype — классический способ; class — тот же механизм, другой синтаксис.
  • Object.create и Object.setPrototypeOf задают прототип явно.
  • Различайте собственные свойства и унаследованные при обходе и расширении объектов.

См. также #