[Update 2015-02-15] Newer version of this blog post: “
Classes in ECMAScript 6 (final semantics)”
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:
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:
- Constructors are created by classes.
- Non-method functions are created by arrow functions [4].
- 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
- ECMAScript: ES.next versus ES 6 versus ES Harmony
- A closer look at super-references in JavaScript and ECMAScript.next
- Asynchronous programming and continuation-passing style in JavaScript
- ECMAScript.next: arrow functions and method definitions
- Translating CoffeeScript classes to JavaScript
- The ECMAScript 6 schedule changes
- Prototypes as classes – an introduction to JavaScript inheritance
- Lightweight JavaScript inheritance APIs
- Subclassing builtins in ECMAScript 6