You Don't Know JS: this & Object Prototypes / Вы не знаете JS: this и прототипы объектов: Appendix A: ES6 class

← →
Английский оригинал Перевод на русский

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

Приложение А: `class` в ES6

baniok 10.09.17 в 16:43

0

Если что-то можно вынести из второй половины этой книги (Главы 4-6), так это то, что классы - опциональный паттерн проектирования кода (не является необходимым). Более того, зачастую, их неудобно реализовывать в "прототипном" языке вроде JS.

baniok 10.09.17 в 16:47

0

Это неудобство связано *не* только с синтаксисом, хоть он и играет значительную роль. Главы 4 и 5 рассмотрели некоторые синтаксические уродства: от многословных ссылок `.prototype`, загромождающих код, до *явного псевдо-полиморфизма* (смотрите Главу 4), когда вы называете метод одним и тем же именем на разных уровнях цепочки и пытаетесь реализовать полиморфные отсылки из методов низкого уровня к методам высокого уровня. Еще одно синтаксическое уродство - `.constructor`, который ошибочно интерпретируется как "был сконструирован с помощью" и тем не менее ненадёжен из-за такого определения.

baniok 10.09.17 в 16:59

0

Но проблема с дизайном классов намного глубже. Глава 4 указывает, что классы в традиционных класс-ориентированных языках на самом деле выполняют *копирование* от родительского к дочернему экземпляру, в то время как в `[[Prototype]]` выполняется не копирование, а скорее наоборот - связывающее делегирование.

baniok 10.09.17 в 17:02

0

По сравнению с простотой кода в OLOO-стиле и делегированием поведения (смотрите Главу 6), которое принимает `[[Prototype]]`, а не прячет его, классы торчат из JS как сломанный палец.

baniok 10.09.17 в 17:08

0

## `class`

baniok 10.09.17 в 17:08

0

Но нам *не нужно* повторять всё это. Я кратко упомянул эти проблемы только чтобы вы держали их в голове пока мы переключаем всё внимание на механизм `class` в ES6. Здесь мы покажем как они работают и посмотрим дает ли `class` что-то существенное в для решения всех этих "классовых" проблем.

baniok 10.09.17 в 17:16

0

Давайте вспомним пример `Widget` / `Button` из Главы 6:

baniok 10.09.17 в 17:16

0

```js
class Widget {
constructor(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
}

class Button extends Widget {
constructor(width,height,label) {
super( width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
render($where) {
super.render( $where );
this.$elem.click( this.onClick.bind( this ) );
}
onClick(evt) {
console.log( "Button '" + this.label + "' clicked!" );
}
}
```

baniok 21.09.17 в 18:26

0

Кроме того, что синтаксис *выглядит* приятнее, какую проблему решает ES6?

baniok 10.09.17 в 17:25

0

1. Больше нет (ну, типа того, смотрите ниже) отсылок к `.prototype`, загромождающих код.
2. `Button` объявляется напрямую чтобы "унаследовать" (`extends`) `Widget`, вместо использования `Object.create(..)` для замены привязанного `.prototype` или установки через `.__proto__` или `Object.setPrototypeOf(..)`.
3. `super(..)` теперь дает нам полезную возможность **относительный полиморфизм**, так что любой метод одного уровня цепочки может обратиться к методу с тем же именем на другой уровень выше по цепочке. Сюда входит решение к заметке из Главы 4 о странностях конструкторов, не принадлежащих к их классу и не связанных друг с другом -- `super()` работает внутри конструкторов именно так как вы ожидаете.
4. Литеральный синтаксис `class` не позволяет указать свойства (только методы). Для кого-то это покажется ограничением, но, скорее всего, большинство ситуаций, в которых свойство (состояние) существует где-либо, кроме конечных экземпляров, являются ошибочными и неожиданными (поскольку это состояние, которе явно "распространяется" по всем "экземплярам").
В общем, *можно* сказать, что синтаксис `class` защищает вас от ошибок.
5. `extends` позволяет вам расширить даже встроенные (под)типы объектов, вроде `Array` или `RegExp` очень естественным путём. Такие действия без `class .. extends` долгое время оставались избыточно сложной и удручающей задачей.

