Прототипы, конструкторы, классы
Прототипы
Каждый объект имеет объект-прототип, который выступает как шаблон, от которого объект наследует методы и свойства. Объект-прототип так же может иметь свой прототип и наследует его свойства и методы и так далее. Это называется цепочкой прототипов, «прототипным наследованием» и объясняет почему одним объектам доступны свойства и методы, которые определены в других объектах.
Это если в общем, а если точнее, то свойства и методы определяются в свойстве prototype функции-конструктора объектов, а не в самих объектах (подробнее тут).
В JavaScript объекты имеют специальное скрытое свойство [[Prototype]], которое либо равно null, либо ссылается на другой объект. Этот объект называется «прототип».
В примере ниже видно, что прототипом созданного объекта является Объект, и ребёнку объекту доступны методы родителя объекта.
let person = {
name: 'Nicolas',
age: 33,
}
// в консоли
{name: 'Nicolas', age: 33}
age: 33
name: "Nicolas"
[[Prototype]]: Object
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
...
Ещё раз «работа» прототипа
Прототип идет сверху вниз. Если он на верхнем уровне находит какое-то поле или функцию, он сразу же его показывает/вызывает. Если не находит, то обращается к прототипу и пытается найти что-то в нём и так далее по цепочке прототипов, пока не найдет метод, который нужно вызвать.
Если мы пытаемся прочитать свойство объекта, либо вызвать метод, а его нет, то объект полезет искать его через ссылку __proto__ в prototype класса, с помощью которого был создан. Свойство __proto__ объекта Object.prototype является свойством доступа (комбинацией геттера и сеттера), которое расширяет внутренний прототип [[Prototype]] объекта, через который осуществлялся доступ. (подробнее тут).
В примере ниже создаём новый объект на основе другого, задаём ему поле на верхнем уровне, а поле на уровне ниже он наследует от своего прототипа.
let person = {
name: 'Nicolas',
age: 33,
}
let lena = Object.create(person)
// обращаемся к глобальному классу Object, у которого есть метод create, который создаёт новые объекты.
// в метод передаем объект, который будет являться прототипом для объекта lena.
lena.name = 'Elena' // задаём поле на верхнем уровне
console.log(lena.name) // Elena (нашёл на этом уровне)
console.log(lena.age) // 33 (нашёл на 2м уровне)
Конструкторы
Любой объект в JS создаётся с помощью функции-конструктора или класса (но стрелочная функция не может быть конструтором).
Если объект создаётся через new, то это явно, если помощью литерала {}, то это неявно, но за кадром браузерный движок все равно создаёт new Object, то есть используется класс Object.
В JS есть и другие встроенные функции-конструкторы, они же классы: Object, Promise, Boolean, Number, String, Array, Function и др.
Когда вызывается new Object() (или создаётся объект с помощью литерала {}), свойство [[Prototype]] этого объекта устанавливается на Object.prototype (подробнее тут). В коде ниже наглядно видно, что переменная person является экземпляром (инстансом) класса Object.
const person = new Object({
name: 'Nicolas',
age: 33,
})
Функции и классы
При создании функции так же работает new Function()
function greet() {} // под капотом new Function()
В JavaScript класс – это разновидность функции, «синтаксический сахар» над ключевым словом function.
Синтаксис
class MyClass { // cоздаётcя функция с именем MyClass
constructor() { ... } // автоматически вызывается метод constructor(), в нём мы можем инициализировать объект
method1() { ... } // метод сохраняется в MyClass.prototype
}
// классы можно также создавать через Class Expression
Пример:
class Cat {
constructor(name) {
this.name = name
}
sayMeow() {
console.log('Meow');
} // этот метод сохраняется в Cat.prototype.
} // под капотом класса new Function()
let cat1 = new Cat('Lili') // инстанс явно создаётся через new Cat()
console.log(typeof Cat) // function
console.log(cat1) // Cat {name: 'Lili'}
console.log(Cat.prototype) // {constructor: ƒ, sayMeow: ƒ}, в prototype конструктор и функция (подробнее далее)
Примитивы
Использование свойств и методов у примитивов также возможно как раз за счёт прототипного наследования. Если к примитиву обратиться как к объекту (т.е. через точку), то в памяти ВРЕМЕННО для этого примитива создастся объектная версия (обертка) с помощью конструктора new, а свойства и методы станут доступными.
let num = 18 // под капотом new Number()
__proto__ и prototype
Эти ключевые понятия нужно различать. Оба являются свойствами объекта (доступ через точку.
При этом и __proto__ и Prototype являются объектами, но __proto__ ссылается на Prototype. То есть, prototype — это какой-то объект и __proto__ ссылается на тот же самый объект.
Любой объект имеет __proto__ и создаётся с помощью класса или function. __proto__ равно какому-то prototype.
Чтобы понимать что это за __proto__, нужно точно знать с помощью какой функции-конструктора (класса) создан данный объект (new XXX()).
const obj = {} // obj.__proto__
const num = 2 // num.__proto__
function func() {} // func.__proto__
const f = () => {} // f.__proto__
class Cls {} // Cls.__proto__
Разные __proto__ разных по «типу» объектов — совершенно независимые разные объекты. У одинаковых по «типу» объектов — они равны, то есть это один и тот же объект.
let man1 = {}
let man2 = {}
console.log(man1.__proto__ === man2.__proto__)
// их __proto__ равны, эти объекты созданы с помощью
// одного и того же класса или функции-конструктора new Object()
let a = 1
let b = 100
console.log(a.__proto__ === b.__proto__)
// их __proto__ равны, так как оба созданы с помощью new Number()
console.log(a__proto__ != man1.__proto__) // их __proto__ не равны,
// a создано с помощью new Number(), а man1 создано с помощью new Object()
У всех функций, в том числе class, __proto__ равны.
function greet() {}
class YoutubeChanell {}
console.log(greet.__proto__ === YoutubeChanell.__proto__)
Любой class или function имеет prorotype. Нам нужно знать с помощью какого класса был создан тот или иной объект — для того чтобы наш __proto__ мог связаться с прототипом того класса, с помощью которого он был создан.
function greet() {}
console.log(function.prototype)
class YoutubeChanell {}
console.log(YoutubeChanell.prototype)
// у встроенных классов тоже есть прототип
console.log(Object.prototype)
console.log(Array.prototype)
console.log(String.prototype)
//каждый prorotype - это независимый объект с определённым набором свойств и методов.
console.log(String.prototype !=Function.prototype )
__proto__ объекта ссылается на prorotype класса (функции-конструктора), с помощью которой этот объект был создан (сконструирован), и __proto__ объекта равно prototype того класса, с помощью которого этот объект создан.
let man1 = {}
console.log(man1.__proto__ === Object.prorotype)
let a = 1
console.log(a.__proto__ === Number.prorotype)
console.log(chanel1.__proto__ === YoutubeChanell.prototype)
Добавление метода
Если мы создали объект, у которого, например, нет функции sayHi, тогда мы не сможем обратиться к ней как к методу через точку. Чтобы добавить метод, нужно обратиться к prototype класса, от которого создан наш объект и задать ему новую функцию sayHi, после чего возможно вызвать метод sayHi.
Пример ниже представлен в учебных в целях. В реальности расширение базовых прототипов (расширение Object.prototype) является плохой практикой.
const person = {
name: 'Nicolas',
age: 33,
}
Object.prototype.sayHi = function () {
console.log('Hi');
} // за счет этой конструкции расширили базовый прототип
// класса Object и добавили в него новый метод
console.log(person.sayHi()) // Hi