Классы в ECMAScript 6

Недавно, 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

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

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

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

Прототип класса наследника является базовым классом в 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 "]"

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

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

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

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

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

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]], которые наследуются):

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

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:

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

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

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

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

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

class C {
}

 

class C extends B {
}

 

class C extends Object {
}

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

class C extends 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, позволяет обойти препятствия:

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 обрабатываются вызовы базового класса. Этот подход поддерживается двумя внутренними привязками, которые имеют состояния функций (состояние обеспечивает хранилище, так называемые привязки, для переменных окружения):

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

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

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

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

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

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

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

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

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

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 позволяет классам-наследникам переопределить значение по умолчанию с помощью так называемого шаблона разновидности:

Вы можете изменить настройки по умолчанию, с помощью статического геттера (строка 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(), используют конструктор, который создал текущий экземпляр их результатов.

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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