You Don't Know JS: this & Object Prototypes / Вы не знаете JS: this и прототипы объектов: Appendix A: ES6 class
| Английский оригинал | Перевод на русский | |
|---|---|---|
Сожалеем, но текст оригинала доступен только зарегистрированным пользователям. |
Приложение А: `class` в ES6 baniok 10.09.17 в 16:43 |
|
|
Если что-то можно вынести из второй половины этой книги (Главы 4-6), так это то, что классы - опциональный паттерн проектирования кода (не является необходимым). Более того, зачастую, их неудобно реализовывать в "прототипном" языке вроде JS. baniok 10.09.17 в 16:47 |
||
|
Это неудобство связано *не* только с синтаксисом, хоть он и играет значительную роль. Главы 4 и 5 рассмотрели некоторые синтаксические уродства: от многословных ссылок `.prototype`, загромождающих код, до *явного псевдо-полиморфизма* (смотрите Главу 4), когда вы называете метод одним и тем же именем на разных уровнях цепочки и пытаетесь реализовать полиморфные отсылки из методов низкого уровня к методам высокого уровня. Еще одно синтаксическое уродство - `.constructor`, который ошибочно интерпретируется как "был сконструирован с помощью" и тем не менее ненадёжен из-за такого определения. baniok 10.09.17 в 16:59 |
||
|
Но проблема с дизайном классов намного глубже. Глава 4 указывает, что классы в традиционных класс-ориентированных языках на самом деле выполняют *копирование* от родительского к дочернему экземпляру, в то время как в `[[Prototype]]` выполняется не копирование, а скорее наоборот - связывающее делегирование. baniok 10.09.17 в 17:02 |
||
|
По сравнению с простотой кода в OLOO-стиле и делегированием поведения (смотрите Главу 6), которое принимает `[[Prototype]]`, а не прячет его, классы торчат из JS как сломанный палец. baniok 10.09.17 в 17:08 |
||
|
## `class` baniok 10.09.17 в 17:08 |
||
|
Но нам *не нужно* повторять всё это. Я кратко упомянул эти проблемы только чтобы вы держали их в голове пока мы переключаем всё внимание на механизм `class` в ES6. Здесь мы покажем как они работают и посмотрим дает ли `class` что-то существенное в для решения всех этих "классовых" проблем. baniok 10.09.17 в 17:16 |
||
|
Давайте вспомним пример `Widget` / `Button` из Главы 6: baniok 10.09.17 в 17:16 |
||
|
```js baniok 21.09.17 в 18:26 |
||
|
Кроме того, что синтаксис *выглядит* приятнее, какую проблему решает ES6? baniok 10.09.17 в 17:25 |
||
|
1. Больше нет (ну, типа того, смотрите ниже) отсылок к `.prototype`, загромождающих код. baniok 10.09.17 в 17:36 |
||
|
Справедливости ради, это лишь некоторые из важных решений множества наиболее очевидных (синтаксических) проблем и сюрпризов, которые встречают люди с кодом в классическом прототипном стиле. baniok 21.09.17 в 18:31 |
||
|
## Глюки `class` baniok 11.09.17 в 21:33 |
||
|
Но не всё так радужно. До сих пор существуют некоторые глубокие и тревожащие проблемы с использованием "классов" в качестве паттерна проектирования на JS. baniok 20.09.17 в 12:45 |
||
|
Во-первых, синтаксис `class` может убедить вас, что в JS существует новый механизм "классов" ES6. **Нет**. `class` — это, в основном, синтаксический сахар поверх существующего механизма (делегирования!) `[[Prototype]]`. baniok 20.09.17 в 12:47 |
||
|
Это означает, что `class` на самом деле не копирует содержимое статически во время объявления, как это происходит в традиционных класс-ориентированных языках. Если вы измените/замените метод (намеренно или случайно) родительского "класса", дочерний "класс" и/или экземпляр тоже будет затронут, поскольку они не копируются при объявлении, а всё еще используют модель делегирования, основанного на `[[Prototype]]`: baniok 20.09.17 в 13:24 |
||
|
baniok 20.09.17 в 13:24 |
||
|
Такое поведение только кажется разумным *если вы уже знаете* о делегирующей природе вещей и не ожидаете *копий* из "настоящих классов". Поэтому задайте себе вопрос: почему вы выбираете синтаксис `class` для чего-то фундаментально отличающегося от классов? baniok 20.09.17 в 13:31 |
||
|
Может быть синтаксис `class` в ES6 **просто мешает** увидеть и понять разницу между традиционными классами и делегированными объектами? baniok 20.09.17 в 13:44 |
||
|
Синтаксис `class` *не предоставляет* способ объявить свойства экземпляра класса (только методы). Поэтому если вам нужно это для отслеживания состояния между экземплярами, вы вернётесь обратно к некрасивому синтаксису `.prototype`, вроде такого: baniok 20.09.17 в 13:46 |
||
|
```js baniok 20.09.17 в 13:47 |
||
|
Самая большая проблема здесь в том, что он предаёт синтаксис `class`, выставляя (утечка!) `.prototype` как часть реализации. baniok 20.09.17 в 13:50 |
||
|
Но у нас всё еще остался неожиданный глюк, когда `this.count++` неявно создает затеняющее свойство `.count` в обоих объектах `c1` и `c2`, вместо того, чтобы обновить общее состояние. `class` не предлагает нам решения этой проблемы, кроме, кажется, предположения, что вы не должны так делать *вообще*, в виду слабой поддержки синтаксиса. baniok 20.09.17 в 14:13 |
||
|
Более того, случайное затенение всё еще представляет угрозу: baniok 20.09.17 в 14:14 |
||
|
```js baniok 20.09.17 в 14:15 |
||
|
Существует еще один тонкий нюанс, связанный с работой `super`. Вы могли предположить, что `super` будет привязан по аналогии с привязкой `this` (смотрите Главу 2), что `super` всегда будет привязан на уровень выше, вне зависимости от текущего положения метода в цепочке `[[Prototype]]`. baniok 20.09.17 в 14:25 |
||
|
Тем не менее, в целях повышения производительности (привязка `this` и так дорого стоит), `super` не привязывается динамически. Его привязка вроде как «статичная», как и момент вызова. Не так уж страшно, верно? baniok 20.09.17 в 14:26 |
||
|
Эх... может быть, а может и нет. Если вы как и большинство разработчиков на JS начинаете назначать функции различным объектам (что следует из определения `class`) различными способами, вас, возможно, не очень обеспокоит, что под капотом механизм `super` вынужден каждый раз привязывать себя заново. baniok 20.09.17 в 14:30 |
||
|
И в зависимости выбранного синтаксического подхода к присваиванию возможны случаи, когда `super` не может быть корректно привязан (по крайней мере не там, где вы ожидаете), поэтому вам может понадобиться (на момент написания обсуждение TC39 продолжается) привязать `super` вручную через `toMethod(..)` (наподобие того как вы делаете `bind(..)` для `this` -- смотрите Главу 2). baniok 20.09.17 в 14:47 |
||
|
Вы привыкли к возможности присваивать методы различным объектам чтобы *автоматически* получать выгоду от динамизма `this` через *скрытое привязывание* (смотрите Главу 2). Но, похоже, всё это не сработает для методов, использующих `super`. baniok 20.09.17 в 14:49 |
||
|
Рассмотрим что `super` должен делать здесь (напротив `D` и `E`): baniok 20.09.17 в 15:09 |
||
|
```js baniok 20.09.17 в 15:09 |
||
|
Если вы думали (вполне обоснованно!), что `super` будет привязан динамически во время вызова, вы могли ожидать, что `super` автоматически распознает, что `E` делегирует к `D`, поэтому `E.foo()`, используя `super()`, должен вызвать `D.foo()`. baniok 20.09.17 в 15:11 |
||
|
**Нет.** В целях производительности, `super` не использует *отложенную привязку* (динамическую привязку) как это делает `this`. На самом деле он получен во время вызова из `[[HomeObject]].[[Prototype]]`, где `[[HomeObject]]` статично привязан в момент создания. baniok 20.09.17 в 15:18 |
||
|
В данном конкретном примере `super()` всё еще разрешается в `P.foo()`, поскольку `[[HomeObject]]` этого метода всё еще `C`, а `C.[[Prototype]]` является `P`. baniok 20.09.17 в 15:20 |
||
|
*Возможно*, найдутся и пути решения таких глюков. Использование `toMethod(..)` чтобы привязать/перепривязать `[[HomeObject]]` для метода (вместе с заданием `[[Prototype]]` этого объекта!), кажется, сработает в таком случае: baniok 20.09.17 в 15:23 |
||
|
```js baniok 20.09.17 в 15:34 |
||
|
Осталось увидеть нет ли глюков в других крайних случаях, которые разрабы встретят за пределами описанного сценария. Как бы то ни было, вам нужно быть начеку и замечать места, где движок автоматически разбирается с `super` за вас, и места, где вам нужно самим об этом позаботиться. **Ох!** baniok 20.09.17 в 15:53 |
||
|
# Статический > Динамический? baniok 20.09.17 в 15:55 |
||
|
Но самая большая проблема `class` ES6 в том, что все эти глюки означают, что `class` как бы навязывает вам синтаксис, который, якобы, подразумевает (как традиционные классы), что однажды объявленный `class` является статическим определением чего-либо (наследуемого в будущем). Вы полностью теряете осознание факта, что `C` — это объект, конкретная вещь, с который вы можете взаимодействовать напрямую. baniok 20.09.17 в 16:03 |
||
|
В традиционных класс-ориентированных языках вы никогда не измените определение класса в дальнейшем, поэтому классы как паттерн проектирования не предлагают таких возможностей. Но **одна из самых мощных особенностей** JS состоит в том, что он является *динамическим*, а определение любого объекта (пока вы не сделаете его иммутабельным) — это *вещь* подвижная и мутабельная. baniok 20.09.17 в 16:09 |
||
|
`class` вроде подразумевает, что вы не должны делать такие штуки, склоняя вас использовать уродливый синтаксис `.prototype` или заставляя вас думать о подвохах `super` и т.д. Он также предоставляет *очень слабую* поддержку на случай подводных камней, которые может принести такой динамизм. baniok 20.09.17 в 16:18 |
||
|
Другими словами, `class` как бы говорит вам: «Динамика — это сильно сложно, так что, возможно, это не лучшая идея». Вот вам синтаксис, который выглядит как статический, так что пишите свой код статически.» baniok 21.09.17 в 10:09 |
||
|
Какой грустный комментарий к JS: **динамика слишком сложная, давайте притворимся (но на самом деле не будем) статикой** baniok 21.09.17 в 17:30 |
||
|
Это причины, по которым `class` в ES6 маскируется под красивое решение синтаксической головной боли, но на самом деле еще сильней мутит воду и ухудшает четкость и краткость понимания JS и его особенностей. baniok 21.09.17 в 17:36 |
||
|
*Примечание:** Если вы используете инструмент `.bind(..)` чтобы создать жестко привязанную функцию (см. Главу 2), эта функция не может быть наследована с помощью `extend` из ES6, в отличие от обычных функций. baniok 21.09.17 в 17:38 |
||
|
## Обзор (TL;DR) baniok 21.09.17 в 17:50 |
||
|
`class` очень хорошо притворяется, что решает проблемы с паттерном класс/наследование в JS. Но на самом деле делает обратное: **он скрывает многие проблемы, но приносит другие, незаметные, но опасные**. baniok 21.09.17 в 18:00 |
||
|
`class` способствует постоянной путанице с «классами» в JS, которая преследует язык около двух десятков лет. Во многом он вызывает больше вопросов чем ответов и в целом чувствуется, что он противоестественно расположился над элегантной простотой механизма `[[Prototype]]`. baniok 21.09.17 в 18:17 |
||
|
Итоги: в ES6 `class` затрудняет надёжное использование `[[Prototype]]` и скрывает самую важную особенность механизма объектов в JS -- **живое делегирование связей между объектами** -- не лучше ли рассматривать `class` как создающий больше проблем чем решающий и просто отнести его к анти-паттерну? baniok 21.09.17 в 18:22 |
