Skip to content

inheritance model

garevna edited this page Apr 5, 2019 · 26 revisions

🎓 Модель наследования JS

Модель наследования JS основана на понятии прототипа

Прототип - это объект

Предшественником JS в плане прототипной модели наследования является язык

Стремление сделать синтаксис Javascript похожим на Java привело к появлению в языке таких "рудиментов", как ключевое слово new, лишенное практического смысла в языках с прототипной моделью наследования и создающее иллюзию наличия классов

Класс - это абстракция, объект - это воплощение

‼️ Уважаемые дамы и господа
Курение вредит вашему здоровью,
а курение конкретно в этом подъезде может вообще резко подорвать его

☕ Класс и объект

Чашка - это абстракция, но чашка, из которой ты в данный момент пьешь чай - это объект, т.е. конкретное воплощение абстракции "чашка"

Для создания конкретного экземпляра класса "чашка" в классической модели используется ключевое слово new


☕ Наследование в классической модели

Абстракция "чашка" вложена в другую абстракцию - "емкость", поскольку емкостью также является кастрюля, и цистерна, и колба

Таким образом, класс "чашка" наследует от класса "емкость"


Так вот:

в JS наследование происходит от объекта, а не класса,

т.е. от конкретного воплощения, а не от абстракции

Другими словами, JS - очень конкретный язык 😉


🎓 prototype

Для создания объекта в JS нам достаточно сделать следующее:

☕ 1

var sample = {
    name: "master"
}

При этом у нашего объекта "магическим образом" появляется свойство __proto__:

▼ { name: "master" }
    name: "master"
  ► __proto__: Object

Это свойство является ссылкой на объект prototype, который реально существует

внутри встроенного нативного объекта Object

На мой взгляд, это вполне соотносится с реальностью:

можно ли получить наследство от класса "дедушка" ? Даже кошке понятно, что только конкретный дедушка может оставить вам наследство

Таким образом, sample наследовал от Object

Чтобы посмотреть, что же унаследовал sample от Object, давайте развернем его свойство __proto__ в консоли:

▼ { name: "master" }
    name: "master"
  ► __proto__:
      ► constructor: ƒ Object()
      ► hasOwnProperty: ƒ hasOwnProperty()
      ► isPrototypeOf: ƒ isPrototypeOf()
      ► propertyIsEnumerable: ƒ propertyIsEnumerable()
      ► toLocaleString: ƒ toLocaleString()
      ► toString: ƒ toString()
      ► valueOf: ƒ valueOf()
      ► __defineGetter__: ƒ __defineGetter__()
      ► __defineSetter__: ƒ __defineSetter__()
      ► __lookupGetter__: ƒ __lookupGetter__()
      ► __lookupSetter__: ƒ __lookupSetter__()
      ► get __proto__: ƒ __proto__()
      ► set __proto__: ƒ __proto__()

Сам "дедушка" Object не является наследством

Логично, что наследство "дедушки" где-то хранится...

Оно хранится в свойстве prototype "дедушки" ( объекта Object )

Проверим:

