- 클래스
15.1 개요
A class and a subclass:
클래스와 서브클래스:
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;
}
}
Using the classes:
클래스 사용하기:
> const cp = new ColorPoint(25, 8, 'green');
> cp.toString();
'(25, 8) in green'
> cp instanceof ColorPoint
true
> cp instanceof Point
true
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:
> typeof Point
'function'
A class is defined like this in ECMAScript 6:
ECMAScript 6에서 클래스는 이렇게 선언된다.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
You use this class just like an ES5 constructor function:
이 클래스는 ES5의 생성자 함수처럼 사용하면 된다.
> var p = new Point(25, 8);
> p.toString()
'(25, 8)'
In fact, the result of a class definition is a function:
사실, 클래스 선언의 결과는 함수다.
> typeof Point
'function'
However, you can only invoke a class via new, not via a function call (the rationale behind this is explained later): 하지만 클래스는 함수 호출이 아닌, 오직 new를 통해서만 호출 할 수 있다(이것을 뒷받침하는 근거는 나중에 설명한다)
> Point()
TypeError: Classes can’t be function-called
In the spec, function-calling classes is prevented in the internal method [[Call]] of function objects. 명세에 따르면, 함수로 호출되는 클래스는 함수 객체의 내부 메소드인 [[Call]]에서 방지된다.
15.2.1.1
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:
class MyClass {
foo() {}
; // OK, ignored
, // SyntaxError
bar() {}
}
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.
15.2.1.2 클래스 선언은 호이스팅되지 않는다.
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:
foo(); // works, because `foo` is hoisted // 'foo'가 호이스팅 되었기 때문에 동작한다.
function foo() {}
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:
new Foo(); // ReferenceError
class Foo {}
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.
function functionThatUsesBar() {
new Bar();
}
functionThatUsesBar(); // ReferenceError
class Bar {}
functionThatUsesBar(); // OK
15.2.1.3 클래스 표현식
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:
const MyClass = class {
···
};
const inst = new MyClass();
Also similarly to function expressions, class expressions can have names that are only visible inside them:
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
const inst = new MyClass();
console.log(inst.getClassName()); // Me
console.log(Me.name); // ReferenceError: Me is not defined
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.
Let’s examine three kinds of methods that you often find in class definitions.
class Foo {
constructor(prop) {
this.prop = prop;
}
static staticMethod() {
return 'classy';
}
prototypeMethod() {
return 'prototypical';
}
}
const foo = new Foo(123);
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:
> Foo === Foo.prototype.constructor
true
> typeof Foo
'function'
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:
> typeof Foo.staticMethod
'function'
> Foo.staticMethod()
'classy'
Third, prototype methods. The prototype properties of Foo are the properties of Foo.prototype. They are usually methods and inherited by instances of Foo.
> typeof Foo.prototype.prototypeMethod
'function'
> foo.prototypeMethod()
'prototypical'
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:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
Point.ZERO = new Point(0, 0);
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:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static get ZERO() {
return new Point(0, 0);
}
}
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:
class MyClass {
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
You use MyClass as follows.
> const inst = new MyClass();
> inst.prop = 123;
setter: 123
> inst.prop
'getter'
계산된 메소드명
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.
대괄호에 표현식을 넣어 메소드를 선언할 수 있다. 예를들어 아래의 Foo를 선언하는 방법은 모두 동등하다
class Foo() {
myMethod() {}
}
class Foo() {
['my'+'Method']() {}
}
const m = 'myMethod';
class Foo() {
[m]() {}
}
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.
ECMAScript 6의 몇몇 특별한 메소드는 심볼키를 갖는다.
class IterableClass {
[Symbol.iterator]() {
···
}
}
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.
class IterableArguments {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (const arg of this.args) {
yield arg;
}
}
}
for (const x of new IterableArguments('hello', 'world')) {
console.log(x);
}
// Output:
// hello
// world
The extends clause lets you create a subclass of an existing constructor (which may or may not have been defined via a class):
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)
}
}
Again, this class is used like you’d expect:
> const cp = new ColorPoint(25, 8, 'green');
> cp.toString()
'(25, 8) in green'
> cp instanceof ColorPoint
true
> cp instanceof Point
true
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:
A class constructor (the pseudo-method constructor in a class definition) uses it like a function call (super(···)), in order to make a superconstructor call (line A). Method definitions (in object literals or classes, with or without 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:
> Object.getPrototypeOf(ColorPoint) === Point
true
That means that static properties are inherited:
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod(); // 'hello'
You can even super-call static methods:
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod(); // 'hello, too'
In a derived class, you must call super() before you can use this:
class Foo {}
class Bar extends Foo {
constructor(num) {
const tmp = num * 2; // OK
this.num = num; // ReferenceError
super();
this.num = num; // OK
}
}
Implicitly leaving a derived constructor without calling super() also causes an error:
class Foo {}
class Bar extends Foo {
constructor() {
}
}
const bar = new Bar(); // ReferenceError
Just like in ES5, you can override the result of a constructor by explicitly returning an object:
class Foo {
constructor() {
return Object.create(null);
}
}
console.log(new Foo() instanceof Foo); // false
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:
constructor() {}
For derived classes, the following default constructor is used:
constructor(...args) {
super(...args);
}
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):
class MyError extends Error {
}
throw new MyError('Something happened!');
You can also create subclasses of Array whose instances properly handle length:
class Stack extends Array {
get top() {
return this[this.length - 1];
}
}
var stack = new Stack();
stack.push('world');
stack.push('hello');
console.log(stack.top); // hello
console.log(stack.length); // 2
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.
Subclassing built-in constructors is something that engines have to support natively, you won’t get this feature via transpilers.
This section explains four approaches for managing private data for ES6 classes:
Keeping private data in the environment of a class constructor Marking private properties via a naming convention (e.g. a prefixed underscore) Keeping private data in WeakMaps Using symbols as keys for private properties 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:
class Countdown {
constructor(counter, action) {
Object.assign(this, {
dec() {
if (counter < 1) return;
counter--;
if (counter === 0) {
action();
}
}
});
}
}
Using Countdown looks like this:
> const c = new Countdown(2, () => console.log('DONE'));
> c.dec();
> c.dec();
DONE
Pros:
The private data is completely safe The names of private properties won’t clash with the names of other private properties (of superclasses or subclasses).
Cons:
The code becomes less elegant, because you need to add all methods to the instance, inside the constructor (at least those methods that need access to the private data). Due to the instance methods, the code wastes memory. If the methods were prototype methods, they would be shared. 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:
class Countdown {
constructor(counter, action) {
this._counter = counter;
this._action = action;
}
dec() {
if (this._counter < 1) return;
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
Pros:
Code looks nice. We can use prototype methods.
Cons:
Not safe, only a guideline for client code. The names of private properties can clash. 15.3.3 Private data via WeakMaps 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.
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) return;
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
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):
const set = WeakMap.prototype.set;
···
set.call(_counter, this, counter);
// _counter.set(this, counter);
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:
We can use prototype methods. Safer than a naming convention for property keys. The names of private properties can’t clash. Relatively elegant.
Con:
Code is not as elegant as a naming convention.
Another storage location for private data are properties whose keys are symbols:
const _counter = Symbol('counter');
const _action = Symbol('action');
class Countdown {
constructor(counter, action) {
this[_counter] = counter;
this[_action] = action;
}
dec() {
if (this[_counter] < 1) return;
this[_counter]--;
if (this[_counter] === 0) {
this[_action]();
}
}
}
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:
const c = new Countdown(2, () => console.log('DONE'));
console.log(Object.keys(c));
// []
console.log(Reflect.ownKeys(c));
// [ Symbol(counter), Symbol(action) ]
Pros:
We can use prototype methods. The names of private properties can’t clash.
Cons:
Code is not as elegant as a naming convention. Not safe: you can list all property keys (including symbols!) of an object via Reflect.ownKeys().
Sect. “Keeping Data Private” in “Speaking JavaScript” (covers ES5 techniques)
Subclassing in JavaScript is used for two reasons:
Interface inheritance: Every object that is an instance of a subclass (as tested by instanceof) is also an instance of the superclass. The expectation is that subclass instances behave like superclass instances, but may do more. Implementation inheritance: Superclasses pass on functionality to their subclasses. 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.
class Person { ··· }
class Employee extends Person { ··· }
Additionally, there are tool classes for storage and for data validation:
class Storage {
save(database) { ··· }
}
class Validation {
validate(schema) { ··· }
}
It would be nice if we could include the tool classes like this:
// Invented ES6 syntax:
class Employee extends Storage, Validation, Person { ··· }
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:
const Storage = Sup => class extends Sup {
save(database) { ··· }
};
const Validation = Sup => class extends Sup {
validate(schema) { ··· }
};
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:
class Employee extends Storage(Validation(Person)) { ··· }
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.
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 "]" Two observations:
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) {}
Semicolons are allowed between methods.
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. Classes can’t be function-called. They throw a TypeException if they are. Prototype methods cannot be used as constructors: class C { m() {} } new C.prototype.m(); // TypeError
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:
Many properties are writable, to allow for dynamic patching. A constructor and the object in its property prototype have an immutable bidirectional link. Method definitions in object literals produce enumerable properties. The properties shown in the table are created in Sect. “Runtime Semantics: ClassDefinitionEvaluation” in the spec.
Classes have lexical inner names, just like named function expressions.
You may know that named function expressions have lexical inner names:
const fac = function me(n) {
if (n > 0) {
// Use inner name `me` to
// refer to function
return n * me(n-1);
} else {
return 1;
}
};
console.log(fac(3)); // 6
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):
class C {
constructor() {
// Use inner name C to refer to class
console.log(`constructor: ${C.prop}`);
}
logProp() {
// Use inner name C to refer to class
console.log(`logProp: ${C.prop}`);
}
}
C.prop = 'Hi!';
const D = C;
C = null;
// C is not a class, anymore:
new C().logProp();
// TypeError: C is not a function
// But inside the class, the identifier C
// still works
new D().logProp();
// constructor: Hi!
// logProp: Hi!
(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.
class Person {
constructor(name) {
this.name = name;
}
toString() {
return `Person named ${this.name}`;
}
static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
toString() {
return `${super.toString()} (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
console.log(jane.toString()); // Person named Jane (CTO)
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:
> Employee.logNames === Person.logNames
true
The prototype of a base class is Function.prototype, which is also the prototype of functions:
> const getProto = Object.getPrototypeOf.bind(Object);
> getProto(Person) === Function.prototype
true
> getProto(function () {}) === Function.prototype
true
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:
> Object.getPrototypeOf({}) === Object.prototype
true
The data flow between class constructors is different from the canonical way of subclassing in ES5. Under the hood, it roughly looks as follows.
// Base class: this is where the instance is allocated
function Person(name) {
// Performed before entering this constructor:
this = Object.create(new.target.prototype);
this.name = name;
}
···
function Employee(name, title) {
// Performed before entering this constructor:
this = uninitialized;
this = Reflect.construct(Person, [name], new.target); // (A)
// super(name);
this.title = title;
}
Object.setPrototypeOf(Employee, Person);
···
const jane = Reflect.construct( // (B)
Employee, ['Jane', 'CTO'],
Employee);
// const jane = new Employee('Jane', 'CTO')
The instance object is created in different locations in ES6 and ES5:
In ES6, it is created in the base constructor, the last in a chain of constructor calls. The superconstructor is invoked via super(), which triggers a constructor call.
In ES5, it is created in the operand of 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. If a constructor is directly invoked via new (as in line B), the value of new.target is that constructor. If a constructor is called via super() (as in line A), the value of new.target is the new.target of the constructor that makes the call. During a normal function call, it is undefined. That means that you can use new.target to determine whether a function was function-called or constructor-called (via new). Inside an arrow function, 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:
function Person(name) {
this.name = name;
}
···
function Employee(name, title) {
Person.call(this, name);
this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
···
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(). Once this is initialized, calling super() produces a ReferenceError. This protects you against calling super() twice. If a constructor returns implicitly (without a return statement), the result is this. If this is uninitialized, a ReferenceError is thrown. This protects you against forgetting to call super(). If a constructor explicitly returns a non-object (including 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. If a constructor explicitly returns an object, it is used as its result. Then it doesn’t matter whether this is initialized or not. 15.6.2.2 The 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.
class C { } Constructor kind: base Prototype of C: Function.prototype (like a normal function) Prototype of C.prototype: Object.prototype (which is also the prototype of objects created via object literals) class C extends B { } Constructor kind: derived Prototype of C: B Prototype of C.prototype: B.prototype class C extends Object { } Constructor kind: derived Prototype of C: Object Prototype of 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.
class C extends null { } Constructor kind: derived Prototype of C: Function.prototype Prototype of C.prototype: null Such a class lets you avoid Object.prototype in the prototype chain. But that is rarely useful. Furthermore, you have to be careful: new-calling such a class leads to an error, because the default constructor makes a superconstructor call and Function.prototype (the superconstructor) can’t be constructor-called. The only way to make the error go away is by adding a constructor that returns an object:
class C extends null {
constructor() {
const _this = Object.create(new.target.prototype);
return _this;
}
}
new.target ensures that C can be subclassed properly – the prototype of _this will always be the operand of new.
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.
function MyArray(len) {
Array.call(this, len); // (A)
}
MyArray.prototype = Object.create(Array.prototype);
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:
> var myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
0
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.
> var a = [];
> var b = Array.call(a, 3);
> a !== b // a is ignored, b is a new object
true
> b.length // set up correctly
3
> a.length // unchanged
0
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:
class MyArray extends Array {
constructor(len) {
super(len);
}
}
This works:
> const myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
1
Let’s examine how the ES6 approach to subclassing removes the previously mentioned obstacles:
The first obstacle, 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 second obstacle, subconstructors not creating exotic instances, is removed by derived classes relying on base classes for allocating instances.
The following ES6 code makes a supermethod call in line B.
class Person {
constructor(name) {
this.name = name;
}
toString() { // (A)
return `Person named ${this.name}`;
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
toString() {
return `${super.toString()} (${this.title})`; // (B)
}
}
const jane = new Employee('Jane', 'CTO');
console.log(jane.toString()); // Person named Jane (CTO)
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:
Start your search in the prototype of the home object of the current method. Look for a method whose name is toString. That method may be found in the object where the search started or later in the prototype chain. Call that method with the current 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 1: supermethod calls in ES5
var result = Person.prototype.toString.call(this) // steps 1,2,3
// Variation 2: ES5, refactored
var superObject = Person.prototype; // step 1
var superMethod = superObject.toString; // step 2
var result = superMethod.call(this) // step 3
// Variation 3: ES6
var homeObject = Employee.prototype;
var superObject = Object.getPrototypeOf(homeObject); // step 1
var superMethod = superObject.toString; // step 2
var result = superMethod.call(this) // step 3
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 an internal property [[HomeObject]] that all methods have that use super. Both the binding and the property are new in ECMAScript 6. Methods are a special kind of function now In a class, a method definition that uses super creates a special kind of function: It is still a function, but it has the internal property [[HomeObject]]. That property is set up by the method definition and can’t be changed in JavaScript. Therefore, you can’t meaningfully move such a method to a different object. (But maybe it’ll be possible in a future version of ECMAScript.)
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.
You can’t move a method that uses super: Such a method has an internal property [[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:
function isObject(value) {
return (value !== null
&& (typeof value === 'object'
|| typeof value === 'function'));
}
/**
* Spec-internal operation that determines whether `x`
* can be used as a constructor.
*/
function isConstructor(x) {
···
}
The standard species pattern is used by Promise.prototype.then(), the filter() method of Typed Arrays and other operations. It works as follows:
If this.constructor[Symbol.species] exists, use it as a constructor for the new instance. Otherwise, use a default constructor (e.g. Array for Arrays). Implemented in JavaScript, the pattern would look like this:
function SpeciesConstructor(O, defaultConstructor) {
const C = O.constructor;
if (C === undefined) {
return defaultConstructor;
}
if (! isObject(C)) {
throw new TypeError();
}
const S = C[Symbol.species];
if (S === undefined || S === null) {
return defaultConstructor;
}
if (! isConstructor(S)) {
throw new TypeError();
}
return S;
}
The standard species pattern is implemented in the spec via the operation SpeciesConstructor().
Normal Arrays implement the species pattern slightly differently:
function ArraySpeciesCreate(self, length) {
let C = undefined;
// If the receiver `self` is an Array,
// we use the species pattern
if (Array.isArray(self)) {
C = self.constructor;
if (isObject(C)) {
C = C[Symbol.species];
}
}
// Either `self` is not an Array or the species
// pattern didn’t work out:
// create and return an Array
if (C === undefined || C === null) {
return new Array(length);
}
if (! IsConstructor(C)) {
throw new TypeError();
}
return new C(length);
}
Array.prototype.map() creates the Array it returns via ArraySpeciesCreate(this, this.length).
The species pattern for Arrays is implemented in the spec via the operation ArraySpeciesCreate().
Promises use a variant of the species pattern for static methods such as Promise.all():
let C = this; // default
if (! isObject(C)) {
throw new TypeError();
}
// The default can be overridden via the property `C[Symbol.species]`
const S = C[Symbol.species];
if (S !== undefined && S !== null) {
C = S;
}
if (!IsConstructor(C)) {
throw new TypeError();
}
const instance = new C(···);
This is the default getter for the property [Symbol.species]:
static get [Symbol.species]() {
return this;
}
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):
class MyArray1 extends Array {
static get [Symbol.species]() { // (A)
return Array;
}
}
As a result, map() returns an instance of Array:
const result1 = new MyArray1().map(x => x);
console.log(result1 instanceof Array); // true
If you don’t override the default species, map() returns an instance of the subclass:
class MyArray2 extends Array { }
const result2 = new MyArray2().map(x => x);
console.log(result2 instanceof MyArray2); // true
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:
Object.defineProperty(
MyArray1, Symbol.species, {
value: Array
});
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).
class MyArray3 extends Array {
static get [Symbol.species]() {
return null;
}
}
const result3 = new MyArray3().map(x => x);
console.log(result3 instanceof Array); // true
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:
They are backwards compatible with much of the current code. Compared to constructors and constructor inheritance, classes make it easier for beginners to get started. Subclassing is supported within the language. Built-in constructors are subclassable. No library for inheritance is needed, anymore; code will become more portable between frameworks. They provide a foundation for advanced features in the future: traits (or mixins), immutable instances, etc. They help tools that statically analyze code (IDEs, type checkers, style checkers, etc.). 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, backwards-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.
traits.js: traits library for JavaScript Check out traits.js if you are interested in how traits work (they are similar to mixins, which you may be familiar with).
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.
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:
You can override the default result returned by the new operator, by returning an object from the constructor method of a class. Due to its built-in modules and classes, ES6 makes it easier for IDEs to refactor code. Therefore, going from 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).
15.9 FAQ: 클래스
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 (...):
function instantiate(TheClass, args) {
return new TheClass(...args);
}
Another option is to use Reflect.construct():
function instantiate(TheClass, args) {
return Reflect.construct(TheClass, args);
}
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:
“Instantiation Reform: One last time”, slides by Allen Wirfs-Brock.