Классы в ECMAScript 6

July 5, 2015

Недавно, TC39 определили финальную семантику классов в ECMAScript 6 2.
Это статья поясняет как работает их реализация. Наиболее значимые из недавних
изменений связаны с тем, как реализована система наследования классов.

1. Обзор

class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}

class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return super.toString() + ' in ' + this.color;
}
}

let cp = new ColorPoint(25, 8, 'green');
cp.toString(); // '(25, 8) in green'

console.log(cp instanceof ColorPoint); // true
console.log(cp instanceof Point); // true

2. Основы

2.1 Базовые классы


Классы определяются в ECMAScript 6 (ES6) следующим образом:

class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}

Использовать этот класс можно просто вызвав конструктор функции, как в ES5:

> var p = new Point(25, 8);
> p.toString()
'(25, 8)'

По факту, результатом создания такого класса будет функция:

> typeof Point
'function'

Однако, вы можете вызывать класс только через new, а не через вызов
функции (Секция 9.2.2 в спецификации):

> Point()
TypeError: Classes can’t be function-called

Объявления классов не поднимаются


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

foo(); // работает, так как `foo` _поднялась_

function foo() {}


В отличие от функций, определения классов не поднимаются. Таким образом,
класс существует только после того, как его определение было достигнуто
и выполнено. Попытка создания класса до этого момента приведет к
«ReferenceError»:

new Foo(); // ReferenceError

class Foo {}


Причина этого ограничения в том, что классы могут быть наследниками. Это
поддерживается с помощью выражения «extends», значение которого может быть
произвольным. Это выражение должно быть установлено в определенном месте,
которое не может быть поднято.

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

function functionThatUsesBar() {
new Bar();
}

functionThatUsesBar(); // ReferenceError
class Bar {}
functionThatUsesBar(); // OK

Выражения класса


Так же, как и для функций, есть два способа определить класс: объявление
класса
и выражение класса.

По аналогии с функциями, идентификатор выражения класса доступен только
внутри выражения:

const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
console.log(inst.getClassName()); // Me
console.log(Me.name); // ReferenceError: Me не определен

2.2 Внутри тела определения класса


Тело класса может содержать только методы, но не свойства.
Прототип, имеющий свойства, обычно считается анти-паттерном.

«Сonstructor», статические методы, прототипные методы


Давайте рассмотрим три вида методов, которые вы часто можете встретить
в классах.

class Foo {
constructor(prop) {
this.prop = prop;
}
static staticMethod() {
return 'classy';
}
prototypeMethod() {
return 'prototypical';
}
}
let foo = new Foo(123);

Диаграмма объекта для это определения класса выглядит следующим образом:

(Совет для понимания: [[Prototype]] — это отношения наследования между
объектами, в то время как prototype — обычное свойство, значением которого
является объект. Значение свойства prototype оператор new использует
как прототип для создаваемых объектов.)

Схема наследования

Для начала рассмотрим псевдо-метод «constructor». Этот метод является
особенным, так как он определяет функцию, которая представляет собой класс:

> Foo === Foo.prototype.constructor
true
> typeof Foo
'function'

Иногда его называют конструктором класса. Он имеет особенности, которые
обычный конструктор функции не имеет (главным образом, способность
конструктора вызывать конструктор базового класса через super(), о котором
я расскажу чуть позже).

Далее, статические методы. Статические свойства (или свойства класса)
являются свойствами самого Foo. Если вы определили метод с помощью static,
значит вы реализовали метод класса:

> typeof Foo.staticMethod
'function'
> Foo.staticMethod()
'classy'

В третьих, прототипные методы. свойства прототипа Foo являются и
свойствами Foo.prototype. Это, как правило, методы, и наследуются
экземплярами Foo.

> typeof Foo.prototype.prototypeMethod
'function'
> foo.prototypeMethod()
'prototypical'

Геттеры и Сеттеры


Синтаксис для геттеров и сеттеров такой же как и в
ECMAScript 5 литералы объекта:

