Позвольте мне начать с того, что: дизайн кода, вдохновленный классами, в 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%.