Прототипы, конструкторы, классы

Прототипы

Каждый объект имеет объект-прототип, который выступает как шаблон, от которого объект наследует методы и свойства. Объект-прототип так же может иметь свой прототип и наследует его свойства и методы и так далее. Это называется цепочкой прототипов, «прототипным наследованием» и объясняет почему одним объектам доступны свойства и методы, которые определены в других объектах.

Это если в общем, а если точнее, то свойства и методы определяются в свойстве 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        
                                
                            

Мини-задачи