Прототипы и прототипное наследование
В JavaScript объекты могут наследовать свойства и методы через цепочку прототипов. У каждого объекта есть скрытое поле [[Prototype]] - ссылка на другой объект (или null).
Поиск свойства
Когда вы обращаетесь к свойству объекта, движок ищет его:
- На самом объекте.
- На его прототипе.
- На прототипе прототипа - и так до
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. При таком вызове:
- Создаётся новый пустой объект.
- Его
[[Prototype]]указывает наКонструктор.prototype. thisвнутри функции ссылается на этот новый объект.- Функция неявно возвращает
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 в самом конце