class MyClass {
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}

MyClass используется следующим способом:

> let inst = new MyClass();
> inst.prop = 123;
setter: 123
> inst.prop
'getter'

Вычисляемые имена методов


Вы можете определить имя метода с помощью выражения, если поместите его
в квадратные скобки. Например, следующие определения класса Foo
эквивалентны:

class Foo() {
myMethod() {}
}

class Foo() {
['my'+'Method']() {}
}

const m = 'myMethod';
class Foo() {
[m]() {}
}


Некоторые специальные методы в ECMAScript 6 имеют ключи, которые являются
символами 3.
Механизм вычисляемых имен методов позволяют вам определять такие методы.
Например, если объект имеет метод с ключом Symbol.iterator, это —
итератор 4. Это означает,
что его содержимое может быть итерировано циклом for-of или другими механизмами языка.

class IterableClass {
[Symbol.iterator]() {
•••
}
}

Генераторы


Если вы определите метод с «*» в начале, то получите _метод генератор_ 4.
Между прочим, генератор полезен для определения метода, ключом которого
является Symbol.iterator. Следующий код демонстрирует, как это работает:

class IterableArguments {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}

for (let x of new IterableArguments('hello', 'world')) {
console.log(x);
}

// Вывод:
// hello
// world

2.3 Классы наследники


Ключевое слово extends позволяет создать класс-наследник существующего
конструктора (который возможно был определен с помощью класса):

class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}

class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // (A)
this.color = color;
}
toString() {
return super.toString() + ' in ' + this.color; // (B)
}
}


Этот класс используется как и ожидалось:

> let cp = new ColorPoint(25, 8, 'green');
> cp.toString()
'(25, 8) in green'

> cp instanceof ColorPoint
true
> cp instanceof Point
true


В данном случае мы имеем два вида классов:

  • Point — это базовый класс, потому что он не имеет выражения extends.
  • ColorPointпроизводный класс.


Есть два способа использовать ключевое слово super:

  • Конструктор класса (псевдо-метод «constructor» в теле класса),
    использует его как вызов функции (_super(•••)_), для того, чтобы
    вызвать базовый конструктор (строка A).
  • Определения методов (в объектах, заданных через литерал, или классах,
    статических или нет), используют это для вызова свойства (_super.prop_),
    или вызова метода (_super.method(•••)_), для ссылки на свойства базового
    класса (строка B).

Прототип класса наследника является базовым классом


Прототип класса наследника является базовым классом в ECMAScript 6:

> Object.getPrototypeOf(ColorPoint) === Point
true

Это означает, что статические свойства наследуются:

class Foo {
static classMethod() {
return 'hello';
}
}

class Bar extends Foo {
}
Bar.classMethod(); // 'hello'


Можно вызывать статические методы базового класса:

class Foo {
static classMethod() {
return 'hello';
}
}

class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod(); // 'hello, too'

Вызов базового конструктора


В классе-наследнике нужно вызвать super() до того, как будете обращаться к
свойствам через this:

class Foo {}

class Bar extends Foo {
constructor(num) {
let tmp = num * 2; // OK
this.num = num; // ReferenceError
super();
this.num = num; // OK
}
}


Пропустив вызов super() в производном классе, вы получите ошибку:

class Foo {}

class Bar extends Foo {
constructor() {
}
}

let bar = new Bar(); // ReferenceError

Переопределение результата конструктора


Так же, как в ES5, можно переопределить результат конструктора, явно
возвращая объект:

class Foo {
constructor() {
return Object.create(null);
}
}
console.log(new Foo() instanceof Foo); // false

Если вы так сделаете, то не имеет значения, инициализирован ли this или нет.
Другими словами: вы не обязаны вызывать super() в производном конструкторе,
если переопределите результат таким образом.

Конструкторы по умолчанию для классов


Если не указать constructor для базового класса, тогда используется
следующая конструкция:

constructor() {}

Для дочерних классов, используется конструктор по умолчанию:

constructor(...args) {
super(...args);
}

Наследования встроенных конструкторов


В ECMAScript 6 наконец-то можно наследоваться от всех встроенных
конструкторов (обходные пути в ES5, но здесь накладываются
значительные ограничения).

Например, теперь вы можете создавать свои собственные классы исключений
(которые будут наследовать такие особенности, как стек вызовов для
большинства браузерных движков):

class MyError extends Error {
}
throw new MyError('Something happened!');

Вы также можете наследоваться от Array, экземпляры которого правильно
работают с length:

class MyArray extends Array {
constructor(len) {
super(len);
}
}

// Экземпляры класса `MyArray` работают так же как обычный массив:
let myArr = new MyArray(0);
console.log(myArr.length); // 0
myArr[0] = 'foo';
console.log(myArr.length); // 1


Заметьте, что наследование от встроенных конструкторов — это то, что движок
должен поддерживать изначально, вы не сможете получить эту функциональность
с помощью транспайлеров.

3. Детали классов


То, что мы до сих пор рассматривали, является основой классов. Если вам
интересно узнать подробнее про механизм классов, то вам нужно читать дальше.
Давайте начнем с синтаксиса классов. Ниже приводится немного модифицированная
верcия синтаксиса, предложенного в Секции A.4 спецификации ECMAScript 6.

ClassDeclaration:
"class" BindingIdentifier ClassTail
ClassExpression:
"class" BindingIdentifier? ClassTail

ClassTail:
ClassHeritage? "{" ClassBody? "}"
ClassHeritage:
"extends" AssignmentExpression
ClassBody:
ClassElement+
ClassElement:
MethodDefinition
"static" MethodDefinition
";"

MethodDefinition:
PropName "(" FormalParams ")" "{" FuncBody "}"
"*" PropName "(" FormalParams ")" "{" GeneratorBody "}"
"get" PropName "(" ")" "{" FuncBody "}"
"set" PropName "(" PropSetParams ")" "{" FuncBody "}"

PropertyName:
LiteralPropertyName
ComputedPropertyName
LiteralPropertyName:
IdentifierName /* foo */
StringLiteral /* "foo" */
NumericLiteral /* 123.45, 0xFF */
ComputedPropertyName:
"[" Expression "]"


Два наблюдения:

  • Значение расширения может быть произвольным выражением. Это значит, что
    вы можете написать следующий код:

    class Foo extends combine(MyMixin, MySuperClass) {}

  • Между методами допускается точка с запятой.

3.1 Различные проверки

  • Проверки ошибок: имя класса не может быть eval или arguments;
    одинаковые имена классов не допускаются; название constructor может
    использоваться только для обычных методов, для геттеров, сеттеров и
    генераторов — не допускается.
  • Классы не могут быть вызываемой функцией. Иначе они бросают исключение
    TypeException
  • Методы прототипа не могут использоваться как конструкторы:
    class C {
    m() {}
    }
    new C.prototype.m(); // TypeError


3.2 Атрибуты свойств


Определения класса создают (изменяемые) разрешаемые связи. Для данного
класса Foo:

  • Статические методы Foo.* доступны для записи и настройки, но
    не для перечисления. Доступность для записи позволяет динамически вносить
    изменения в них.
  • Конструктор и объект в свойстве prototype имеют неизменяемые ссылки:
    • Foo.prototype не доступен для записи, перечисления и настройки.
    • Foo.prototype.constructor не доступен для записи, перечисления
      и настройки.


  • Прототипные методы Foo.prototype.* доступны для записи и настройки,
    но не для перечисления.


Заметьте, что определения методов в литералах объекта создают перечисляемые
свойства.

4. Детали наследования классов


В ECMAScript 6, наследование классов выглядит следующим образом:

class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
•••
}

class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
•••
}

let cp = new ColorPoint(25, 8, 'green');


Этот код создает следующие объекты:

Цепочки прототипов

