new
A class and a subclass:
Using the classes:
Under the hood, ES6 classes are not something that is radically new: They mainly provide more convenient syntax to create old-school constructor functions. You can see that if you use typeof
:
A class is defined like this in ECMAScript 6:
You use this class just like an ES5 constructor function:
In fact, the result of a class definition is a function:
However, you can only invoke a class via new
, not via a function call (the rationale behind this is explained later):
There is no separating punctuation between the members of a class definition. For example, the members of an object literal are separated by commas, which are illegal at the top levels of class definitions. Semicolons are allowed, but ignored:
Semicolons are allowed in preparation for future syntax which may include semicolon-terminated members. Commas are forbidden to emphasize that class definitions are different from object literals.
Function declarations are hoisted: When entering a scope, the functions that are declared in it are immediately available – independently of where the declarations happen. That means that you can call a function that is declared later:
In contrast, class declarations are not hoisted. Therefore, a class only exists after execution reached its definition and it was evaluated. Accessing it beforehand leads to a ReferenceError
:
The reason for this limitation is that classes can have an extends
clause whose value is an arbitrary expression. That expression must be evaluated in the proper “location”, its evaluation can’t be hoisted.
Not having hoisting is less limiting than you may think. For example, a function that comes before a class declaration can still refer to that class, but you have to wait until the class declaration has been evaluated before you can call the function.
Similarly to functions, there are two kinds of class definitions, two ways to define a class: class declarations and class expressions.
Similarly to function expressions, class expressions can be anonymous:
Also similarly to function expressions, class expressions can have names that are only visible inside them:
The last two lines demonstrate that Me
does not become a variable outside of the class, but can be used inside it.
A class body can only contain methods, but not data properties. Prototypes having data properties is generally considered an anti-pattern, so this just enforces a best practice.
constructor
, static methods, prototype methods Let’s examine three kinds of methods that you often find in class definitions.
The object diagram for this class declaration looks as follows. Tip for understanding it: [[Prototype]]
is an inheritance relationship between objects, while prototype
is a normal property whose value is an object. The property prototype
is only special w.r.t. the new
operator using its value as the prototype for instances it creates.
First, the pseudo-method constructor
. This method is special, as it defines the function that represents the class:
It is sometimes called a class constructor
. It has features that normal constructor functions don’t have (mainly the ability to constructor-call its superconstructor via super()
, which is explained later).
Second, static methods. Static properties (or class properties) are properties of Foo
itself. If you prefix a method definition with static
, you create a class method:
Third, prototype methods. The prototype properties of Foo
are the properties of Foo.prototype
. They are usually methods and inherited by instances of Foo
.
For the sake of finishing ES6 classes in time, they were deliberately designed to be “maximally minimal”. That’s why you can currently only create static methods, getters, and setters, but not static data properties. There is a proposal for adding them to the language. Until that proposal is accepted, there are two work-arounds that you can use.
First, you can manually add a static property:
You could use Object.defineProperty()
to create a read-only property, but I like the simplicity of an assignment.
Second, you can create a static getter:
In both cases, you get a property Point.ZERO
that you can read. In the first case, the same instance is returned every time. In the second case, a new instance is returned every time.
The syntax for getters and setters is just like in ECMAScript 5 object literals:
You use MyClass
as follows.
You can define the name of a method via an expression, if you put it in square brackets. For example, the following ways of defining Foo
are all equivalent.
Several special methods in ECMAScript 6 have keys that are symbols. Computed method names allow you to define such methods. For example, if an object has a method whose key is Symbol.iterator
, it is iterable. That means that its contents can be iterated over by the for-of
loop and other language mechanisms.
If you prefix a method definition with an asterisk (*
), it becomes a generator method. Among other things, a generator is useful for defining the method whose key is Symbol.iterator
. The following code demonstrates how that works.
The extends
clause lets you create a subclass of an existing constructor (which may or may not have been defined via a class):
Again, this class is used like you’d expect:
There are two kinds of classes:
Point
is a base class, because it doesn’t have an extends
clause.ColorPoint
is a derived class.There are two ways of using super
:
constructor
in a class definition) uses it like a function call (super(···)
), in order to make a superconstructor call (line A).static
) use it like property references (super.prop
) or method calls (super.method(···)
), in order to refer to superproperties (line B).The prototype of a subclass is the superclass in ECMAScript 6:
That means that static properties are inherited:
You can even super-call static methods:
In a derived class, you must call super()
before you can use this
:
Implicitly leaving a derived constructor without calling super()
also causes an error:
Just like in ES5, you can override the result of a constructor by explicitly returning an object:
If you do so, it doesn’t matter whether this
has been initialized or not. In other words: you don’t have to call super()
in a derived constructor if you override the result in this manner.
If you don’t specify a constructor
for a base class, the following definition is used:
For derived classes, the following default constructor is used:
In ECMAScript 6, you can finally subclass all built-in constructors (there are work-arounds for ES5, but these have significant limitations).
For example, you can now create your own exception classes (that will inherit the feature of having a stack trace in most engines):
You can also create subclasses of Array
whose instances properly handle length
:
Note that subclassing Array
is usually not the best solution. It’s often better to create your own class (whose interface you control) and to delegate to an Array in a private property.
This section explains four approaches for managing private data for ES6 classes:
constructor
Approaches #1 and #2 were already common in ES5, for constructors. Approaches #3 and #4 are new in ES6. Let’s implement the same example four times, via each of the approaches.
Our running example is a class Countdown
that invokes a callback action
once a counter (whose initial value is counter
) reaches zero. The two parameters action
and counter
should be stored as private data.
In the first implementation, we store action
and counter
in the environment of the class constructor. An environment is the internal data structure, in which a JavaScript engine stores the parameters and local variables that come into existence whenever a new scope is entered (e.g. via a function call or a constructor call). This is the code:
Using Countdown
looks like this:
Pros:
Cons:
More information on this technique: Sect. “Private Data in the Environment of a Constructor (Crockford Privacy Pattern)” in “Speaking JavaScript”.
The following code keeps private data in properties whose names a marked via a prefixed underscore:
Pros:
Cons:
There is a neat technique involving WeakMaps that combines the advantage of the first approach (safety) with the advantage of the second approach (being able to use prototype methods). This technique is demonstrated in the following code: we use the WeakMaps _counter
and _action
to store private data.
Each of the two WeakMaps _counter
and _action
maps objects to their private data. Due to how WeakMaps work that won’t prevent objects from being garbage-collected. As long as you keep the WeakMaps hidden from the outside world, the private data is safe.
If you want to be even safer, you can store WeakMap.prototype.get
and WeakMap.prototype.set
in variables and invoke those (instead of the methods, dynamically):
Then your code won’t be affected if malicious code replaces those methods with ones that snoop on our private data. However, you are only protected against code that runs after your code. There is nothing you can do if it runs before yours.
Pros:
Con:
Another storage location for private data are properties whose keys are symbols:
Each symbol is unique, which is why a symbol-valued property key will never clash with any other property key. Additionally, symbols are somewhat hidden from the outside world, but not completely:
Pros:
Cons:
Reflect.ownKeys()
.Subclassing in JavaScript is used for two reasons:
instanceof
) is also an instance of the superclass. The expectation is that subclass instances behave like superclass instances, but may do more.The usefulness of classes for implementation inheritance is limited, because they only support single inheritance (a class can have at most one superclass). Therefore, it is impossible to inherit tool methods from multiple sources – they must all come from the superclass.
So how can we solve this problem? Let’s explore a solution via an example. Consider a management system for an enterprise where Employee
is a subclass of Person
.
Additionally, there are tool classes for storage and for data validation:
It would be nice if we could include the tool classes like this:
That is, we want Employee
to be a subclass of Storage
which should be a subclass of Validation
which should be a subclass of Person
. Employee
and Person
will only be used in one such chain of classes. But Storage
and Validation
will be used multiple times. We want them to be templates for classes whose superclasses we fill in. Such templates are called abstract subclasses or mixins.
One way of implementing a mixin in ES6 is to view it as a function whose input is a superclass and whose output is a subclass extending that superclass:
Here, we profit from the operand of the extends
clause not being a fixed identifier, but an arbitrary expression. With these mixins, Employee
is created like this:
Acknowledgement. The first occurrence of this technique that I’m aware of is a Gist by Sebastian Markbåge.
What we have seen so far are the essentials of classes. You only need to read on if you are interested how things happen under the hood. Let’s start with the syntax of classes. The following is a slightly modified version of the syntax shown in Sect. A.4 of the ECMAScript 6 specification.
Two observations:
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.TypeException
if they are.Class declarations create (mutable) let bindings. The following table describes the attributes of properties related to a given class Foo
:
writable | enumerable | configurable | |
---|---|---|---|
Static properties Foo.*
|
true |
false |
true |
Foo.prototype |
false |
false |
false |
Foo.prototype.constructor |
false |
false |
true |
Prototype properties Foo.prototype.*
|
true |
false |
true |
Notes:
Classes have lexical inner names, just like named function expressions.
You may know that named function expressions have lexical inner names:
The name me
of the named function expression becomes a lexically bound variable that is unaffected by which variable currently holds the function.
Interestingly, ES6 classes also have lexical inner names that you can use in methods (constructor methods and regular methods):
(In the ES6 spec the inner name is set up by the dynamic semantics of ClassDefinitionEvaluation.)
Acknowledgement: Thanks to Michael Ficarra for pointing out that classes have inner names.
In ECMAScript 6, subclassing looks as follows.
The next section examines the structure of the objects that were created by the previous example. The section after that examines how jane
is allocated and initialized.
The previous example creates the following objects.
Prototype chains are objects linked via the [[Prototype]]
relationship (which is an inheritance relationship). In the diagram, you can see two prototype chains:
The prototype of a derived class is the class it extends. The reason for this setup is that you want a subclass to inherit all properties of its superclass:
The prototype of a base class is Function.prototype
, which is also the prototype of functions:
That means that base classes and all their derived classes (their prototypees) are functions. Traditional ES5 functions are essentially base classes.
The main purpose of a class is to set up this prototype chain. The prototype chain ends with Object.prototype
(whose prototype is null
). That makes Object
an implicit superclass of every base class (as far as instances and the instanceof
operator are concerned).
The reason for this setup is that you want the instance prototype of a subclass to inherit all properties of the superclass instance prototype.
As an aside, objects created via object literals also have the prototype Object.prototype
:
The data flow between class constructors is different from the canonical way of subclassing in ES5. Under the hood, it roughly looks as follows.
The instance object is created in different locations in ES6 and ES5:
super()
, which triggers a constructor call.new
, the first in a chain of constructor calls. The superconstructor is invoked via a function call.The previous code uses two new ES6 features:
new.target
is an implicit parameter that all functions have. In a chain of constructor calls, its role is similar to this
in a chain of supermethod calls.
new
(as in line B), the value of new.target
is that constructor.super()
(as in line A), the value of new.target
is the new.target
of the constructor that makes the call.undefined
. That means that you can use new.target
to determine whether a function was function-called or constructor-called (via new
).new.target
refers to the new.target
of the surrounding non-arrow function.Reflect.construct()
lets you make constructor calls while specifying new.target
via the last parameter.The advantage of this way of subclassing is that it enables normal code to subclass built-in constructors (such as Error
and Array
). A later section explains why a different approach was necessary.
As a reminder, here is how you do subclassing in ES5:
this
originally being uninitialized in derived constructors means that an error is thrown if they access this
in any way before they have called super()
.this
is initialized, calling super()
produces a ReferenceError
. This protects you against calling super()
twice.return
statement), the result is this
. If this
is uninitialized, a ReferenceError
is thrown. This protects you against forgetting to call super()
.undefined
and null
), the result is this
(this behavior is required to remain compatible with ES5 and earlier). If this
is uninitialized, a TypeError
is thrown.this
is initialized or not.extends
clause Let’s examine how the extends
clause influences how a class is set up (Sect. 14.5.14 of the spec).
The value of an extends
clause must be “constructible” (invocable via new
). null
is allowed, though.
C
: Function.prototype
(like a normal function)C.prototype
: Object.prototype
(which is also the prototype of objects created via object literals)C
: B
C.prototype
: B.prototype
C
: Object
C.prototype
: Object.prototype
Note the following subtle difference with the first case: If there is no extends
clause, the class is a base class and allocates instances. If a class extends Object
, it is a derived class and Object
allocates the instances. The resulting instances (including their prototype chains) are the same, but you get there differently.
C
: Function.prototype
C.prototype
: null
Such a class lets you avoid Object.prototype
in the prototype chain.
In ECMAScript 5, most built-in constructors can’t be subclassed (several work-arounds exist).
To understand why, let’s use the canonical ES5 pattern to subclass Array
. As we shall soon find out, this doesn’t work.
Unfortunately, if we instantiate MyArray
, we find out that it doesn’t work properly: The instance property length
does not change in reaction to us adding Array elements:
There are two obstracles that prevent myArr
from being a proper Array.
First obstacle: initialization. The this
you hand to the constructor Array
(in line A) is completely ignored. That means you can’t use Array
to set up the instance that was created for MyArray
.
Second obstacle: allocation. The instance objects created by Array
are exotic (a term used by the ECMAScript specification for objects that have features that normal objects don’t have): Their property length
tracks and influences the management of Array elements. In general, exotic objects can be created from scratch, but you can’t convert an existing normal object into an exotic one. Unfortunately, that is what Array
would have to do, when called in line A: It would have to turn the normal object created for MyArray
into an exotic Array object.
In ECMAScript 6, subclassing Array
looks as follows:
This works:
Let’s examine how the ES6 approach to subclassing removes the previously mentioned obstacles:
Array
not being able to set up an instance, is removed by Array
returning a fully configured instance. In contrast to ES5, this instance has the prototype of the subclass.The following ES6 code makes a supermethod call in line B.
To understand how super-calls work, let’s look at the object diagram of jane
:
In line B, Employee.prototype.toString
makes a super-call (line B) to the method (starting in line A) that it has overridden. Let’s call the object, in which a method is stored, the home object of that method. For example, Employee.prototype
is the home object of Employee.prototype.toString()
.
The super-call in line B involves three steps:
toString
. That method may be found in the object where the search started or later in the prototype chain.this
. The reason for doing so is: the super-called method must be able to access the same instance properties (in our example, the own properties of jane
).Note that even if you are only getting (super.prop
) or setting (super.prop = 123
) a superproperty (versus making a method call), this
may still (internally) play a role in step #3, because a getter or a setter may be invoked.
Let’s express these steps in three different – but equivalent – ways:
Variation 3 is how ECMAScript 6 handles super-calls. This approach is supported by two internal bindings that the environments of functions have (environments provide storage space, so-called bindings, for the variables in a scope):
[[thisValue]]
: This internal binding also exists in ECMAScript 5 and stores the value of this
.[[HomeObject]]
: Refers to the home object of the environment’s function. Filled in via the internal slot [[HomeObject]]
that all methods have that use super
. Both the binding and the slot are new in ECMAScript 6.super
? Referring to superproperties is handy whenever prototype chains are involved, which is why you can use it in method definitions (incl. generator method definitions, getters and setters) inside object literals and class definitions. The class can be derived or not, the method can be static or not.
Using super
to refer to a property is not allowed in function declarations, function expressions and generator functions.
super
can’t be moved You can’t move a method that uses super
: Such a method has the internal slot [[HomeObject]]
that ties it to the object it was created in. If you move it via an assignment, it will continue to refer to the superproperties of the original object. In future ECMAScript versions, there may be a way to transfer such a method, too.
One more mechanism of built-in constructors has been made extensible in ECMAScript 6: Sometimes a method creates new instances of its class. If you create a subclass – should the method return an instance of its class or an instance of the subclass? A few built-in ES6 methods let you configure how they create instances via the so-called species pattern.
As an example, consider a subclass SortedArray
of Array
. If we invoke map()
on instances of that class, we want it to return instances of Array
, to avoid unnecessary sorting. By default, map()
returns instances of the receiver (this
), but the species patterns lets you change that.
In the following three sections, I’ll use two helper functions in the examples:
The standard species pattern is used by Promise.prototype.then()
, the filter()
method of Typed Arrays and other operations. It works as follows:
this.constructor[Symbol.species]
exists, use it as a constructor for the new instance.Array
for Arrays).Implemented in JavaScript, the pattern would look like this:
Normal Arrays implement the species pattern slightly differently:
Array.prototype.map()
creates the Array it returns via ArraySpeciesCreate(this, this.length)
.
Promises use a variant of the species pattern for static methods such as Promise.all()
:
This is the default getter for the property [Symbol.species]
:
This default getter is implemented by the built-in classes Array
, ArrayBuffer
, Map
, Promise
, RegExp
, Set
and %TypedArray%
. It is automatically inherited by subclasses of these built-in classes.
There are two ways in which you can override the default species: with a constructor of your choosing or with null
.
You can override the default species via a static getter (line A):
As a result, map()
returns an instance of Array
:
If you don’t override the default species, map()
returns an instance of the subclass:
If you don’t want to use a static getter, you need to use Object.defineProperty()
. You can’t use assignment, as there is already a property with that key that only has a getter. That means that it is read-only and can’t be assigned to.
For example, here we set the species of MyArray1
to Array
:
null
If you set the species to null
then the default constructor is used (which one that is depends on which variant of the species pattern is used, consult the previous sections for more information).
Classes are controversial within the JavaScript community: On one hand, people coming from class-based languages are happy that they don’t have to deal with JavaScript’s unconventional inheritance mechanisms, anymore. On the other hand, there are many JavaScript programmers who argue that what’s complicated about JavaScript is not prototypal inheritance, but constructors.
ES6 classes provide a few clear benefits:
Let’s look at a few common complaints about ES6 classes. You will see me agree with most of them, but I also think that they benefits of classes much outweigh their disadvantages. I’m glad that they are in ES6 and I recommend to use them.
Yes, ES6 classes do obscure the true nature of JavaScript inheritance. There is an unfortunate disconnect between what a class looks like (its syntax) and how it behaves (its semantics): It looks like an object, but it is a function. My preference would have been for classes to be constructor objects, not constructor functions. I explore that approach in the Proto.js
project, via a tiny library (which proves how good a fit this approach is).
However, backward-compatibility matters, which is why classes being constructor functions also makes sense. That way, ES6 code and ES5 are more interoperable.
The disconnect between syntax and semantics will cause some friction in ES6 and later. But you can lead a comfortable life by simply taking ES6 classes at face value. I don’t think the illusion will ever bite you. Newcomers can get started more quickly and later read up on what goes on behind the scenes (after they are more comfortable with the language).
Classes only give you single inheritance, which severely limits your freedom of expression w.r.t. object-oriented design. However, the plan has always been for them to be the foundation of a multiple-inheritance mechanism such as traits.
Then a class becomes an instantiable entity and a location where you assemble traits. Until that happens, you will need to resort to libraries if you want multiple inheritance.
new
If you want to instantiate a class, you are forced to use new
in ES6. That means that you can’t switch from a class to a factory function without changing the call sites. That is indeed a limitation, but there are two mitigating factors:
new
operator, by returning an object from the constructor
method of a class.new
to a function call will be simple. Obviously that doesn’t help you if you don’t control the code that calls your code, as is the case for libraries.Therefore, classes do somewhat limit you syntactically, but, once JavaScript has traits, they won’t limit you conceptually (w.r.t. object-oriented design).
Function-calling classes is currently forbidden. That was done to keep options open for the future, to eventually add a way to handle function calls via classes.
What is the analog of Function.prototype.apply()
for classes? That is, if I have a class TheClass
and an Array args
of arguments, how do I instantiate TheClass
?
One way of doing so is via the spread operator (...
):
Another option is to use Reflect.construct()
:
The design motto for classes was “maximally minimal”. Several advanced features were discussed, but ultimately discarded in order to get a design that would be unanimously accepted by TC39.
Upcoming versions of ECMAScript can now extend this minimal design – classes will provide a foundation for features such as traits (or mixins), value objects (where different objects are equal if they have the same content) and const classes (that produce immutable instances).
The following document is an important source of this chapter: