Позвольте мне начать с того, что: дизайн кода, вдохновленный классами, в JavaScript запутан и запутан. Не делайте этого! Скоро я объясню, почему.
В этом посте я попытаюсь объяснить мыслительный процесс человека из языка, ориентированного на классы (например, Python или Ruby), пытающегося реализовать основанный на классах подход с помощью цепочки [[Prototype]]
в JavaScript. Нет, на самом деле я не из языка, основанного на классах; JavaScript — мой первый. Это умственное упражнение для меня.
Классический (прототипный) объектно-ориентированный дизайн кода
Я буду использовать и объяснять примеры из Главы 6: Делегирование поведения YDKJS — прототипы this и Object.
Вот реализация того, что Симпсон называет «классическим (прототипным) объектно-ориентированным стилем».
function Foo(who) { this.me = who; } Foo.prototype.identify = function() { return "I am " + this.me; }; function Bar(who) { Foo.call( this, who ); } Bar.prototype = Object.create( Foo.prototype ); Bar.prototype.speak = function() { alert( "Hello, " + this.identify() + "." ); }; var b1 = new Bar( "b1" ); var b2 = new Bar( "b2" ); b1.speak(); b2.speak();
Симпсон говорит:
Родительский класс
Foo
, наследуемый дочерним классомBar
, который затем создается дважды какb1
иb2
.
Я скопировал и вставил тот же код ниже, но изменил имена переменных, чтобы сделать дизайн, вдохновленный классами, более очевидным.
function parentClassFoo(who) { this.me = who; } parentClassFoo.prototype.identify = function() { return "I am " + this.me; }; function childClassBar(who) { parentClassFoo.call( this, who ); } childClassBar.prototype = Object.create( parentClassFoo.prototype ); childClassBar.prototype.speak = function() { alert( "Hello, " + this.identify() + "." ); }; var instanceB1 = new childClassBar( "instanceB1" ); var instanceB2 = new childClassBar( "instanceB2" ); instanceB1.speak(); instanceB2.speak();
Теперь я попытаюсь объяснить мыслительный процесс человека, пришедшего из объектно-ориентированного языка, и результат такого подхода в коде.
Программисты объектно-ориентированного программирования, скорее всего, захотят использовать оператор new
, потому что он напоминает конструктор в языках, основанных на классах, или что-то, что создает экземпляр класса. Это сразу ограничивает программиста в JavaScript. new
в JavaScript — это не конструктор, а нечто, что создает объект, а прототип связывает его с конструктором (вещь справа от new
). Конструктор должен быть функцией. Это означает, что "классы", используемые с new
, должны быть функциями, а не объектами.
Итак, программист ООП делает function parentClassFoo
. Поскольку это функция, как нам добавить к ней метод? Через его свойство [[Prototype]]
в parentClassFoo.prototype.identity = function() {..}
. То же самое касается childClassBar
и .speak
. Мы связываем прототипы двух функций с помощью Object.create
. Обратите внимание, что здесь мы связываем прототипы, а не сами функции. Object.create(..)
должен принимать в качестве аргументов объекты, а не функции.
ООП-программист затем использует оператор new
для «создания» childClassBar
экземпляров. Помните, что в JavaScript не существует настоящего экземпляра. Здесь JS только создает объекты, связанные с прототипом childClassBar
.
Если вы думаете: Вау, это действительно запутанно и запутанно, то вы правы. Большая часть этого YDKJS: прототипы this и Object объясняет, почему это не лучший дизайн для JavaScript, и вы можете понять, почему выше.
Ментальное картографирование этого подхода
При этом важно понимать, что происходит. Симпсон пытается представить мысленную карту этого кода на паре диаграмм. Я постараюсь их разбить.
Простая ментальная карта
Обратите внимание, что это соответствует первому блоку кода выше; тот, что взят прямо из текста. Я попытаюсь объяснить вещи в том порядке, в котором они появились в коде, а стрелки отношений указывали в сторону от предмета обсуждения.
Во-первых, у нас есть Foo()
. Вы можете видеть в легенде, что это функция, хотя предполагается, что она эмулирует класс в коде. У него есть стрелка свойства/отношения, помеченная .prototype
, указывающая на identify()
. Это означает, что Foo()
имеет отношение к identify()
, потому что identify()
— это метод, существующий на Foo.prototype
.
Перейдем к identify()
. У него есть стрелка отношения, помеченная .__proto__ [[Prototype]]
, указывающая на toString(), valueOf(), hasOwnProperty(),l isPrototypeOf()
. Ярлык отношения немного сбивает с толку. Похоже, что [[Prototype]]
просто означает цепочку [[Prototype]]
, но .__proto__
— это отдельная концепция. Помните, что это геттер/сеттер, существующий на Object.prototype
, вместе с toString, valueOf()
и т. д. Он позволяет вам проверять прототип объекта или перемещаться по цепочке прототипов. Таким образом, вы можете рассматривать эту связь как цепочку [[Prototype]]
.
Bar()
похож на Foo()
и имеет такое же отношение .prototype
к методу-прототипу speak()
.
speak()
имеет отношение [[Prototype]]
к identify()
, потому что они оба существуют в цепочке [[Prototype]]
. speak()
имеет «подразумеваемую связь через делегирование [[Prototype]]
» стрелку с надписью «конструктор», указывающую на Foo()
.
Хорошо, это сбивает с толку. Свойство .constructor
не означает создание с помощью '..' в JavaScript. Это произвольное свойство, которое по умолчанию является ссылкой на свойство объекта. Его можно переопределить.
speak()
существует на Bar.prototype
. Bar.prototype
был связан с Foo.prototype
через Object.create(..)
. Поскольку исходный Bar.prototype
был перезаписан новым объектом, он потерял свое свойство .constructor
по умолчанию. Таким образом, Bar.prototype.constructor
(который будет существовать в том же месте, что и speak()
) делегируется до Foo.prototype
, где .constructor
существует и указывает на Foo
. Если вы запустите Bar.prototype.constructor
в консоли, вы получите function Foo(who) {this.me = who;}
. Ого, какая психологическая разминка. Ознакомьтесь с разделом Constructor Redux в главе 5 для получения более подробной информации.
b1
является экземпляром Bar()
. Он имеет отношение [[Prototype]]
к speak(), что означает, что b1
делегирует Bar.prototype
. b1
имеет точечное отношение конструктора к Foo()
, потому что это следующая вещь в цепочке [[Prototype]]
, которая имеет свойство .constructor
. b2
совпадает с b1
.
В верхней части диаграммы вы видите функцию Object()
, которая доступна всем объектам через цепочку [[Prototype]]
. Он имеет отношение прототипа к объектам toString(), valueOf()
и т. д., поскольку эти методы существуют в Object.prototype
.
Сложная ментальная карта
Вот более сложная диаграмма, которая показывает больше вещей, которые происходят. Симпсон говорит, что нам не обязательно знать все, что происходит, но важно это понимать.
Я пройдусь по дополнениям к этой диаграмме, отсутствующим в предыдущей более простой диаграмме.
Function()
— это объект global Function
. Взято из МДН:
Глобальный объект Function не имеет собственных методов или свойств, однако, поскольку он сам является функцией, он наследует некоторые методы и свойства по цепочке прототипов от Function.prototype.
Function()
имеет отношение прототипа к call(), apply(), bind()
, которые помечены как функции. Function()
также имеет отношения [[Prototype]]
. Я собираюсь сделать заметку об этом позже; Я не понимаю, что здесь происходит.
call(), apply(), bind()
— это методы Function.prototype
, которым могут делегировать все функции в JavaScript. call(), apply(), bind()
имеет отношение конструктора к Function()
, потому что первый объект, который имеет .constructor
в цепочке [[Prototype]]
в этой ситуации, начиная с call(), apply(), bind()
, это Function()
. call(), apply(), bind()
имеет отношение [[Prototype]]
к toString(), valueOf()
и т. д., потому что объект global Function
является объектом, а это означает, что он может делегировать методы Object.prototype
.
Все остальное, что является новым, — это стрелки отношений на вещах, которые были на первой диаграмме.
И Foo()
, и Bar()
имеют отношение [[Prototype]]
к call(), apply(), bind()
, поскольку все функции могут делегироваться объекту global Function
. Foo()
и Bar()
также имеют отношение .constructor
к Function()
. Поскольку ни Foo()
, ни Bar()
не имеют свойства .constructor
, он делегирует это свойство объекту global Function
. Не путайте Foo()
с Foo.prototype
. Это разные объекты!!! Foo.constructor
относится к объекту global Function
, а Foo.prototype.constructor
относится к Foo()
.
Глобальный функциональный объект Object()
также имеет отношение конструктора к объекту global Function
. Object()
также имеет отношение .__proto__
к call(), apply(), bind()
, потому что Object()
является функциональным объектом; следовательно, он делегирует эти действия.
Примечание. Отношения между Object()
и Function()
кажутся циклическими, поскольку их цепочки [[Prototype]]
пересекаются друг с другом. Оба они являются глобальными функциональными объектами.
Вывод и некоторые мысли
Вот что вы должны вынести из этого:
1) Использование дизайна кода, вдохновленного классами, сбивает с толку и запутывает.
2) Хотя внутренние механизмы JavaScript сложны, они последовательны и поэтому понятны.
В следующем посте я расскажу о подходе к разработке кода, который гораздо более разумен и прост, учитывая природу JavaScript, основанную на прототипах.
Вау, это было тяжелое умственное упражнение. На самом деле в книге сначала показана более сложная диаграмма, и, честно говоря, это было действительно пугающе. Попытка объяснить все, что происходит, определенно помогла моему пониманию. Тем не менее, я все еще не понимаю одного: почему существует связь .__proto__ / [[Prototype]]
, идущая от объекта global Function
к его методам, таким как call(), apply(), bind()
на комплексной диаграмме? До сих пор я думал, что отношение представляет собой путь/направление цепочки [[Protoype]]
, которая существует в свойстве .prototype
. Я не уверен сейчас. Я попросил помощи на Форумах Freecodecamp и обновлю это, как только получу ответ.
редактировать: я получил хороший ответ на форумах FCC, который объясняет мое замешательство выше. Все функции можно делегировать global Function
методам call(), apply(), bind()
. Вот почему существует .__proto__ / [[Prototype]]
стрелка отношения от Function()
к call(), apply(), bind()
. Это своего рода рекурсия в этом аспекте.
редактировать 2: получил еще один ответ, который кратко объясняет это. Это взято из последней спецификации ECMAScript:
19.2.2 Свойства конструктора функции
Конструктор функции сам по себе является встроенным функциональным объектом. Значением внутреннего слота [[Prototype]] конструктора Function является встроенный объект %FunctionPrototype%.