Следующий подраздел рассматривает цепочки прототипов (в две колонки),
далее рассматривает как cp выделяется в памяти и инициализируется.

4.1 Цепочки прототипов


На диаграмме видно, что есть 2 цепочки прототипов (объекты связаны через
отношения [[Prototype]], которые наследуются):

  • Левая колонка: классы (функции). Прототипом производного класса является
    расширяемый класс. Прототип базового класса является Function.prototype,
    которая также есть прототип функции:

    > const getProto = Object.getPrototypeOf.bind(Object);

    > getProto(Point) === Function.prototype
    true
    > getProto(function () {}) === Function.prototype
    true


  • Правая колонка: цепочки прототипов экземпляров. Цель класса —
    установить эту цепочку прототипов. Цепочка прототипов заканчивается на
    Object.prototype (чей прототип является null), который также прототип
    объектов, созданных через литералы объекта:

    > const getProto = Object.getPrototypeOf.bind(Object);

    > getProto(Point.prototype) === Object.prototype
    true
    > getProto({}) === Object.prototype
    true




Из цепочки прототипов в левой колонке следует, что статические свойства
наследуются.

4.2 Выделение памяти и инициализация экземпляров объектов


Потоки данных между конструкторами классов отличаются от канонического пути
наследования в ES5. Под капотом это выглядит примерно так:

// Экземпляр находится тут
function Point(x, y) {
// Выполняется до выполнения кода конструктора:
this = Object.create(new.target.prototype);

this.x = x;
this.y = y;
}
•••

function ColorPoint(x, y, color) {
// Выполняется до выполнения кода конструктора:
this = uninitialized;

this = Reflect.construct(Point, [x, y], new.target); // (A)
// super(x, y);
this.color = color;
}
Object.setPrototypeOf(ColorPoint, Point);
•••

let cp = Reflect.construct( // (B)
ColorPoint, [25, 8, 'green'],
ColorPoint);
// let cp = new ColorPoint(25, 8, 'green');


В ES5 и ES6 экземпляр объекта создается в разных местах:

  • В ES6 он создается базовым конструктором, последним в цепочке вызовов
    конструкторов.
  • В ES5 он создается оператором new, первым в цепочке вызовов
    конструкторов.


Предыдущий код использует две новые возможности ES6:

  • new.target является неявным параметром, который имеют все функции. Это
    вызов конструктора, где this является вызовом метода.

    • Если конструктор напрямую вызывается через new, его значение это
      и есть этот конструктор (строка B).
    • Если конструктор был вызван через super(), его значение это
      new.target конструктора, который был вызван (строка A).
    • Вызвав функцию обычным способом, значение будет undefined. Это
      значит, что вы можете использовать new.target чтобы определить, была
      ли функция функцией вызова или вызовом конструктора (через new).
    • Внутри стрелочной функции new.target ссылается на new.target
      окружающей нестрелочной функции.


  • Reflect.construct() 5 позволяет вызвать конструктор при задании
    new.target в качестве последнего параметра.


Преимуществом этой реализации наследования является то, что это позволяет
писать нормальный код для наследования встроенных конструкторов (такие как
Error и Array). Последний раздел объясняет, почему иной подход был
необходим.

Проверки безопасности

  • this инициализируется в производных конструкторах, а это значит, что если
    к this обращаются до того как вызвали super(), то будет
    брошено исключение.
  • Вызов super() после инициализации this приведет к ReferenceError.
    Это защита от двойного вызова super().
  • Если конструктор возвращается неявно (без return), тогда результат будет
    this. Если this инициализирован, тогда бросится исключение
    ReferenceError . Это защита от невызова super().
  • Если конструктор явно возвращает не объект (включая undefined и null),
    результатом будет this (это поведение оставляет совместимость с ES5
    и ранее). Если this инициализирован, тогда бросится исключение
    TypeError.
  • Если конструктор явно возвращает объект, тогда он и будет результатом.
    Тогда не имеет значение инициализирован this или нет.

Выражение «extends»


