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

Spread the love

В статье высказывается частное мнение о состояние и развитие концепции ООП в JavaScript.

Перевод: Fernando DoglioWhat’s Wrong with Classes in 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. Статический полиморфизм позволяет нам определять один и тот же метод несколько раз в одном классе, но с разными сигнатурами. Другими словами, используется много раз одно и тоже имя функции, но с разными параметрами. Теперь у нас есть параметр rest с JS, и это позволяет нам использовать произвольное число аргументов, однако это также означает, что мы должны добавить дополнительный код в наш метод для обработки этого уровня динамизма. Если бы вместо этого мы могли более четко различать сигнатуры методов, мы могли бы напрямую инкапсулировать разные варианты одного и того же поведения в разные методы.

Image for post
Image for post

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

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

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

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

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

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

А что насчет вас? Какую концепцию ООП вам не хватает в JS, о которой я еще не упомянул? Или, что еще лучше, какая из уже упомянутых идей вам нужна больше всего? Оставьте комментарий в блоге автора статьи (What’s Wrong with Classes in JavaScript?)!

Была ли вам полезна эта статья?
[11 / 3]

Spread the love
Подписаться
Уведомление о
guest
4 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Анонимно
Анонимно
3 лет назад

Статья ни о чем

Анонимно
Анонимно
3 лет назад

Приватные методы как раз добавили в последней версии. Последний абзац не актуален получается. https://learn.javascript.ru/private-protected-properties-methods

Анонимно
Анонимно
3 лет назад

Последний абзац не о них

Анонимно
Анонимно
3 лет назад

TLDR: в JS нет строгой типизации. ООП здесь притянут за уши