baniok 10.09.17 в 17:36

0

Справедливости ради, это лишь некоторые из важных решений множества наиболее очевидных (синтаксических) проблем и сюрпризов, которые встречают люди с кодом в классическом прототипном стиле.

baniok 21.09.17 в 18:31

0

## Глюки `class`

baniok 11.09.17 в 21:33

0

Но не всё так радужно. До сих пор существуют некоторые глубокие и тревожащие проблемы с использованием "классов" в качестве паттерна проектирования на JS.

baniok 20.09.17 в 12:45

0

Во-первых, синтаксис `class` может убедить вас, что в JS существует новый механизм "классов" ES6. **Нет**. `class` — это, в основном, синтаксический сахар поверх существующего механизма (делегирования!) `[[Prototype]]`.

baniok 20.09.17 в 12:47

0

Это означает, что `class` на самом деле не копирует содержимое статически во время объявления, как это происходит в традиционных класс-ориентированных языках. Если вы измените/замените метод (намеренно или случайно) родительского "класса", дочерний "класс" и/или экземпляр тоже будет затронут, поскольку они не копируются при объявлении, а всё еще используют модель делегирования, основанного на `[[Prototype]]`:

baniok 20.09.17 в 13:24

0


```js
class C {
constructor() {
this.num = Math.random();
}
rand() {
console.log( "Random: " + this.num );
}
}
var c1 = new C();
c1.rand(); // "Random: 0.4324299..."
C.prototype.rand = function() {
console.log( "Random: " + Math.round( this.num * 1000 ));
};
var c2 = new C();
c2.rand(); // "Random: 867"
c1.rand(); // "Random: 432" -- Ой!!!
```

baniok 20.09.17 в 13:24

0

Такое поведение только кажется разумным *если вы уже знаете* о делегирующей природе вещей и не ожидаете *копий* из "настоящих классов". Поэтому задайте себе вопрос: почему вы выбираете синтаксис `class` для чего-то фундаментально отличающегося от классов?

baniok 20.09.17 в 13:31

0

Может быть синтаксис `class` в ES6 **просто мешает** увидеть и понять разницу между традиционными классами и делегированными объектами?

baniok 20.09.17 в 13:44

0

Синтаксис `class` *не предоставляет* способ объявить свойства экземпляра класса (только методы). Поэтому если вам нужно это для отслеживания состояния между экземплярами, вы вернётесь обратно к некрасивому синтаксису `.prototype`, вроде такого:

baniok 20.09.17 в 13:46

0

```js
class C {
constructor() {
// убедитесь, что изменяете общее состояние,
// а не добавляете затеняющее свойство
// к экземплярам!
C.prototype.count++;
// Здесь, `this.count` работает как и ожидается
// через делегирование
console.log( "Hello: " + this.count );
}
}
// добавим свойство для общего состояния напрямую
// к объекту-прототипу
C.prototype.count = 0;
var c1 = new C();
// Hello: 1
var c2 = new C();
// Hello: 2
c1.count === 2; // true
c1.count === c2.count; // true
```

baniok 20.09.17 в 13:47

0

Самая большая проблема здесь в том, что он предаёт синтаксис `class`, выставляя (утечка!) `.prototype` как часть реализации.

baniok 20.09.17 в 13:50

0

Но у нас всё еще остался неожиданный глюк, когда `this.count++` неявно создает затеняющее свойство `.count` в обоих объектах `c1` и `c2`, вместо того, чтобы обновить общее состояние. `class` не предлагает нам решения этой проблемы, кроме, кажется, предположения, что вы не должны так делать *вообще*, в виду слабой поддержки синтаксиса.

baniok 20.09.17 в 14:13

0

Более того, случайное затенение всё еще представляет угрозу:

baniok 20.09.17 в 14:14

0

```js
class C {
constructor(id) {
// Ой, блин, мы затеняем метод `id()`
// значением свойства в экземпляре
this.id = id;
}
id() {
console.log( "Id: " + this.id );
}
}
var c1 = new C( "c1" );
c1.id(); // TypeError -- `c1.id` теперь строка "c1"
```

baniok 20.09.17 в 14:15

0

Существует еще один тонкий нюанс, связанный с работой `super`. Вы могли предположить, что `super` будет привязан по аналогии с привязкой `this` (смотрите Главу 2), что `super` всегда будет привязан на уровень выше, вне зависимости от текущего положения метода в цепочке `[[Prototype]]`.

baniok 20.09.17 в 14:25

0

Тем не менее, в целях повышения производительности (привязка `this` и так дорого стоит), `super` не привязывается динамически. Его привязка вроде как «статичная», как и момент вызова. Не так уж страшно, верно?

baniok 20.09.17 в 14:26

0

Эх... может быть, а может и нет. Если вы как и большинство разработчиков на JS начинаете назначать функции различным объектам (что следует из определения `class`) различными способами, вас, возможно, не очень обеспокоит, что под капотом механизм `super` вынужден каждый раз привязывать себя заново.

baniok 20.09.17 в 14:30

0

И в зависимости выбранного синтаксического подхода к присваиванию возможны случаи, когда `super` не может быть корректно привязан (по крайней мере не там, где вы ожидаете), поэтому вам может понадобиться (на момент написания обсуждение TC39 продолжается) привязать `super` вручную через `toMethod(..)` (наподобие того как вы делаете `bind(..)` для `this` -- смотрите Главу 2).

baniok 20.09.17 в 14:47

0

Вы привыкли к возможности присваивать методы различным объектам чтобы *автоматически* получать выгоду от динамизма `this` через *скрытое привязывание* (смотрите Главу 2). Но, похоже, всё это не сработает для методов, использующих `super`.

baniok 20.09.17 в 14:49

0

Рассмотрим что `super` должен делать здесь (напротив `D` и `E`):

baniok 20.09.17 в 15:09

0

```js
class P {
foo() { console.log( "P.foo" ); }
}
class C extends P {
foo() {
super();
}
}
var c1 = new C();
c1.foo(); // "P.foo"

var D = {
foo: function() { console.log( "D.foo" ); }
};
var E = {
foo: C.prototype.foo
};
// Link E to D for delegation
Object.setPrototypeOf( E, D );
E.foo(); // "P.foo"
```

baniok 20.09.17 в 15:09

0

Если вы думали (вполне обоснованно!), что `super` будет привязан динамически во время вызова, вы могли ожидать, что `super` автоматически распознает, что `E` делегирует к `D`, поэтому `E.foo()`, используя `super()`, должен вызвать `D.foo()`.

baniok 20.09.17 в 15:11

0

**Нет.** В целях производительности, `super` не использует *отложенную привязку* (динамическую привязку) как это делает `this`. На самом деле он получен во время вызова из `[[HomeObject]].[[Prototype]]`, где `[[HomeObject]]` статично привязан в момент создания.

baniok 20.09.17 в 15:18

0

В данном конкретном примере `super()` всё еще разрешается в `P.foo()`, поскольку `[[HomeObject]]` этого метода всё еще `C`, а `C.[[Prototype]]` является `P`.

baniok 20.09.17 в 15:20

0

*Возможно*, найдутся и пути решения таких глюков. Использование `toMethod(..)` чтобы привязать/перепривязать `[[HomeObject]]` для метода (вместе с заданием `[[Prototype]]` этого объекта!), кажется, сработает в таком случае:

baniok 20.09.17 в 15:23

0

```js
var D = {
foo: function() { console.log( "D.foo" ); }
};
// Привязать E к D для делегирования
var E = Object.create( D );
// вручную связать `[[HomeObject]]` из `foo` в виде
// `E`, а `E.[[Prototype]]` — это `D`, так что
// `super()` — это `D.foo()`
E.foo = C.prototype.foo.toMethod( E, "foo" );
E.foo(); // "D.foo"
```

**Примечание:** `toMethod(..)` клонирует метод и принимает `homeObject` в качестве первого параметра (поэтому мы передаём `E`), а второй параметр (необязательный) задаёт `name` для нового метода (который хранится в "foo").

baniok 20.09.17 в 15:34

0

Осталось увидеть нет ли глюков в других крайних случаях, которые разрабы встретят за пределами описанного сценария. Как бы то ни было, вам нужно быть начеку и замечать места, где движок автоматически разбирается с `super` за вас, и места, где вам нужно самим об этом позаботиться. **Ох!**

baniok 20.09.17 в 15:53

0

# Статический > Динамический?

baniok 20.09.17 в 15:55

0

Но самая большая проблема `class` ES6 в том, что все эти глюки означают, что `class` как бы навязывает вам синтаксис, который, якобы, подразумевает (как традиционные классы), что однажды объявленный `class` является статическим определением чего-либо (наследуемого в будущем). Вы полностью теряете осознание факта, что `C` — это объект, конкретная вещь, с который вы можете взаимодействовать напрямую.

baniok 20.09.17 в 16:03

0

В традиционных класс-ориентированных языках вы никогда не измените определение класса в дальнейшем, поэтому классы как паттерн проектирования не предлагают таких возможностей. Но **одна из самых мощных особенностей** JS состоит в том, что он является *динамическим*, а определение любого объекта (пока вы не сделаете его иммутабельным) — это *вещь* подвижная и мутабельная.

baniok 20.09.17 в 16:09

0

`class` вроде подразумевает, что вы не должны делать такие штуки, склоняя вас использовать уродливый синтаксис `.prototype` или заставляя вас думать о подвохах `super` и т.д. Он также предоставляет *очень слабую* поддержку на случай подводных камней, которые может принести такой динамизм.

baniok 20.09.17 в 16:18

0

Другими словами, `class` как бы говорит вам: «Динамика — это сильно сложно, так что, возможно, это не лучшая идея». Вот вам синтаксис, который выглядит как статический, так что пишите свой код статически.»

baniok 21.09.17 в 10:09

0

Какой грустный комментарий к JS: **динамика слишком сложная, давайте притворимся (но на самом деле не будем) статикой**

baniok 21.09.17 в 17:30

0

Это причины, по которым `class` в ES6 маскируется под красивое решение синтаксической головной боли, но на самом деле еще сильней мутит воду и ухудшает четкость и краткость понимания JS и его особенностей.

baniok 21.09.17 в 17:36

0

*Примечание:** Если вы используете инструмент `.bind(..)` чтобы создать жестко привязанную функцию (см. Главу 2), эта функция не может быть наследована с помощью `extend` из ES6, в отличие от обычных функций.

baniok 21.09.17 в 17:38

0

## Обзор (TL;DR)

baniok 21.09.17 в 17:50

0

`class` очень хорошо притворяется, что решает проблемы с паттерном класс/наследование в JS. Но на самом деле делает обратное: **он скрывает многие проблемы, но приносит другие, незаметные, но опасные**.

baniok 21.09.17 в 18:00

0

`class` способствует постоянной путанице с «классами» в JS, которая преследует язык около двух десятков лет. Во многом он вызывает больше вопросов чем ответов и в целом чувствуется, что он противоестественно расположился над элегантной простотой механизма `[[Prototype]]`.

baniok 21.09.17 в 18:17

0

Итоги: в ES6 `class` затрудняет надёжное использование `[[Prototype]]` и скрывает самую важную особенность механизма объектов в JS -- **живое делегирование связей между объектами** -- не лучше ли рассматривать `class` как создающий больше проблем чем решающий и просто отнести его к анти-паттерну?

baniok 21.09.17 в 18:22

0
Минутку...

Следующая страница →