Давайте рассмотрим, как выражение extends влияет на работу класса
(Секция. 14.5.14 спецификации).

Значение extends должно быть «конструктивно» (вызываться через new)
хотя null тоже поддерживается.

class C {
}

  • Тип конструктора: базовый
  • Прототип C: Function.prototype (как обычная функция)
  • Прототип C.prototype: Object.prototype (который также прототип
    объекта, созданный через литералы объекта)


 

class C extends B {
}

  • Тип конструктора: наследник
  • Прототип C: B
  • Прототип C.prototype: B.prototype


 

class C extends Object {
}

  • Тип конструктора: наследник
  • Прототип C: Object
  • Прототип C.prototype: Object.prototype


Обратите внимание на следующее различие с первым случаем:
Если нет extends, класс является базовым и выделяет в памяти экземпляры.
Если класс расширяет Object, это производный класс объекта и выделяет
экземпляры. Полученные экземпляры (в том числе их цепочки прототипов)
одинаковы, только получены разными способами.

class C extends null {
}

  • Тип конструктора: наследник
  • Прототип C: Function.prototype
  • Прототип C.prototype: null


Такой класс бесполезный: вызов через new приведет к ошибке, потому
что конструктор по умолчанию сделает вызов базового конструктора и
Function.prototype (базовый конструктор) не может быть конструктором вызова.
Единственный способ избежать ошибки — это добавить конструктор, который
возвратит объект.

4.3 Почему мы не можем наследовать встроенные конструкторы в ЕS5?


В ECMAScript 5, большинство встроенных конструкторов не могут быть
унаследованы (несколько обходных путей).

Чтобы понять почему, давайте используем канонический ES5 шаблон наследования
Array. Как мы вскоре узнаем, это не работает.

function MyArray(len) {
Array.call(this, len); // (A)
}
MyArray.prototype = Object.create(Array.prototype);

К сожалению, если мы создадим MyArray, мы поймем, что он не работает
должным образом: экземпляр свойства length не изменится в ответ на наше
добавление элементов в массив:

> var myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
0

Есть два препятствия, которые мешают myArr быть правильным массивом.

Первое препятствие: инициализация. this, который вы передаете в
конструктор Array (в строке A) полностью игнорируется. Это значит, что вы
не можете использовать Array чтобы настроить экземпляр, который создал
MyArray.

> var a = [];
> var b = Array.call(a, 3);
> a !== b // a игнорируется, b — новый объект
true
> b.length // определилось верно
3
> a.length // неизменно
0

Второе препятствие: выделение памяти. Экземпляры объектов, созданные через
Array являются экзотичными (термин, используемый в спецификации ECMAScript
для объектов, которые имеют особенности, которые нормальные объекты не имеют):
их свойства length отслеживают и влияют на управление элементами массива.
В общем, экзотические объекты могут быть созданы с нуля, но вы не можете
преобразовать существующий обычный объект в экзотический. К сожалению,
это то, что делает Array, когда вызывается на строке A:
Он должен был превратить обычный объект, созданный из MyArray в
экзотический объект массива.

Решение: ES6 наследование


В ECMAScript 6, наследование Array выглядит следующим образом:

class MyArray extends Array {
constructor(len) {
super(len);
}
}

Это работает (но это не то, что ES6 транспайлеры могут поддерживать, это
зависит от того, поддерживает ли движок JavaScript это изначально):

> let myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
1

Сейчас рассмотрим, как подход к наследованию в ES6, позволяет обойти
препятствия:

  • Выделение памяти происходит в базовом конструкторе. Это значит, что
    Array может выделить в памяти экзотический объект. В то время как
    большая часть нового подхода связана с тем, как полученные конструкторы
    ведут себя, этот шаг требует, чтобы базовый конструктор понимал
    new.target и делал new.target.prototype прототипом выделенного
    экземпляра.
  • Инициализация также происходит в базовом конструкторе, конструктор
    класса-наследника получает инициализированный объект и работает с ним,
    вместо того, чтобы создавать собственный объект и отдавать его
    конструктору базового класса, чтобы тот его создавал.

