2012-07-29

ECMAScript 6: classes

During the July 2012 meeting of TC39 [1], classes have been accepted for ECMAScript 6, the upcoming version of the JavaScript language standard. This blog post explains how those classes work. It is based on Allen Wirfs-Brock’s annotated slides.

Overview

An ECMAScript 6 class is syntactic sugar for a constructor – a function, to be invoked via new. That is, class declarations and class expressions are simply more convenient syntax for writing functions. The following is an example of a class Person having a subclass Employee:
    // Superclass
    class Person {
        constructor(name) {
            this.name = name;
        }
        describe() {
            return "Person called "+this.name;
        }
    }
    // Subclass
    class Employee extends Person {
        constructor(name, title) {
            super.constructor(name);
            this.title = title;
        }
        describe() {
            return super.describe() + " (" + this.title + ")";
        }
    }
This is how you use these classes:
    > let jane = new Employee("Jane", "CTO");

    > jane instanceof Person
    true
    > jane instanceof Employee
    true
    > jane.describe()
    Person called Jane (CTO)
The classes are roughly equivalent to the following code (super has no simple equivalent [2]):
    // Superclass
    function Person(name) {
        this.name = name;
    }
    Person.prototype.describe = function () {
        return "Person called "+this.name;
    };

    // Subclass
    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 + ")";
    };
As you can see, the classes are effectively turned inside out: Given a class C, its method constructor becomes a function C and all other methods are added to C.prototype.

Details

Grammar

    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 "}"
Observations:
  • Similar to functions, there are both class declarations and class expressions. Also similarly, the identifier of a class expression is only visible within the expression.
  • The value to be extended can be produced by an arbitrary expression. Which means that you’ll be able to write code such as the following:
        class Foo extends combine(MyMixin, MySuperClass) {}
    
  • A class body can only contain methods, no data properties. Prototypes having data properties is generally considered an anti-pattern, so this just enforces a best practice.
  • Semicolons are allowed between methods.
  • A method is a generator [3] if its name is prefixed with an asterisk (*). Such a method is translated to a property whose value is a generator function.

Various checks and features

  • Error checks: the class name cannot be eval or arguments; duplicate class element names are not allowed; the name constructor can only be used for a normal method, not for a getter, a setter or a generator method.
  • Class initialization is not hoisted:
        new Bar(); // runtime error
        class Bar {}
    
    That is usually not a problem, because you can still refer to the class everywhere. You just have to wait until the class definition has been evaluated, before you can use it:
        function useBar() {
            new Bar();
        }
        useBar(); // error
        class Bar {}
        useBar(); // OK
    
  • Instance methods cannot be used as constructors:
        class C {
            m() {}
        }
        new C.prototype.m(); // TypeError
    
    That shows that we are heading towards specialization: Whereas previously, the roles constructor, method and non-method function were all taken on by functions, ECMAScript 6 will have a dedicated syntactic construct for each role:
    1. Constructors are created by classes.
    2. Non-method functions are created by arrow functions [4].
    3. Methods are created by method definitions (inside classes and object literals).
    Each of these syntactic constructs produces functions, but ones that differ slightly from those that are produced by function () {}: #1 functions have properties with different attributes (see below, prototype is frozen, etc.). #2 functions have a bound this. #3 functions have additional data to enable the use of super [2].
  • If there is no method constructor then the default is:
        constructor(...args) {
            super(...args);
        }
    
  • If you call a class as a function (without specifying this) then this will be undefined. That’s also how functions as constructors work right now (in strict mode).

Extending

Rules for extending:
  • Don’t extend: class Foo {}
    • The prototype of Foo is Function.prototype (as for all functions).
    • The prototype of Foo.prototype is Object.prototype.
    That is the same as for functions. Note that the above is not equivalent to class Foo extends Object, for the only reason that you normally want to avoid Foo inheriting methods such as Object.create().
  • Extend null: class Foo extends null {}
    • The prototype of Foo is Function.prototype.
    • The prototype of Foo.prototype is null.
    Prevent the methods of Object.prototype from being available to instances of Foo.
  • Extend a constructor: class Foo extends SomeClass
    • The prototype of Foo is SomeClass.
    • The prototype of Foo.prototype is SomeClass.prototype.
    Therefore, class methods are inherited, too. For example: If there is a method SomeClass.bar() then that method is also available via Foo.bar(). That is how CoffeeScript implements inheritance [5].
  • Extend a non-constructor: class Foo extends someObject
    • The prototype of Foo is Function.prototype
    • The prototype of Foo.prototype is someObject
Error checks: The extends value must either be an object or null. If it is a constructor then that constructor’s prototype must be either an object or null.

Mutability and property attributes

Class declarations create (mutable) let bindings. For a given class Foo:
  • Foo.prototype is neither writeable, nor configurable, nor enumerable.
  • Foo.constructor is writeable and configurable, but not enumerable.
  • Foo.prototype.* methods are writable and configurable, but not enumerable. Making them writable allows for dynamic patching. Getters and setters are enumerable (because they are similar to data properties).
This is exactly like current built-in constructors are set up.

Conclusion

When will you be able to use classes?

The ECMAScript 6 will be standardized by mid 2015 [6]. You can expect many pieces to be prototyped in JavaScript engines well before then. The new standard becoming dominant will take a few years, just as you only now can slowly rely on ECMAScript 5 being there. Until then, there will be compilers (such as Traceur) to compile ECMAScript 6 to, say, ECMAScript 3. That is, on legacy JavaScript engines, ECMAScript 6 might become similar to CoffeeScript: A more powerful language (than supported by the engine) that compiles to JavaScript.

Advantages of classes

I used to oppose classes and prefer object exemplars [7]. But given the constraint of not breaking legacy code, I’m now glad that they got accepted for ECMAScript 6. The three main benefits of classes are:
  • Classes will make it easier for beginners to get started with JavaScript.
  • Classes will make subclassing easier for both beginners and experienced JavaScript programmers. Helper APIs such as [8], won’t be needed anymore.
  • Classes will allow you to subclass built-in classes/constructors such as Error and Array [9].
  • Classes will help make code more portable between frameworks. Currently, many frameworks implement their own inheritance API, which makes it more difficult to reuse code.
Otherwise, nothing much changes, we’ll still have the same old constructor functions under the hood.

References

  1. ECMAScript: ES.next versus ES 6 versus ES Harmony
  2. A closer look at super-references in JavaScript and ECMAScript.next
  3. Asynchronous programming and continuation-passing style in JavaScript
  4. ECMAScript.next: arrow functions and method definitions
  5. Translating CoffeeScript classes to JavaScript
  6. The ECMAScript 6 schedule changes
  7. Prototypes as classes – an introduction to JavaScript inheritance
  8. Lightweight JavaScript inheritance APIs
  9. Subclassing builtins in ECMAScript 6

No comments: