Автор: Джастен Робертсон
Перевод: сумасшедший технический ботаник
оригинал:Woohoo.topthem.com/JavaScript/…
Копирование без разрешения строго запрещено
JavaScript — странный язык. Хотя он вдохновлен Smalltalk, он использует синтаксис, подобный C. Он сочетает в себе все аспекты процедурного, функционального и объектно-ориентированного программирования (ООП). У него есть несколько способов решения практически любой проблемы программирования, которые часто являются избыточными и настоятельно не рекомендуются, но предпочтительнее. Он имеет слабую динамическую типизацию, но использует подход принудительного типа, что делает его доступным и для опытных разработчиков.
JavaScript также имеет свои недостатки, ловушки и сомнительные возможности. Начинающим программистам нужно разобраться с некоторыми из более сложных концепций — асинхронностью, замыканием и подъемом. Программисты, имеющие опыт работы с другими языками, разумно предположили бы что-то с похожим названием, но то, что кажется таким же способом работы, как JavaScript, часто оказывается неверным. Массивы на самом деле не массивы, чтоthis, что такое прототип,newЧто он на самом деле делает?
Проблемы с классами ES6
Безусловно, худший виновник — последняя версия JavaScript — ECMAScript 6 (ES6).Добрый. Некоторые обсуждения классов откровенно шокируют и раскрывают глубоко укоренившееся непонимание того, как на самом деле работает язык:
«JavaScript теперь, наконец,действительноОбъектно-ориентированный язык, потому что у него есть классы! "
Либо:
«Давайте освободимся от сломанной модели наследования в JavaScript».
Четное:
«Создание типов в JavaScript — более безопасный и простой способ».
Эти замечания меня не беспокоят, так как они подразумевают, что в прототипном наследовании есть проблема, давайте отложим эти аргументы в сторону. Эти слова беспокоят меня, потому что ни одно из них не соответствует действительности, и они демонстрируют последствия подхода JavaScript «все для всех» к разработке языка: он ослабляет понимание языка программистами. Прежде чем идти дальше, позвольте мне привести пример.
Тест по JavaScript №1: В чем существенная разница между этими блоками кода?
function PrototypicalGreeting(greeting = "Hello", name = "World") {
this.greeting = greeting
this.name = name
}
PrototypicalGreeting.prototype.greet = function() {
return `${this.greeting}, ${this.name}!`
}
const greetProto = new PrototypicalGreeting("Hey", "folks")
console.log(greetProto.greet())
class ClassicalGreeting {
constructor(greeting = "Hello", name = "World") {
this.greeting = greeting
this.name = name
}
greet() {
return `${this.greeting}, ${this.name}!`
}
}
const classyGreeting = new ClassicalGreeting("Hey", "folks")
console.log(classyGreeting.greet())
Ответ здесьне единственный. Код действительно работает, вопрос только в том, используется ли синтаксис класса ES6.
Правда, второй пример более выразителен, так что можно подуматьclassотличное дополнение к языку. К сожалению, проблема становится более тонкой.
Тест JavaScript №2: Что делает следующий код?
function Proto() {
this.name = 'Proto'
return this;
}
Proto.prototype.getName = function() {
return this.name
}
class MyClass extends Proto {
constructor() {
super()
this.name = 'MyClass'
}
}
const instance = new MyClass()
console.log(instance.getName())
Proto.prototype.getName = function() { return 'Overridden in Proto' }
console.log(instance.getName())
MyClass.prototype.getName = function() { return 'Overridden in MyClass' }
console.log(instance.getName())
instance.getName = function() { return 'Overridden in instance' }
console.log(instance.getName())
Правильный ответ — вывод, который он выводит на консоль:
> MyClass
> Overridden in Proto
> Overridden in MyClass
> Overridden in instance
Если вы ответите неправильно, значит, вы не понимаетеclassЧто именно это такое. Но это не твоя вина. подобноArray,classне то же самое, что особенности языка, этонезнание грамматики. Он пытается скрыть прототипную модель наследования и сопутствующие ей неуклюжие идиомы, а это означает, что JavaScript не делает того, что вы думаете.
Возможно, вам говорили, что в JavaScript введеноclass, чтобы сделать классических ООП-разработчиков из таких языков, как Java, более знакомыми с моделью наследования классов ES6. Если вы такой разработчик, этот пример может напугать вас. Примеры показывают, что JavaScriptclassКлючевые слова не дают никаких гарантий, которые требуются классам. Он также демонстрирует одно существенное отличие прототипной модели наследования: прототипэкземпляр объекта, вместоТипы.
Прототипы и классы
Наиболее важное различие между наследованием на основе классов и на основе прототипов заключается в том, что класс определяетТипы, который может быть создан во время выполнения, а сам прототип является экземпляром объекта.
Подклассы классов ES6 — еще одинТипыопределение, которое расширяет родительский класс новыми свойствами и методами, которые затем могут быть созданы во время выполнения. Дочерним элементом прототипа является другой объектпример, который делегирует все свойства, не реализованные в дочернем элементе, родительскому.
Примечание: вам может быть интересно, почему я упомянул методы класса, а не методы прототипа. Это потому, что в JavaScript нет концепции методов. Функции первоклассны в JavaScript, они могут иметь свойства или свойства других объектов.
Конструкторы классов используются для создания экземпляров классов. Конструктор в JavaScript — это обычная функция, которая возвращает объект. Единственная особенность конструкторов JavaScript заключается в том, что при использованииnewПри вызове по ключевому слову он назначает свой прототип в качестве прототипа возвращаемого объекта. Если это звучит для вас немного запутанно, вы не одиноки — вот почему прототипы трудно понять.
Чтобы проиллюстрировать, потомок прототипа не является прототипомкопировать, и это не тот же объект, что и прототип. Дочерний элемент имеет живую ссылку на прототип, а свойство прототипа, не существующее в дочернем элементе, является односторонней ссылкой на свойство с таким же именем в прототипе. .
Рассмотрим следующий код:
let parent = { foo: 'foo' }
let child = { }
Object.setPrototypeOf(child, parent)
console.log(child.foo) // 'foo'
child.foo = 'bar'
console.log(child.foo) // 'bar'
console.log(parent.foo) // 'foo'
delete child.foo
console.log(child.foo) // 'foo'
parent.foo = 'baz'
console.log(child.foo) // 'baz'
Примечание. Вряд ли вы когда-нибудь напишете подобный код в реальной жизни — это ужасная практика, — но он лаконично демонстрирует принцип.
В предыдущем примере, когдаchild.fooдаundefined, он цитируетparent.foo. однажды вchildопределено вышеfoo,child.fooценность'bar',ноparent.fooИсходное значение сохраняется. как только мыdelete child.foo, он снова будет ссылатьсяparent.foo, что означает, что когда мы меняем значение родителя,child.fooОтносится к новому значению.
Давайте посмотрим, что только что произошло (для ясности предположим, что этоStringsвместо строковых литералов разница здесь не имеет большого значения):
Как это работает, особенноnewа такжеthisВозможности — это отдельная тема, но если вы хотите узнать больше, загляните на сайт Mozilla.Обширная статья о прототипной цепочке наследования JavaScript..
Ключевым моментом является то, что прототип не определенtype, которые самиinstances, и они изменяются во время выполнения.
У вас еще есть смелость читать дальше? Давайте вернемся назад и проанализируем классы JavaScript.
Тест по JavaScript №3: Как реализовать private в классе?
Приведенные выше свойства прототипа и класса не «инкапсулированы» как закрытые члены, недоступные для внешнего мира. Как следует решить эту проблему?
Здесь нет примеров кода. Ответ в том, что вы не можете.
В JavaScript нет понятия private, но есть замыкания:
function SecretiveProto() {
const secret = "The Class is a lie!"
this.spillTheBeans = function() {
console.log(secret)
}
}
const blabbermouth = new SecretiveProto()
try {
console.log(blabbermouth.secret)
}
catch(e) {
// TypeError: SecretiveClass.secret is not defined
}
blabbermouth.spillTheBeans() // "The Class is a lie!"
Вы понимаете, что только что произошло? Если вы этого не понимаете, вы не понимаете замыканий. Хорошо, но они менее пугающие и очень полезные, вам следуетНайдите время, чтобы узнать о них.
Тест JavaScript #4: Как использоватьclassключевое слово для написания кода, который выполняет ту же функцию, что и выше?
Извините, это еще один вопрос с подвохом. Вы можете сделать то же самое, но это выглядит так:
class SecretiveClass {
constructor() {
const secret = "I am a lie!"
this.spillTheBeans = function() {
console.log(secret)
}
}
looseLips() {
console.log(secret)
}
}
const liar = new SecretiveClass()
try {
console.log(liar.secret)
}
catch(e) {
console.log(e) // TypeError: SecretiveClass.secret is not defined
}
liar.spillTheBeans() // "I am a lie!"
Если вы думаете, что это выглядит лучше, чемSecretiveProtoПроще или понятнее, тогда, пожалуйста, дайте мне знать. По моему личному мнению, это что-то плохое — ломает JavaScriptclassДекларативная идиома, и она пришла не из Java, как можно было бы ожидать. Об этом будет свидетельствовать:
Тест JavaScript №5:SecretiveClass::looseLips()Для чего это?
Давайте посмотрим на этот код:
try {
liar.looseLips()
}
catch(e) {
// ReferenceError: secret is not defined
}
Хм... стыдно.
JavaScript Pop Quiz № 6: опытные разработчики JavaScript предпочитают прототипы или классы?
Как вы уже догадались, это снова вопрос мастерства — опытные разработчики JavaScript стараются избегать и того, и другого, насколько это возможно. Вот идиоматический хороший способ сделать это с помощью JavaScript:
function secretFactory() {
const secret = "Favor composition over inheritance, `new` is considered harmful, and the end is near!"
const spillTheBeans = () => console.log(secret)
return {
spillTheBeans
}
}
const leaker = secretFactory()
leaker.spillTheBeans()
Речь идет не только о том, чтобы избежать уродливого наследования или принудительной инкапсуляции. Думаю, вы можете использоватьsecretFactoryа такжеleakerСделайте что-то, что вы не можете легко сделать с прототипами или классами.
Во-первых, вы можете разобрать его, потому что вам не нужно беспокоиться оthisКонтекст:
const { spillTheBeans } = secretFactory()
spillTheBeans() // Favor composition over inheritance, (...)
Это действительно приятно. Помимо избеганияnewа такжеthisПомимо глупых вещей, это также позволяет нам взаимозаменяемо использовать объекты с модулями CommonJS и ES6. Это также упрощает разработку:
function spyFactory(infiltrationTarget) {
return {
exfiltrate: infiltrationTarget.spillTheBeans
}
}
const blackHat = spyFactory(leaker)
blackHat.exfiltrate() // Favor composition over inheritance, (...)
console.log(blackHat.infiltrationTarget) // undefined (looks like we got away with it)
использоватьblackHatпрограммистам не о чем беспокоитьсяexfiltrateоткуда,spyFactoryне используй этоFunction::bindКонтекстные трюки или глубоко вложенные свойства. Обратите внимание, что нам не нужно беспокоиться о простом коде процесса синхронизации.this, но вызывает различные проблемы в асинхронном коде.
Немного подумав,spyFactoryМожет быть превращен в очень сложный инструмент шпионажа, способный справиться с различными целями проникновения — другими словами,Внешний вид Режим.
Конечно, вы также можете сделать это с помощью классов или, скорее, классов, которые наследуются отabstract classилиinterfaceи т. д., но в JavaScript нет концепции абстракции или интерфейса.
Давайте используем лучший пример, чтобы увидеть, как это реализовать с помощью фабричного шаблона:
function greeterFactory(greeting = "Hello", name = "World") {
return {
greet: () => `${greeting}, ${name}!`
}
}
console.log(greeterFactory("Hey", "folks").greet()) // Hey, folks!
Это более лаконично, чем версия прототипа или класса. Это позволяет более эффективно инкапсулировать его свойства. Кроме того, в некоторых случаях он оказывает меньшее влияние на память и производительность (маловероятно на первый взгляд, но JIT-компилятор незаметно выполняет работу по сокращению дублирования и выводу типов за кулисами).
Так что безопаснее, быстрее и проще писать такой код. Зачем нам снова нужны занятия? О, и, конечно же, возможность повторного использования. Что, если мы хотим несчастного и восторженного приветствия? Ну, а если мы используемClassicalGreetingклассы, возможно, прыгнув прямо в иерархию классов мечты. Мы знаем, что нам нужны параметризованные символы, поэтому проведем рефакторинг и добавим несколько подклассов:
// Greeting class
class ClassicalGreeting {
constructor(greeting = "Hello", name = "World", punctuation = "!") {
this.greeting = greeting
this.name = name
this.punctuation = punctuation
}
greet() {
return `${this.greeting}, ${this.name}${this.punctuation}`
}
}
// An unhappy greeting
class UnhappyGreeting extends ClassicalGreeting {
constructor(greeting, name) {
super(greeting, name, " :(")
}
}
const classyUnhappyGreeting = new UnhappyGreeting("Hello", "everyone")
console.log(classyUnhappyGreeting.greet()) // Hello, everyone :(
// An enthusiastic greeting
class EnthusiasticGreeting extends ClassicalGreeting {
constructor(greeting, name) {
super(greeting, name, "!!")
}
greet() {
return super.greet().toUpperCase()
}
}
const greetingWithEnthusiasm = new EnthusiasticGreeting()
console.log(greetingWithEnthusiasm.greet()) // HELLO, WORLD!!
Это хороший подход, и все это не имеет смысла, пока кто-нибудь не придет и не попросит реализовать функцию, которая не совсем вписывается в иерархию. Когда мы попытаемся написать ту же функцию с фабричным шаблоном, закрепите эту идею:
const greeterFactory = (greeting = "Hello", name = "World", punctuation = "!") => ({
greet: () => `${greeting}, ${name}${punctuation}`
})
// Makes a greeter unhappy
const unhappy = (greeter) => (greeting, name) => greeter(greeting, name, ":(")
console.log(unhappy(greeterFactory)("Hello", "everyone").greet()) // Hello, everyone :(
// Makes a greeter enthusiastic
const enthusiastic = (greeter) => (greeting, name) => ({
greet: () => greeter(greeting, name, "!!").greet().toUpperCase()
})
console.log(enthusiastic(greeterFactory)().greet()) // HELLO, WORLD!!
Хотя его код короче, выгода не очевидна. На самом деле вам может быть труднее читать, может быть, это вялый подход. Разве мы не можем просто иметь одинunhappyGreeterFactoryс однимpassionsticGreeterFactory?
Затем появляется ваш клиент и говорит: «Мне нужен несчастный новый сотрудник, и я хочу, чтобы об этом узнал весь офис!»
console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(
Если нам нужно использовать этот восторженный и недовольный приветствующий более одного раза, это может быть проще сделать:
const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory))
console.log(aggressiveGreeterFactory("You're late", "Jim").greet())
Этот подход в стиле композиции работает с прототипами или классами. Например, вы можете поставитьUnhappyGreetingа такжеEnthusiasticGreetingПереосмыслить себя декоратором. Он по-прежнему требует больше шаблонного кода, чем описанный выше подход функционального стиля, но это то, что вы получаете задействительноЦена безопасности класса и инкапсуляции.
Проблема в том, что в JavaScript вы не получаете автоматическую защиту. подчеркиватьclassИспользование Фреймворки JavaScript делают много «волшебства» для решения этих проблем и заставляют классы использовать свое собственное поведение. Обратите внимание на ПолимерElementMixin исходный код, смею сказать. Это просто код уровня артефакта JavaScript, я не хочу показаться саркастичным.
Конечно, мы можем использоватьObject.freezeилиObject.definePropertiesдля решения некоторых из проблем, обсуждавшихся выше, для большего или меньшего эффекта. Но зачем эмулировать формы без функций, игнорируя инструменты, которые дает нам сам JavaScript? Используете ли вы молоток с надписью «отвертка» для закручивания шурупов, когда у вас есть настоящая отвертка рядом с ящиком для инструментов?
найти хорошую часть
Разработчики JavaScript часто подчеркивают преимущества языка. Мы решили попытаться избежать его сомнительного дизайна языка и ловушек, придерживаясь написания чистого, читаемого, минимального, многократно используемого кода.
Что касается того, какие части JavaScript разумны, я надеюсь, что убедил вас,classНи один из них. В противном случае, надеюсь, вы понимаете, что наследование в JavaScript может сбивать с толку. а такжеclassНи исправления, ни принуждения к пониманию прототипа. Дополнительные преимущества, если вы изучите подсказки о том, что шаблоны объектно-ориентированного проектирования работают без классов или наследования ES6.
Я не говорю вам избегать этого полностьюclass. Иногда вам нужно наследовать, иclassДля этого предусмотрен более чистый синтаксис. особенно,class X extends YЛучше, чем подход старого прототипа. Кроме того, многие популярные интерфейсные фреймворки поощряют его использование, и вам в принципе следует избегать написания странного нестандартного кода в одиночку. Мне просто не нравится, как это происходит.
В моем кошмаре целое поколение библиотек JavaScript используетclassнаписан с расчетом на то, что он будет вести себя как другие популярные языки. Даже если мы случайно не упалиclassЕго также можно воскресить не на том кладбище JavaScript. Опытные разработчики JavaScript часто страдают от этих монстров, потому что то, что популярно, не всегда хорошо.
В конце концов мы все сдались и начали изобретать колесо Rust, Go, Haskell или что-то в этом роде, затем компилировать в Wasm для Интернета, а новые веб-фреймворки и библиотеки распространялись на бесконечное количество языков.
Это не дает мне спать по ночам.