4.4 Ссылка на базовые свойства в методах


Следующий ES6 код вызывает базовый метод со строкой «B» в качестве аргумента.

class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() { // (A)
return '(' + this.x + ', ' + this.y + ')';
}
}

class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return super.toString() // (B)
+ ' in ' + this.color;
}
}

let cp = new ColorPoint(25, 8, 'green');
console.log(cp.toString()); // (25, 8) in green


Чтобы понять как работает базовые вызовы, давайте взглянем на диаграмму
объекта cp:

Диаграмма объекта

ColorPoint.prototype.toString делает вызов метода базового класса (строка B)
(начиная со строки A), который переопределен. Давайте вызовем объект, в
котором хранится этот метод, домашний объект. Например,
ColorPoint.prototype — это домашний объект для
ColorPoint.prototype.toString().

Вызов базового класса на строке B состоит из трёх этапов:

  1. Начинается поиск в прототипе домашнего объекта текущего метода.
  2. Поиск метода с названием toString. Этот метод должен быть найден в
    объекте, где начался поиск, или позже по цепочке прототипов.
  3. Вызвать этот метод с текущим this. Причина почему это происходит:
    метод вызываемый как базовый должен иметь возможность доступа к тем же
    свойствам экземпляра (в нашем примере, к свойствам cp).


Обратите внимание, что даже если вы только получаете или устанавливаете
свойство (без вызова метода), вам все равно придется учитывать this
в шаге 3, потому что свойство может быть реализовано через геттер или сеттер.

Давайте реализуем эти шаги в трех различных, но эквивалентных способах:

// Вариант 1: вызов супер-метода в ES5
var result = Point.prototype.toString.call(this) // шаги 1,2,3

// Вариант 2: ES5, после рефакторинга
var superObject = Point.prototype; // шаг 1
var superMethod = superObject.toString; // шаг 2
var result = superMethod.call(this) // шаг 3

// Вариант 3: ES6
var homeObject = ColorPoint.prototype;
var superObject = Object.getPrototypeOf(homeObject); // шаг 1
var superMethod = superObject.toString; // шаг 2
var result = superMethod.call(this) // шаг 3


Способ 3 показывает, как в ECMAScript 6 обрабатываются вызовы базового класса.
Этот подход поддерживается двумя внутренними привязками, которые имеют
состояния функций (состояние обеспечивает хранилище, так называемые
привязки, для переменных окружения):

  • [[thisValue]]: Эта внутренняя привязка также есть и в ECMAScript 5 и
    хранит значение переменной this.
  • [[HomeObject]]: Относится к домашнему объекту состояния функции.
    Заполняется через внутреннее свойство [[HomeObject]] которое имеют все
    функции, использовавшие super. И привязка и свойство являются новыми
    в ECMAScript 6.


Определение метода в литерале класса, который использует super, теперь имеет
особенность: это значение все еще функция, но имеет внутреннее свойство
[[HomeObject]]. Это свойство устанавливается определением метода и не может
быть изменено в JavaScript. Таким образом, вы не можете перенести этот метод
в другой объект.

Использование super не допускается для обращения к свойству в определениях
функций, выражениях функций и генераторах.

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

5. Пояснение вызовов конструктора через JavaScript код


Код JavaScript в этом разделе достаточно упрощен по сравнению с тем, как
спецификация описывает вызовы конструктора и вызовы базового конструктора.
Это может быть интересно для вас, если вы предпочитаете объяснения кода
человеческим языком. Прежде чем мы углубимся в функциональность, мы должны
понимать несколько других механизмов.

5.1 Внутренние переменные и свойства


Спецификация описывает внутренние переменные и свойства в двойных скобках
([[Foo]]). В коде я использую двойные подчеркивания вместо этого
(__Foo__).

