Что не так с классами в JavaScript?

От автора: я сторонник синтаксического сахара, но нам все еще не хватает некоторых вещей. Дело не в том, что с классами JavaScript что-то не так, но если вы какое-то время работали с языком, особенно если вы работали с ES5 раньше, вы, вероятно, видели эволюцию от наследования прототипов к текущей модели классов.

Но почему? Что не так с цепочкой прототипов? По моему скромному мнению, ответ на этот вопрос: ничего. Но сообщество потратило годы на внедрение концепции классов в разные конструкции и библиотеки, поэтому технический комитет ECMA решил добавить ее в любом случае.

Вы спросите, в чем проблема? Все, что они действительно сделали — это добавили немного макияжа поверх уже имеющегося у нас прототипного наследования и решили назвать его «классами», что, в свою очередь, дает разработчикам представление о том, что они имеют дело с объектно-ориентированным языком, что в действительности не так.

Классы — ни что иное, как синтаксический сахар

JavaScript не имеет полной поддержки ООП, ее никогда не было, и это потому, что он никогда не нуждался в ней. На поверхностном уровне текущая версия классов демонстрирует парадигму ООП, потому что:

Мы можем создать определение нашего базового класса, состояние группировки и поведение вместе с очень классическим синтаксисом.

Мы можем наследовать от одного класса к другому.

Мы можем определить видимость наших свойств и методов между общедоступным и частным (хотя частные поля все еще являются экспериментальной функцией).

Мы можем определять геттеры и сеттеры для наших свойств.

Мы можем переопределить методы во время наследования классов.

Конечно, мы можем создавать экземпляры наших классов.

Так почему я говорю, что классы — это синтаксический сахар? Поскольку, хотя на поверхностном уровне они могут показаться очень «объектно-ориентированными», если мы попытаемся сделать что-то за пределами их возможностей, например, определение одного класса, расширяющего два других (что в настоящее время невозможно), нам нужно использовать следующий код:

//The helper function
function applyMixins(derivedCtor, baseCtors) { baseCtors.forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { let descriptor = Object.getOwnPropertyDescriptor(baseCtor.prototype, name) Object.defineProperty(derivedCtor.prototype, name, descriptor); }); });
} //The parent classes
class A { methodA(){ console.log("A") }
} class B { methodB(){ console.log("B") }
} //The child class
class C { } //Using mixins
applyMixins(C, [A, B])
let o = new C()
o.methodA()
o.methodB()

Нам нужно это сделать, потому что в JS мы не можем писать:

class A { methodA(){ console.log("A") }
} class B { methodB(){ console.log("B") }
} class C extends A, B { }

И поскольку бывают случаи, когда такое поведение может пригодиться, ребята из TypeScript создали приведенный выше фрагмент, я просто удалил лишний код, чтобы он работал для ванильного JS.

Но ключевым выводом из примера кода должна быть функция applyMixins. Даже если вы не совсем понимаете, что он пытается сделать, вы можете ясно видеть, что он обращается к свойствам прототипа всех классов для копирования и переназначения методов и свойств. Это все необходимое доказательство, чтобы увидеть истину: классы — это не что иное, как синтаксический сахар на вершине испытанной и верной прототипной модели наследования.

Означает ли это, что мы должны прекратить использовать классы? Вовсе нет, но это важно понимать, и тот факт, что если вам когда-либо понадобится раздвинуть границы этих классов, можно и нельзя, вам придется иметь дело с прототипами для этого.

Чего же тогда не хватает в ООП-модели JavaScript?

Если наша текущая модель ООП настолько тонкая и представляет собой всего лишь слой абстракции над прототипным наследованием, что именно нам не хватает? Что могло сделать JS действительно ООП?

Хороший способ оценить этот вопрос — просто посмотреть, что делает TypeScript. Команда, стоящая за этим языком, определенно подталкивает JavaScript к пределам, создавая что-то, что можно перевести на JS. Это, в свою очередь, также ограничивает их возможности, но хороший способ начать наш список желаний ООП — это посмотреть на их функции, связанные с ООП:

Есть одно предостережение, которое вы заметите через секунду: некоторые из недостающих ООП-конструкций в JavaScript прямо сейчас имеют встроенную функцию проверки типов, которая не имеет реального значения внутри динамически типизированного языка, и, вероятно, поэтому они этого не сделали.

Интерфейсы

Это отличные конструкции, помогающие определить API, которому должен соответствовать класс. Одно из основных преимуществ интерфейсов, которое было бы потеряно внутри безтипового JS, заключается в том, что вы можете определить переменную любого класса, реализующего тот же интерфейс, и безопасно вызывать любой из его методов.

interface Animal { speak()
} class Dog implements Animal{ speak() { console.log("Woof!") }
} class Cat implements Animal{ speak() { console.log("Meau!") }
} class Human implements Animal{ speak() { console.log("Hey dude, what's up?") }
} //if we had Interfaces in JS we could safely do:
let objects = [new Dog(), new Cat(), new Human()]
objects.forEach(o => o.speak())

Этого нельзя сделать на простом JS

Вы определенно можете добиться того же с помощью класса, определяющего метод speak, а затем перезаписывая его. Но опять же, вы также можете сделать это на любом другом языке с сильной ООП. Интерфейсы чище и элегантнее.

Абстрактные классы

Я определенно скучаю по абстрактным классам из JS всякий раз, когда пытаюсь использовать свой код полностью ООП. Абстрактный класс — это класс, который определяет и реализует методы, но никогда не будет создан. Это способ группировки общего поведения, который можно расширить, но никогда не использовать напрямую. Это отличный ресурс, и это определенно то, что можно было бы реализовать в текущей области JS без особых проблем.

Статический полиморфизм

Мне лично понравился этот вариант из моих дней ООП, и иногда мне кажется, что он пригодился бы, если бы он поддерживался и в JS. Статический полиморфизм позволяет нам определять один и тот же метод несколько раз в одном классе, но с разными сигнатурами. Другими словами, повторяйте имя, но убедитесь, что оно принимает другие параметры. Теперь у нас есть остальные параметры с JS, и это позволяет нам иметь произвольное число, однако это также означает, что мы должны добавить дополнительный код в метод для обработки этого уровня динамизма. Если бы вместо этого мы могли более четко различать сигнатуры методов, мы могли бы напрямую инкапсулировать разные разновидности одного и того же поведения в разные методы.

Что не так с классами в JavaScript?

Что не так с классами в JavaScript?

Первая версия не является допустимым JS, но она обеспечивает более чистый код и, следовательно, требует меньше когнитивной нагрузки для мысленного анализа. Версия вторая, однако, полностью верна. Это требует небольшого мысленного анализа, и вокруг больше кода, потому что это не только ведение журнала (что должно быть его единственной целью), но также попытка решить, КАК регистрировать, на основе предоставленных вами параметров.
Статический полиморфизм обычно реализуется путем рассмотрения типов параметров, полученных в методах. Однако из-за того, как работает JS, мы знаем, что это невозможно.

Защищенные свойства и методы

У нас уже есть общедоступная видимость, и скоро мы получим частную видимость методов и свойств (хотя и через префикс #, который все еще разбивает мне сердце). Я предполагаю, что следующим логическим шагом будет добавление защищенной видимости, однако сейчас она отсутствует, и я думаю, что все три необходимы, если вы хотите иметь надлежащий «опыт ООП». Доступ к защищенным свойствам и методам можно получить только из класса или одного из его дочерних элементов (в отличие от частной видимости, которая ограничивает доступ только к родительскому классу).
Мне всегда было сложно назвать JS языком ООП, и пока я не увижу способ работы с внутренними компонентами классов без необходимости ссылаться на цепочку прототипов, я буду продолжать бороться. Почему они не могли просто продолжить работу над расширением прототипной модели наследования вместо того, чтобы предоставить нам эту дешевую версию классов? Это вопрос на века.

Прямо сейчас я просто скажу «спасибо» за добавленный синтаксический сахар и буду следить за новыми функциями на основе ООП в будущем.

Автор: Fernando Doglio

Источник: blog.bitsrc.io

Редакция: Команда webformyself.