Статьи

ECMAScript.next: классы


В июле 2012 года на совещании ТК39
[1] были приняты классы для ECMAScript.next, будущей версии стандарта языка JavaScript. В этом блоге объясняется, как работают эти классы. Он основан на аннотированных слайдах Аллена Вирфса-Брока
.

обзор

Класс ECMAScript.next является синтаксическим сахаром для конструктора — функции, которая вызывается через new. То есть объявления классов и выражения классов — это просто более удобный синтаксис для написания функций. Ниже приведен пример класса Person с подклассом Employee:

    // Supertype
    class Person {
        constructor(name) {
            this.name = name;
        }
        describe() {
            return "Person called "+this.name;
        }
    }
    // Subtype
    class Employee extends Person {
        constructor(name, title) {
            super.constructor(name);
            this.title = title;
        }
        describe() {
            return super.describe() + " (" + this.title + ")";
        }
    }

Вот как вы используете эти классы:

    > let jane = new Employee("Jane", "CTO");

    > jane instanceof Person
    true
    > jane instanceof Employee
    true
    > jane.describe()
    Person called Jane (CTO)

Классы примерно эквивалентны следующему коду (супер не имеет простого эквивалента
[2] ):

    // Supertype
    function Person(name) {
        this.name = name;
    }
    Person.prototype.describe = function () {
        return "Person called "+this.name;
    };

    // Subtype
    function Employee(name, title) {
        Person.call(this, name);
        this.title = title;
    }
    Employee.prototype = Object.create(Person.prototype);
    Employee.prototype.constructor = Employee;
    Employee.prototype.describe = function () {
        return Person.prototype.describe.call(this)
               + " (" + this.title + ")";
    };

подробности

грамматика

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

    ClassTail:
        ClassHeritage? "{" ClassBody? "}"
    ClassHeritage:
        "extends" AssignmentExpression
    ClassBody:
        ClassElement+
    ClassElement:
        MethodDefinition
        ";"
        
    MethodDefinition:
        PropName "(" FormalParamList ")" "{" FuncBody "}"
        "*" PropName "(" FormalParamList ")" "{" FuncBody "}"
        "get" PropName "(" ")" "{" FuncBody "}"
        "set" PropName "(" PropSetParamList ")" "{" FuncBody "}"

Замечания:

  • Подобно функциям, существуют как объявления классов, так и выражения классов. Также аналогично, идентификатор выражения класса виден только внутри выражения.
  • Значение, которое будет расширено, может быть получено произвольным выражением. Это означает, что вы сможете написать такой код:
        class Foo extends combine(MyMixin, MySuperClass) {}
    
  • Тело класса может содержать только методы, без свойств данных. Прототипы, имеющие свойства данных, обычно считаются анти-паттернами, так что это просто на практике.
  • Точки с запятой разрешены между методами.
  • Метод является генератором [3], если перед его именем стоит звездочка (*). Такой метод переводится в свойство, значение которого является функцией генератора.

Различные проверки и особенности

  • Проверка ошибок: имя класса не может быть eval или аргументом; повторяющиеся имена элементов класса не допускаются; конструктор имени может использоваться только для обычного метода, но не для метода получения, установки или генератора.
  • Класс инициализации не поднимается:
        new Bar(); // runtime error
        class Bar {}
    

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

        function useBar() {
            new Bar();
        }
        useBar(); // error
        class Bar {}
        useBar(); // OK
    
  • Методы экземпляра нельзя использовать в качестве конструкторов:
        class C {
            m() {}
        }
        new C.prototype.m(); // TypeError
    

    Это показывает, что мы движемся к специализации: если ранее конструктор ролей, метод и не-методная функция были приняты функциями, то для ECMAScript.next будет выделена синтаксическая конструкция для каждой роли:

    1. Конструкторы создаются классами.
    2. Не-методные функции создаются функциями стрелок [4] .
    3. Методы создаются с помощью определений методов (внутри классов и литералов объектов).

    Каждая из этих синтаксических конструкций создает функции, но те, которые немного отличаются от тех, которые создаются функцией () {}: функции # 1 имеют свойства с различными атрибутами (см. Ниже, прототип заморожен и т. Д.). # 2 функции имеют это ограничение. Функции # 3 имеют дополнительные данные, позволяющие использовать super [2] .

  • Если конструктор метода отсутствует, по умолчанию используется значение:
        constructor(...args) {
            super(...args);
        }
    

простирающийся

Правила продления:

  • Не расширяйте: class Foo {}

    • Прототип Foo — это Function.prototype (как и для всех функций).
    • Прототипом Foo.prototype является Object.prototype.

    Это то же самое, что и для функций. Обратите внимание, что приведенное выше не эквивалентно классу Foo extends Object, по единственной причине, по которой вы обычно хотите избегать наследования методов Foo, таких как Object.create ().

  • Расширить ноль: класс Foo расширяет нуль {}

    • Прототипом Foo является Function.prototype.
    • Прототип Foo.prototype является нулевым.

    Запретить методы Object.prototype быть доступными для экземпляров Foo.

  • Расширяем конструктор: класс Foo расширяет SomeClass

    • Прототип Foo — SomeClass.
    • Прототипом Foo.prototype является SomeClass.prototype.

    Поэтому методы класса тоже наследуются. Например: если есть метод SomeClass.bar (), то этот метод также доступен через Foo.bar (). Именно так CoffeeScript реализует наследование [5] .

  • Расширяем не-конструктор: класс Foo расширяет someObject

    • Прототип Foo — это Function.prototype
    • Прототипом Foo.prototype является someObject

Проверка ошибок: значение extends должно быть либо объектом, либо нулем. Если это конструктор, то прототип этого конструктора должен быть либо объектом, либо нулем.

Изменчивость и конфигурируемость

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

  • Foo.prototype не является ни записываемым, ни конфигурируемым, ни перечисляемым.
  • Foo.constructor доступен для записи и настройки, но не для перечисления.
  • Методы Foo.prototype. * Доступны для записи и настройки, но не для перечисления. Создание их для записи позволяет динамическое исправление.

Это точно так же, как текущие встроенные конструкторы.

Вывод

Раньше я выступал против классов и предпочитал образцы объектов
[6] . Но учитывая ограничение не нарушать унаследованный код, я теперь рад, что они были приняты для ECMAScript.next. Три основных преимущества занятий:

  • Классы помогут новичкам освоиться с JavaScript.
  • Классы облегчат подтипирование для опытных программистов JavaScript. Нет больше вспомогательных API, таких как [7] .
  • Классы помогут сделать код более переносимым между фреймворками. В настоящее время многие фреймворки реализуют собственный API наследования, что затрудняет повторное использование кода.

В остальном ничего особенного не меняется, у нас все те же старые функции под капотом.

Рекомендации

  1. ECMAScript: ES.next против ES 6 против ES Harmony
  2. Пристальный взгляд на супер-ссылки в JavaScript и ECMAScript.next
  3. Асинхронное программирование и стиль прохождения продолжения в JavaScript
  4. ECMAScript.next: функции стрелок и определения методов
  5. Перевод классов CoffeeScript в JavaScript
  6. Прототипы как классы — введение в наследование JavaScript
  7. Облегченные API наследования JavaScript