Внутренние переменные используемые в коде:

  • [[NewTarget]]: Операнд оператора new, который запускает текущий вызов
    конструктора (передается, если [[Construct]] вызывается рекурсивно через
    super()).
  • [[thisValue]]: Хранит значение this.
  • [[FunctionObject]]: Ссылается на функцию, которая в настоящее
    время выполняется.


Внутренние свойства используемые в коде:

  • [[Construct]]: Все функции конструктора (включая также созданные
    классом) имеют этот собственный (не наследуемый) метод. Он реализует
    вызов конструктора и вызывается через new.
  • [[ConstructorKind]]: Свойство функций конструктора значение которого
    либо «base» либо «derived».

5.2 Состояния


Состояния обеспечивают хранилище для переменных, одно состояние на
окружение. Состояния управляются как стек. Состояние на вершине стека
считается активным. Следующий код демонстрирует, как состояния обрабатываются.

/**
* Окружение функций — особенное, в нем на несколько
* внутренних переменных больше, чем в других окружениях.
* (Окружение тут показывается)
*/
class FunctionEnvironment extends Environment {
constructor(Func) {
// [[FunctionObject]] это специфическая для функций
// внутренняя переменная
this.__FunctionObject__ = Func;
}
}

/**
* Добавляем окружение в стек
*/
function PushEnvironment(env) { ••• }

/**
* Удаляем самое верхнее окружение из стека
*/
function PopEnvironment() { ••• }

/**
* Находим самое верхнее окружение в стеке
*/
function GetThisEnvironment() { ••• }

5.3 Вызов конструктора


Давайте начнем с основ (ES6 спецификация, Секция. 9.2.3), где вызовы
конструктора обрабатываются для функций:

/**
* У всех функций с конструктором есть этот метод,
* он вызывается оператором `new`
*/
AnyFunction.__Construct__ = function (args, newTarget) {
let Constr = this;
let kind = Constr.__ConstructorKind__;

let env = new FunctionEnvironment(Constr);
env.__NewTarget__ = newTarget;
if (kind === 'base') {
env.__thisValue__ = Object.create(newTarget.prototype);
} else {
// Пока «this» не инициализировано, попытка установить или считать её
// приведет к выбрасыванию «ReferenceError»
env.__thisValue__ = uninitialized;
}

PushEnvironment(env);
let result = Constr(...args);
PopEnvironment();

// Давайте представим, что есть способ сказать, был ли «result»
// возвращен в явном виде или нет
if (WasExplicitlyReturned(result)) {
if (isObject(result)) {
return result;
}
// Явно возвращаем примитив
if (kind === 'base') {
// Конструкторы должны обладать обратной совместимостью
return env.__thisValue__; // всегда инициализирована!
}
throw new TypeError();
}
// Implicit return
if (env.__thisValue__ === uninitialized) {
throw new ReferenceError();
}
return env.__thisValue__;
}

5.4 Вызов базового конструктора


Вызов базового конструктора обрабатывается следующим образом
(ES6 спецификация, Секция. 12.3.5.1).

/**
* Обработка вызовов супер-конструктора
*/
function super(...args) {
let env = GetThisEnvironment();
let newTarget = env.__NewTarget__;
let activeFunc = env.__FunctionObject__;
let superConstructor = Object.getPrototypeOf(activeFunc);

env.__thisValue__ = superConstructor
.__Construct__(args, newTarget);
}

6. Шаблон разновидностей


Еще один механизм встроенных конструкторов был расширен в ECMAScript 6:
если метод, такой как Array.prototype.map(), возвращает экземпляр, то какой
конструктор следует использовать для создания этого экземпляра? По умолчанию,
используется тот же конструктор, который создал this, но некоторые
наследники могут оставаться прямым экземпляром Array. ES6 позволяет
классам-наследникам переопределить значение по умолчанию с помощью так
называемого шаблона разновидности:

  • При создании нового экземпляра Array, методы, такие как map()
    используют конструктор, хранящийся в this.constructor[Symbol.species].
  • Если конструктор наследника Array ничего не делает, он наследует
    Array[Symbol.species]. Это свойство является геттером, который
    возвращает this.


