JavaScript

Как работает прототипное наследование?

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

В JavaScript объекты могут наследовать свойства и методы через цепочку прототипов. У каждого объекта есть скрытое поле [[Prototype]] - ссылка на другой объект (или null).

Поиск свойства

Когда вы обращаетесь к свойству объекта, движок ищет его:

  1. На самом объекте.
  2. На его прототипе.
  3. На прототипе прототипа - и так до null.
const animal = {
  speak() { return "..." }
}

const dog = Object.create(animal)
dog.name = "Бобик"

console.log(dog.name)    // "Бобик"  - своё свойство
console.log(dog.speak()) // "..."    - из прототипа (animal)
console.log(dog.hasOwnProperty("name"))   // true
console.log(dog.hasOwnProperty("speak"))  // false

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

До появления классов в ES6 (2015) в JavaScript не было ключевого слова class. Объекты с общим поведением создавали через функции-конструкторы - это были «классы до классов».

Функция-конструктор - это обычная функция, которую вызывают с оператором new. При таком вызове:

  1. Создаётся новый пустой объект.
  2. Его [[Prototype]] указывает на Конструктор.prototype.
  3. this внутри функции ссылается на этот новый объект.
  4. Функция неявно возвращает this.
function Person(name) {
  this.name = name          // своё свойство каждого экземпляра
}

// общие методы кладут в prototype - они не копируются в каждый объект,
// а наследуются всеми экземплярами через цепочку прототипов
Person.prototype.greet = function() {
  return "Привет, я " + this.name
}

const alice = new Person("Алиса")
console.log(alice.greet())                         // "Привет, я Алиса"
console.log(alice.__proto__ === Person.prototype)  // true

Данные (name) живут на самом объекте, а методы (greet) - на prototype и общие для всех экземпляров. Именно так до ES6 эмулировали классы и наследование.

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

JavaScript изначально проектировался вокруг функций и прототипов, а не классов: функции здесь - объекты первого класса, и всё ООП собиралось из них. Полноценного синтаксиса классов в языке не было до 2015 года.

class из ES6 ничего нового под капотом не добавил - это просто привычный синтаксис поверх тех же функций-конструкторов и прототипов:

class Animal {
  constructor(name) { this.name = name }
  speak() { return this.name + " говорит" }
}

class Dog extends Animal {
  speak() { return super.speak() + ": гав!" }
}

const d = new Dog("Рекс")
console.log(d.speak())  // "Рекс говорит: гав!"

То же самое на функциях-конструкторах - длиннее, и наследование приходится настраивать вручную:

function Animal(name) { this.name = name }
Animal.prototype.speak = function() { return this.name + " говорит" }

function Dog(name) { Animal.call(this, name) }      // вызов «родительского» конструктора
Dog.prototype = Object.create(Animal.prototype)     // связываем цепочку прототипов
Dog.prototype.constructor = Dog

Под капотом extends делает ровно это - выставляет цепочку прототипов: Dog.prototype.__proto__ === Animal.prototype. То есть class - это «функции-конструкторы, только удобнее».

Частые вопросы на собеседовании

Чем Object.create отличается от new?

Object.create(proto) создаёт новый объект и напрямую назначает ему прототип proto - больше ничего не происходит. new Конструктор() делает больше: создаёт объект, ставит ему прототипом Конструктор.prototype, выполняет тело конструктора с this и возвращает результат.

const obj1 = Object.create(null)  // объект вообще без прототипа
const obj2 = {}                   // прототип - Object.prototype

obj1.toString  // undefined - нет Object.prototype в цепочке
obj2.toString  // функция     - досталась от Object.prototype

Объект из Object.create(null) иногда используют как «чистый словарь» (map): у него нет унаследованных свойств вроде toString или hasOwnProperty, которые могли бы случайно совпасть с ключами.

Как работает instanceof?

x instanceof F проверяет, встречается ли F.prototype где-нибудь в цепочке прототипов объекта x. Поэтому экземпляр является «instanceof» и своего класса, и всех родительских.

const d = new Dog("Рекс")

console.log(d instanceof Dog)     // true - Dog.prototype в цепочке
console.log(d instanceof Animal)  // true - Animal.prototype тоже выше по цепочке
console.log(d instanceof Object)  // true - и Object.prototype в самом конце

Источники