Object.prototype
▼ {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
  ► constructor: ƒ Object()
  ► hasOwnProperty: ƒ hasOwnProperty()
  ► isPrototypeOf: ƒ isPrototypeOf()
  ► propertyIsEnumerable: ƒ propertyIsEnumerable()
  ► toLocaleString: ƒ toLocaleString()
  ► toString: ƒ toString()
  ► valueOf: ƒ valueOf()
  ► __defineGetter__: ƒ __defineGetter__()
  ► __defineSetter__: ƒ __defineSetter__()
  ► __lookupGetter__: ƒ __lookupGetter__()
  ► __lookupSetter__: ƒ __lookupSetter__()
  ► get __proto__: ƒ __proto__()
  ► set __proto__: ƒ __proto__()

Например, "наследство" включает метод hasOwnProperty

Давайте попробуем его использовать:

sample.hasOwnProperty( "name" )   // true

О-па, мы получили true, т.е. метод работает!

Значит, наследство благополучно получено 😸

⁉️ зачем нам вообще нужно ключевое слово new ?

Итак, сделаем выводы:

  • "наследство" хранится в свойстве prototype "дедушки"
  • "наследник" получает свойство __proto__
  • свойство __proto__ является ссылкой на объект "наследство" ( prototype )

🎓 constructor

Теперь обратим внимание на первое, что мы видим в объекте prototype и в свойстве __proto__ экземпляра

Это свойство constructor

Значение у него ƒ Object()

Буковка ƒ говорит нам, что это функция

Object() говорит нам, что эта функция - Object

Итак, мы выяснили, что встроенный нативный объект Object:

  • является функцией
  • выступает в роли constructor

Таким образом, в объекте "наследства" ( prototype ) есть ссылка на "дедушку", от которого наследство получено

Кроме того, "дедушка" является функцией

И у "дедушки" вполне конкретная роль - он constructor

Теперь вопрос: а что сделало его "конструктором" ?


☕ 2

Проделаем следующий "финт":

console.dir ( function Sigma () {} )

и обратим внимание на наличие свойства prototype,

в котором есть свойство constructor

▼ ƒ Sigma()
    arguments: null
    caller: null
    length: 0
    name: "Sigma"
  ▼ prototype:
    ► constructor: ƒ Sigma()
    ► __proto__: Object
  ► __proto__: ƒ ()

Куда показывает свойство constructor ? - на функцию Sigma

То есть она по дефолту уже конструктор

У нее есть все, чтобы выступать в роли "дедушки"

Давайте именовать такие объекты "конструкторы", а не "дедушки" - это ближе к семантике языка

И опять обращаю ваше внимание на тот факт, что у нас все совершенно конкретно, нет никаких абстракций, никаких классов - только объекты ( функции тоже объекты )

Совершенно конкретная функция Sigma уже готова предоставить своим наследникам свой совершенно конкретный объект prototype в качестве наследства

Осталось только вызвать ее с ключевым словом new:

var obj = new Sigma

и мы получим наследника...

А можно ли без new передать наследство ?

function Sigma () {}
Sigma.prototype.say = function () {
    console.log ( "I'm the instance of Sigma: ", this instanceof Sigma )
}
var obj = {}
obj.__proto__ = Sigma.prototype

И у нас все в шоколаде!

Одна строчка:

obj.__proto__ = Sigma.prototype

сделала объект obj наследником Sigma

Причем оператор instanceof совершенно благопристойно возвращает нам true, как будто obj является экземпляром класса Sigma:

obj.say()
Результат в консоли:
I'm the instance of Sigma:  true

Но ведь не существует никакого класса Sigma - совершенно справедливо заметите вы

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

Sigma - просто функция, а не класс

Мы ей дали имя, начинающееся с литеры в верхнем регистре, чтобы придать ей "статус класса" - чистая липа

На самом деле все, что произошло - это помещение ссылки на объект prototype функции Sigma в свойство __proto__ объекта obj

Зачем же имитировать классовую модель, пряча "под капот" истинную природу наследования в JS ?

Это сбивает с толку, приводит к непониманию истинной природы происходящего

В этом примере мы столкнулись с еще одним важным явлением - this

Ранее мы уже сталкивались с ключевым словом this в контексте исполнения функций

Давайте заглянем еще разок


🎓 this

Итак, мы уже поняли, что любая функция в JS является конструктором по своей сути, поскольку имеет контейнер для "наследства" - свойство prototype

Но что еще мы знаем о функциях JS ?

Мы знаем, что в момент вызова у них появляется ссылка на контекст вызова - this

Вы заметили, что в примере выше внутри функции say использовано это ключевое слово ?

Сама функция say находится внутри объекта prototype функции Sigma

Давайте изменим функцию say, не трогая все остальное:

Sigma.prototype.say = function () {
    console.log ( "My name is ", this.name )
}

и добавим объекту obj свойство name:

obj.name = "Google"

Теперь вызовем метод say, который унаследовал объект obj:

obj.say()
Результат в консоли:
My name is  Google

Тыц, this.name оказался "Google" !

т.е. this является ссылкой на obj !

Это как?

Да все правильно и закономерно:

  • во-первых, метод say доступен объекту obj, потому что унаследован
  • а во-вторых, при вызове
obj.say()

мы конкретно указали контекст вызова

Другими словами, когда один из наследников вызывает унаследованный метод, то контекстом вызова этого метода будет данный конкретный наследник

Кто вызвал - на того и покажет this внутри метода

Ведь this - это же ссылка на контекст вызова

То есть каждый наследник может попользоваться, и в момент, когда он пользуется, этот метод принадлежит ему

ферштейн? 😉


🎓 Наследование, или цепочка прототипов

Обратите внимание, что в предыдущем примере мы создали объект obj с двумя вложенными свойствами __proto__

▼ Sigma {name: "Google"}
    name: "Google"
  ▼ __proto__:
    ► say: ƒ ()
    ► constructor: ƒ Sigma()
    ► __proto__: Object

первое свойство __proto__ является ссылкой на объект, который содержит:

  • свойство say
    • функция, которая выводит в консоль значение this.name
  • свойство constructor

    ссылка на функцию Sigma

  • еще одно свойство __proto__

    ссылка на Object

Итак, наша цепочка прототипов состоит уже из двух звеньев

Этих звеньев может быть значительно больше

Что это означает?

Что при обращении к какому-либо свойству ( методу ) экземпляра obj поиск будет происходить сверху вниз по цепочке прототипов

Значит, если свойство с таким именем есть на нескольких уровнях, то будет использовано значение, которое находится выше остальных в цепочке прототипов


🎓 Конструктор Object

⤴️ ⤵️

Object - это "Адам" всех объектов в JS

Корень, от которого все произрастает

Ссылка на его свойство prototype будет последним звеном в любой цепочке прототипов,

перед "жирной точкой" в конце - null

Однако у вас полная свобода творить

Вы можете создать объект без прототипа, например, так:

var obj = Object.create( null )

( сразу поставили "жирую точку" )

или так:

var obj = {}
obj.__proto__ = null

собственные свойства конструктора Object

Свойства и методы объекта Object, которые не находятся в свойстве prototype, не наследуются экземплярами, и могут быть вызваны только как свойства и методы объекта Object:

▼ Object()
    arguments: (...)
  ► assign: ƒ assign()
    caller: (...)
  ► create: ƒ create()
  ► defineProperties: ƒ defineProperties()
  ► defineProperty: ƒ defineProperty()
  ► entries: ƒ entries()
  ► freeze: ƒ freeze()
  ► fromEntries: ƒ fromEntries()
  ► getOwnPropertyDescriptor: ƒ getOwnPropertyDescriptor()
  ► getOwnPropertyDescriptors: ƒ getOwnPropertyDescriptors()
  ► getOwnPropertyNames: ƒ getOwnPropertyNames()
  ► getOwnPropertySymbols: ƒ getOwnPropertySymbols()
  ► getPrototypeOf: ƒ getPrototypeOf()
  ► is: ƒ is()
  ► isExtensible: ƒ isExtensible()
  ► isFrozen: ƒ isFrozen()
  ► isSealed: ƒ isSealed()
  ► keys: ƒ keys()
    length: 1
    name: "Object"
  ► preventExtensions: ƒ preventExtensions()
  ► prototype: {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
  ► seal: ƒ seal()
  ► setPrototypeOf: ƒ setPrototypeOf()
  ► values: ƒ values()
  ► __proto__: ƒ ()

Поскольку они не передаются экземплярам, их называют статическими

☕ 3

Воспользуемся методом Object.create для создания нового экземпляра объекта:

var sample = Object.create ( { type: "figure" } )
sample.name = "circle"

и выведем его в консоль:

▼ { name: "circle" }
    name: "circle"
  ▼ __proto__: 
        type: "figure"
      ► __proto__: Object

Как видите, мы опять обошлись без ключевого слова new,

и благополучно создали экземпляр sample,

у которого первым звеном в цепочке прототипов является ссылка на безымянный объект { type: "figure" },

а далее следует ссылка на свойство prototype конструктора Object


☕ 4

Воспользуемся методом Object.setPrototypeOf():

var sample = {
    name: "circle"
}
var sample = Object.setPrototypeOf (
    sample,
    { type: "figure" }
)

Результат будет абсолютно такой же, как в предыдущем примере, и мы опять обошлись без ключевого слова new

"Похимичим" далее:

var test = Object.create ( sample )
test.draw = function () {
    console.log ( this.name )
}

Новый экземпляр test наследует от объекта sample все, включая его цепочку прототипов:

▼ { draw: ƒ }
    draw: ƒ ()
  ▼ __proto__: 
        name: "circle"
      ► __proto__: 
            type: "figure"
          ► __proto__: Object

Правда, при этом у нас нет имитации классов ( в цепочке прототипов отсутствует свойство constructor ) и оператор instanceof становится бесполезен

Зато все по-честному 😉


🎓 Object.__proto__

Выведем в консоль свойство __proto__ конструктора Object

▼ __proto__: ƒ ()
    ► apply: ƒ ()
      arguments: (...)
    ► bind: ƒ ()
    ► call: ƒ ()
      caller: (...)
    ► constructor: ƒ ()
      length: 0
      name: ""
    ► toString: ƒ ()
    ► Symbol(Symbol.hasInstance): ƒ ()
    ► get arguments: ƒ ()
    ► set arguments: ƒ ()
    ► get caller: ƒ ()
    ► set caller: ƒ ()
    ► __proto__: Object

а теперь выведем в консоль свойство prototype конструктора Function

▼ prototype: ƒ ()
    ► apply: ƒ ()
      arguments: (...)
    ► bind: ƒ ()
    ► call: ƒ ()
      caller: (...)
    ► constructor: ƒ ()
      length: 0
      name: ""
    ► toString: ƒ ()
    ► Symbol(Symbol.hasInstance): ƒ ()
    ► get arguments: ƒ ()
    ► set arguments: ƒ ()
    ► get caller: ƒ ()
    ► set caller: ƒ ()
    ► __proto__: Object

Очевидно, что Object наследует от Function, что логично, поскольку Object - это конструктор, т.е. функция

console.dir ( Object.__proto__.constructor.name )
// Function

При этом объект Function.prototype наследует от Object

( свойство Function.prototype.__proto__ является ссылкой на Object.prototype )

ссылка на
Object.__proto__ Function.prototype
Function.prototype.__proto__ Object.prototype

💼 Упражнения


🔗 MDN 🔗 MDN

© Irina H.Fylyppova 2018
Использование данных материалов или любой их части коммерческими школами ( курсами ) является нарушением авторских прав


Новая версия


1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19

Занятие 1

⤵️

Занятие 2

⤴️ ⤵️

Занятие 3

⤴️ ⤵️

Занятие 4

⤴️ ⤵️

Занятие 5

⤴️ ⤵️

Занятие 6

⤴️ ⤵️

Занятие 7

⤴️ ⤵️

Занятие 8

⤴️ ⤵️

Занятие 9

⤴️ ⤵️

Занятие 10

⤴️ ⤵️

Занятие 11

⤴️ ⤵️

Занятие 12

⤴️ ⤵️

Занятие 13

⤴️ ⤵️

Занятие 14

⤴️ ⤵️

Занятие 15

⤴️ ⤵️

Занятие 16

⤴️ ⤵️

Занятие 17

⤴️ ⤵️

Занятие 18

⤴️ ⤵️

Занятие 19

⤴️ ⤵️

⤴️

ico20 Дополнительно
dir-20 Справочная инфо

Clone this wiki locally