Вы можете изменить настройки по умолчанию, с помощью статического геттера
(строка A):

class MyArray1 extends Array {
}
let result1 = new MyArray1().map(x => x);
console.log(result1 instanceof MyArray1); // true

class MyArray2 extends Array {
static get [Symbol.species]() { // (A)
return Array;
}
}
let result2 = new MyArray2().map(x => x);
console.log(result2 instanceof MyArray2); // false


Альтернативой является использование Object.defineProperty() (вы не можете
использовать присвоение, т.к. вызываете сеттер, который не существует):

Object.defineProperty(
MyArray2, Symbol.species, {
value: Array
});

Следующие геттеры возвращают this, это означает, что такие методы
как Array.prototype.map(), используют конструктор, который создал текущий
экземпляр их результатов.

  • Array.get [Symbol.species]()
  • ArrayBuffer.get [Symbol.species]()
  • Map.get [Symbol.species]()
  • Promise.get [Symbol.species]()
  • RegExp.get [Symbol.species]()
  • Set.get [Symbol.species]()
  • %TypedArray%.get [Symbol.species]()

7. Заключение

7.1 Специализация функций


Существует интересная тенденция в ECMAScript 6: ранее единственный вид
функции был на трех ролях: функция, метод и конструктор.
В ES6, есть еще специализация:

  • Стрелочные функции специализируются на функциях обратного вызова, где их
    использует this в окружающем методе, или конструктор как преимущество.
    Без this они не имеют смысла как методы и они бросают исключение,
    если вызываются через new.
  • Определения метода позволяют использовать super, определив свойство
    [[HomeObject]]. Функции, которые они производят, не могут быть
    вызываемыми конструкторами.
  • Определения классов являются единственным способом создания производных
    конструкторов (включающий в ES6 наследование, которое работает на
    встроенных конструкторах). Определения классов создают функции, которые
    могут быть только вызываемыми конструкторами.

7.2 Будущее классов


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

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

7.3 Нужны ли классы JavaScript'у?


Классы являются спорными в сообществе JavaScript. С одной стороны, люди
которые пришли из языков, основанных на классах — счастливы, что им больше не
придется иметь дело с необычными механизмами наследования в JavaScript.
С другой стороны, существует множество JavaScript программистов, которые
утверждают, что в JavaScript прототипное наследование проще, чем наследование
с помощью конструкторов 6.

Классы ES6 обеспечивают несколько очевидных преимуществ:

  • Они обратно совместимы с большей частью текущего кода.
  • По сравнению с конструкторами и наследованием конструкторов, классы
    реализуют это проще для начинающих.
  • Наследование поддерживается в языке.
  • Встроенные конструкторы наследуемы.
  • Теперь не нужны библиотеки, реализующие наследование; код станет более
    переносимым между фреймворками.
  • Они обеспечивают основу для расширенной функциональности в будущем
    (миксины и т.п).
  • Они помогают инструментам, которые статически анализируют код (IDE,
    проверки типов, проверки стилей, и т.д.).


Я закончил эту статью с классами и я рад, что они есть в ES6. Я бы
предпочел, чтобы они были прототипными (на основе конструктора объектов 6,
а не конструктора функций), но я также понимаю, что обратная совместимость
является важной.


Для дополнительного чтения


Обратите внимание на №1 — он играл роль значимого источника информации при написании этой статьи.

1. Реформа создания экземпляров: в последний раз, слайды Аллена Вирфс-Брока (Allen Wirfs-Brock)

2. Анализ ES6: Обновление до новой версии JavaScript, книга Акселя Роушмайера (Axel Rauschmayer)

3. Символы в ECMAScript 6

4. Итераторы и генераторы в ECMAScript 6

5. Метапрограммирование с прокси в ECMAScript 6

6. Прототипы и классы – введение в наследование на JavaScript


Source: http://frontender.info/

Комментарии

comments powered by Disqus