Movatterモバイル変換


[0]ホーム

URL:


15. Classes
Table of contents
Please support this book:buy it (PDF, EPUB, MOBI) ordonate
(Ad, please don’t block.)

15.Classes



15.1Overview

A class and a subclass:

classPoint{constructor(x,y){this.x=x;this.y=y;}toString(){return`(${this.x},${this.y})`;}}classColorPointextendsPoint{constructor(x,y,color){super(x,y);this.color=color;}toString(){returnsuper.toString()+' in '+this.color;}}

Using the classes:

> const cp = new ColorPoint(25, 8, 'green');> cp.toString();'(25, 8) in green'> cp instanceof ColorPointtrue> cp instanceof Pointtrue

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 usetypeof:

> typeof Point'function'

15.2The essentials

15.2.1Base classes

A class is defined like this in ECMAScript 6:

classPoint{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:

> 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 vianew, not via a function call (the rationale behind thisis explained later):

> Point()TypeError: Classes can’t be function-called

In the spec, function-calling classes is prevented inthe internal method[[Call]] of function objects.

15.2.1.1No separators between members of class definitions

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:

classMyClass{foo(){};// OK, ignored,// SyntaxErrorbar(){}}

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.2Class declarations are not hoisted

Function declarations arehoisted: 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 hoistedfunctionfoo(){}

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 aReferenceError:

newFoo();// ReferenceErrorclassFoo{}

The reason for this limitation is that classes can have anextends 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.

functionfunctionThatUsesBar(){newBar();}functionThatUsesBar();// ReferenceErrorclassBar{}functionThatUsesBar();// OK
15.2.1.3Class expressions

Similarly to functions, there are two kinds ofclass definitions, two ways to define a class:class declarations andclass expressions.

Similarly to function expressions, class expressions can be anonymous:

constMyClass=class{···};constinst=newMyClass();

Also similarly to function expressions, class expressions can have names that are only visible inside them:

constMyClass=classMe{getClassName(){returnMe.name;}};constinst=newMyClass();console.log(inst.getClassName());// Meconsole.log(Me.name);// ReferenceError: Me is not defined

The last two lines demonstrate thatMe does not become a variable outside of the class, but can be used inside it.

15.2.2Inside the body of a class definition

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.

15.2.2.1constructor, static methods, prototype methods

Let’s examine three kinds of methods that you often find in class definitions.

classFoo{constructor(prop){this.prop=prop;}staticstaticMethod(){return'classy';}prototypeMethod(){return'prototypical';}}constfoo=newFoo(123);

The object diagram for this class declaration looks as follows. Tip for understanding it:[[Prototype]] is an inheritance relationship between objects, whileprototype is a normal property whose value is an object. The propertyprototype is only special w.r.t. thenew operator using its value as the prototype for instances it creates.

First, the pseudo-methodconstructor. This method is special, as it defines the function that represents the class:

> Foo === Foo.prototype.constructortrue> typeof Foo'function'

It is sometimes called aclass constructor. It has features that normal constructor functions don’t have (mainly the ability to constructor-call its superconstructor viasuper(), which is explained later).

Second, static methods.Static properties (orclass properties) are properties ofFoo itself. If you prefix a method definition withstatic, you create a class method:

> typeof Foo.staticMethod'function'> Foo.staticMethod()'classy'

Third, prototype methods. Theprototype properties ofFoo are the properties ofFoo.prototype. They are usually methods and inherited by instances ofFoo.

> typeof Foo.prototype.prototypeMethod'function'> foo.prototypeMethod()'prototypical'
15.2.2.2Static data properties

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:

classPoint{constructor(x,y){this.x=x;this.y=y;}}Point.ZERO=newPoint(0,0);

You could useObject.defineProperty() to create a read-only property, but I like the simplicity of an assignment.

Second, you can create a static getter:

classPoint{constructor(x,y){this.x=x;this.y=y;}staticgetZERO(){returnnewPoint(0,0);}}

In both cases, you get a propertyPoint.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.

15.2.2.3Getters and setters

The syntax for getters and setters is just likein ECMAScript 5 object literals:

classMyClass{getprop(){return'getter';}setprop(value){console.log('setter: '+value);}}

You useMyClass as follows.

> const inst = new MyClass();> inst.prop = 123;setter: 123> inst.prop'getter'
15.2.2.4Computed method names

You can define the name of a method via an expression, if you put it in square brackets. For example, the following ways of definingFoo are all equivalent.

classFoo{myMethod(){}}classFoo{['my'+'Method'](){}}constm='myMethod';classFoo{[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 isSymbol.iterator, it isiterable. That means that its contents can be iterated over by thefor-of loop and other language mechanisms.

classIterableClass{[Symbol.iterator](){···}}
15.2.2.5Generator methods

If you prefix a method definition with an asterisk (*), it becomes agenerator method. Among other things, a generator is useful for defining the method whose key isSymbol.iterator. The following code demonstrates how that works.

classIterableArguments{constructor(...args){this.args=args;}*[Symbol.iterator](){for(constargofthis.args){yieldarg;}}}for(constxofnewIterableArguments('hello','world')){console.log(x);}// Output:// hello// world

15.2.3Subclassing

Theextends clause lets you create a subclass of an existing constructor (which may or may not have been defined via a class):

classPoint{constructor(x,y){this.x=x;this.y=y;}toString(){return`(${this.x},${this.y})`;}}classColorPointextendsPoint{constructor(x,y,color){super(x,y);// (A)this.color=color;}toString(){returnsuper.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 ColorPointtrue> cp instanceof Pointtrue

There are two kinds of classes:

There are two ways of usingsuper:

15.2.3.1The prototype of a subclass is the superclass

The prototype of a subclass is the superclass in ECMAScript 6:

> Object.getPrototypeOf(ColorPoint) === Pointtrue

That means that static properties are inherited:

classFoo{staticclassMethod(){return'hello';}}classBarextendsFoo{}Bar.classMethod();// 'hello'

You can even super-call static methods:

classFoo{staticclassMethod(){return'hello';}}classBarextendsFoo{staticclassMethod(){returnsuper.classMethod()+', too';}}Bar.classMethod();// 'hello, too'
15.2.3.2Superconstructor calls

In a derived class, you must callsuper() before you can usethis:

classFoo{}classBarextendsFoo{constructor(num){consttmp=num*2;// OKthis.num=num;// ReferenceErrorsuper();this.num=num;// OK}}

Implicitly leaving a derived constructor without callingsuper() also causes an error:

classFoo{}classBarextendsFoo{constructor(){}}constbar=newBar();// ReferenceError
15.2.3.3Overriding the result of a constructor

Just like in ES5, you can override the result of a constructor by explicitly returning an object:

classFoo{constructor(){returnObject.create(null);}}console.log(newFoo()instanceofFoo);// false

If you do so, it doesn’t matter whetherthis has been initialized or not. In other words: you don’t have to callsuper() in a derived constructor if you override the result in this manner.

15.2.3.4Default constructors for classes

If you don’t specify aconstructor for a base class, the following definition is used:

constructor(){}

For derived classes, the following default constructor is used:

constructor(...args){super(...args);}
15.2.3.5Subclassing built-in constructors

In ECMAScript 6, you can finally subclass all built-in constructors (there arework-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):

classMyErrorextendsError{}thrownewMyError('Something happened!');

You can also create subclasses ofArray whose instances properly handlelength:

classStackextendsArray{gettop(){returnthis[this.length-1];}}varstack=newStack();stack.push('world');stack.push('hello');console.log(stack.top);// helloconsole.log(stack.length);// 2

Note that subclassingArray 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.

15.3Private data for classes

This section explains four approaches for managing private data for ES6 classes:

  1. Keeping private data in the environment of a classconstructor
  2. Marking private properties via a naming convention (e.g. a prefixed underscore)
  3. Keeping private data in WeakMaps
  4. 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.

15.3.1Private data via constructor environments

Our running example is a classCountdown that invokes a callbackaction once a counter (whose initial value iscounter) reaches zero. The two parametersaction andcounter should be stored as private data.

In the first implementation, we storeaction andcounter in theenvironment 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:

classCountdown{constructor(counter,action){Object.assign(this,{dec(){if(counter<1)return;counter--;if(counter===0){action();}}});}}

UsingCountdown looks like this:

> const c = new Countdown(2, () => console.log('DONE'));> c.dec();> c.dec();DONE

Pros:

Cons:

More information on this technique: Sect. “Private Data in the Environment of a Constructor (Crockford Privacy Pattern)” in “Speaking JavaScript”.

15.3.2Private data via a naming convention

The following code keeps private data in properties whose names a marked via a prefixed underscore:

classCountdown{constructor(counter,action){this._counter=counter;this._action=action;}dec(){if(this._counter<1)return;this._counter--;if(this._counter===0){this._action();}}}

Pros:

Cons:

15.3.3Private 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=newWeakMap();const_action=newWeakMap();classCountdown{constructor(counter,action){_counter.set(this,counter);_action.set(this,action);}dec(){letcounter=_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 storeWeakMap.prototype.get andWeakMap.prototype.set in variables and invoke those (instead of the methods, dynamically):

constset=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:

Con:

15.3.4Private data via symbols

Another storage location for private data are properties whose keys are symbols:

const_counter=Symbol('counter');const_action=Symbol('action');classCountdown{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:

constc=newCountdown(2,()=>console.log('DONE'));console.log(Object.keys(c));// []console.log(Reflect.ownKeys(c));// [ Symbol(counter), Symbol(action) ]

Pros:

Cons:

15.3.5Further reading

15.4Simple mixins

Subclassing in JavaScript is used for two reasons:

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 whereEmployee is a subclass ofPerson.

classPerson{···}classEmployeeextendsPerson{···}

Additionally, there are tool classes for storage and for data validation:

classStorage{save(database){···}}classValidation{validate(schema){···}}

It would be nice if we could include the tool classes like this:

// Invented ES6 syntax:classEmployeeextendsStorage,Validation,Person{···}

That is, we wantEmployee to be a subclass ofStorage which should be a subclass ofValidation which should be a subclass ofPerson.Employee andPerson will only be used in one such chain of classes. ButStorage andValidation will be used multiple times. We want them to be templates for classes whose superclasses we fill in. Such templates are calledabstract subclasses ormixins.

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:

constStorage=Sup=>classextendsSup{save(database){···}};constValidation=Sup=>classextendsSup{validate(schema){···}};

Here, we profit from the operand of theextends clause not being a fixed identifier, but an arbitrary expression. With these mixins,Employee is created like this:

classEmployeeextendsStorage(Validation(Person)){···}

Acknowledgement. The first occurrence of this technique that I’m aware of isa Gist by Sebastian Markbåge.

15.5The details of classes

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 inSect. A.4 of the ECMAScript 6 specification.

ClassDeclaration:    "class" BindingIdentifier ClassTailClassExpression:    "class" BindingIdentifier? ClassTailClassTail:    ClassHeritage? "{" ClassBody? "}"ClassHeritage:    "extends" AssignmentExpressionClassBody:    ClassElement+ClassElement:    MethodDefinition    "static" MethodDefinition    ";"MethodDefinition:    PropName "(" FormalParams ")" "{" FuncBody "}"    "*" PropName "(" FormalParams ")" "{" GeneratorBody "}"    "get" PropName "(" ")" "{" FuncBody "}"    "set" PropName "(" PropSetParams ")" "{" FuncBody "}"PropertyName:    LiteralPropertyName    ComputedPropertyNameLiteralPropertyName:    IdentifierName  /* foo */    StringLiteral   /* "foo" */    NumericLiteral  /* 123.45, 0xFF */ComputedPropertyName:    "[" Expression "]"

Two observations:

15.5.1Various checks

15.5.2Attributes of properties

Class declarations create (mutable) let bindings. The following table describes the attributes of properties related to a given classFoo:

 writableenumerableconfigurable
Static propertiesFoo.*truefalsetrue
Foo.prototypefalsefalsefalse
Foo.prototype.constructorfalsefalsetrue
Prototype propertiesFoo.prototype.*truefalsetrue

Notes:

The properties shown in the table are created in Sect. “Runtime Semantics: ClassDefinitionEvaluation” in the spec.

15.5.3Classes have inner names

Classes have lexical inner names, just like named function expressions.

15.5.3.1The inner names of named function expressions

You may know that named function expressions have lexical inner names:

constfac=functionme(n){if(n>0){// Use inner name `me` to// refer to functionreturnn*me(n-1);}else{return1;}};console.log(fac(3));// 6

The nameme of the named function expression becomes a lexically bound variable that is unaffected by which variable currently holds the function.

15.5.3.2The inner names of classes

Interestingly, ES6 classes also have lexical inner names that you can use in methods (constructor methods and regular methods):

classC{constructor(){// Use inner name C to refer to classconsole.log(`constructor:${C.prop}`);}logProp(){// Use inner name C to refer to classconsole.log(`logProp:${C.prop}`);}}C.prop='Hi!';constD=C;C=null;// C is not a class, anymore:newC().logProp();// TypeError: C is not a function// But inside the class, the identifier C// still worksnewD().logProp();// constructor: Hi!// logProp: Hi!

(In the ES6 spec the inner name is set up bythe dynamic semantics of ClassDefinitionEvaluation.)

Acknowledgement: Thanks to Michael Ficarra for pointing out that classes have inner names.

15.6The details of subclassing

In ECMAScript 6, subclassing looks as follows.

classPerson{constructor(name){this.name=name;}toString(){return`Person named${this.name}`;}staticlogNames(persons){for(constpersonofpersons){console.log(person.name);}}}classEmployeeextendsPerson{constructor(name,title){super(name);this.title=title;}toString(){return`${super.toString()} (${this.title})`;}}constjane=newEmployee('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 howjane is allocated and initialized.

15.6.1Prototype chains

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:

15.6.1.1Left column: classes (functions)

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.logNamestrue

The prototype of a base class isFunction.prototype, which is also the prototype of functions:

> const getProto = Object.getPrototypeOf.bind(Object);> getProto(Person) === Function.prototypetrue> getProto(function () {}) === Function.prototypetrue

That means that base classes and all their derived classes (their prototypees) are functions. Traditional ES5 functions are essentially base classes.

15.6.1.2Right column: the prototype chain of the instance

The main purpose of a class is to set up this prototype chain. The prototype chain ends withObject.prototype (whose prototype isnull). That makesObject an implicit superclass of every base class (as far as instances and theinstanceof 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 prototypeObject.prototype:

> Object.getPrototypeOf({}) === Object.prototypetrue

15.6.2Allocating and initializing instances

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 allocatedfunctionPerson(name){// Performed before entering this constructor:this=Object.create(new.target.prototype);this.name=name;}···functionEmployee(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);···constjane=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:

The previous code uses two new ES6 features:

The advantage of this way of subclassing is that it enables normal code to subclass built-in constructors (such asError andArray). A later section explains why a different approach was necessary.

As a reminder, here is how you do subclassing in ES5:

functionPerson(name){this.name=name;}···functionEmployee(name,title){Person.call(this,name);this.title=title;}Employee.prototype=Object.create(Person.prototype);Employee.prototype.constructor=Employee;···
15.6.2.1Safety checks
15.6.2.2Theextends clause

Let’s examine how theextends clause influences how a class is set up (Sect. 14.5.14 of the spec).

The value of anextends clause must be “constructible” (invocable vianew).null is allowed, though.

classC{}
classCextendsB{}
classCextendsObject{}

Note the following subtle difference with the first case: If there is noextends clause, the class is a base class and allocates instances. If a class extendsObject, it is a derived class andObject allocates the instances. The resulting instances (including their prototype chains) are the same, but you get there differently.

classCextendsnull{}

Such a class lets you avoidObject.prototype in the prototype chain.

15.6.3Why can’t you subclass built-in constructors in ES5?

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 subclassArray. As we shall soon find out, this doesn’t work.

functionMyArray(len){Array.call(this,len);// (A)}MyArray.prototype=Object.create(Array.prototype);

Unfortunately, if we instantiateMyArray, we find out that it doesn’t work properly: The instance propertylength does not change in reaction to us adding Array elements:

> var myArr = new MyArray(0);> myArr.length0> myArr[0] = 'foo';> myArr.length0

There are two obstracles that preventmyArr from being a proper Array.

First obstacle: initialization. Thethis you hand to the constructorArray (in line A) is completely ignored. That means you can’t useArray to set up the instance that was created forMyArray.

> var a = [];> var b = Array.call(a, 3);> a !== b  // a is ignored, b is a new objecttrue> b.length // set up correctly3> a.length // unchanged0

Second obstacle: allocation. The instance objects created byArray areexotic (a term used by the ECMAScript specification for objects that have features that normal objects don’t have): Their propertylength 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 whatArray would have to do, when called in line A: It would have to turn the normal object created forMyArray into an exotic Array object.

15.6.3.1The solution: ES6 subclassing

In ECMAScript 6, subclassingArray looks as follows:

classMyArrayextendsArray{constructor(len){super(len);}}

This works:

> const myArr = new MyArray(0);> myArr.length0> myArr[0] = 'foo';> myArr.length1

Let’s examine how the ES6 approach to subclassing removes the previously mentioned obstacles:

15.6.4Referring to superproperties in methods

The following ES6 code makes a supermethod call in line B.

classPerson{constructor(name){this.name=name;}toString(){// (A)return`Person named${this.name}`;}}classEmployeeextendsPerson{constructor(name,title){super(name);this.title=title;}toString(){return`${super.toString()} (${this.title})`;// (B)}}constjane=newEmployee('Jane','CTO');console.log(jane.toString());// Person named Jane (CTO)

To understand how super-calls work, let’s look at the object diagram ofjane:

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, thehome object of that method. For example,Employee.prototype is the home object ofEmployee.prototype.toString().

The super-call in line B involves three steps:

  1. Start your search in the prototype of the home object of the current method.
  2. Look for a method whose name istoString. That method may be found in the object where the search started or later in the prototype chain.
  3. Call that method with the currentthis. 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 ofjane).

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 ES5varresult=Person.prototype.toString.call(this)// steps 1,2,3// Variation 2: ES5, refactoredvarsuperObject=Person.prototype;// step 1varsuperMethod=superObject.toString;// step 2varresult=superMethod.call(this)// step 3// Variation 3: ES6varhomeObject=Employee.prototype;varsuperObject=Object.getPrototypeOf(homeObject);// step 1varsuperMethod=superObject.toString;// step 2varresult=superMethod.call(this)// step 3

Variation 3 is how ECMAScript 6 handles super-calls. This approach is supported bytwo internalbindings that theenvironments of functions have (environments provide storage space, so-calledbindings, for the variables in a scope):

Methods are a special kind of function now

In a class, a method definition that usessuper creates a special kind of function: It is still a function, but it has the internal slot[[HomeObject]]. That slot 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.)

15.6.4.1Where can you usesuper?

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.

Usingsuper to refer to a property is not allowed in function declarations, function expressions and generator functions.

15.6.4.2Pitfall: A method that usessuper can’t be moved

You can’t move a method that usessuper: 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.

15.7The species pattern

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-calledspecies pattern.

As an example, consider a subclassSortedArray ofArray. If we invokemap() on instances of that class, we want it to return instances ofArray, to avoid unnecessary sorting. By default,map() returns instances of the receiver (this), but the species patterns lets you change that.

15.7.1Helper methods for examples

In the following three sections, I’ll use two helper functions in the examples:

functionisObject(value){return(value!==null&&(typeofvalue==='object'||typeofvalue==='function'));}/** * Spec-internal operation that determines whether `x` * can be used as a constructor. */functionisConstructor(x){···}

15.7.2The standard species pattern

The standard species pattern is used byPromise.prototype.then(), thefilter() method of Typed Arrays and other operations. It works as follows:

Implemented in JavaScript, the pattern would look like this:

functionSpeciesConstructor(O,defaultConstructor){constC=O.constructor;if(C===undefined){returndefaultConstructor;}if(!isObject(C)){thrownewTypeError();}constS=C[Symbol.species];if(S===undefined||S===null){returndefaultConstructor;}if(!isConstructor(S)){thrownewTypeError();}returnS;}

The standard species pattern is implemented in the spec via the operationSpeciesConstructor().

15.7.3The species pattern for Arrays

Normal Arrays implement the species pattern slightly differently:

functionArraySpeciesCreate(self,length){letC=undefined;// If the receiver `self` is an Array,// we use the species patternif(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 Arrayif(C===undefined||C===null){returnnewArray(length);}if(!IsConstructor(C)){thrownewTypeError();}returnnewC(length);}

Array.prototype.map() creates the Array it returns viaArraySpeciesCreate(this, this.length).

The species pattern for Arrays is implemented in the spec via the operationArraySpeciesCreate().

15.7.4The species pattern in static methods

Promises use a variant of the species pattern for static methods such asPromise.all():

letC=this;// defaultif(!isObject(C)){thrownewTypeError();}// The default can be overridden via the property `C[Symbol.species]`constS=C[Symbol.species];if(S!==undefined&&S!==null){C=S;}if(!IsConstructor(C)){thrownewTypeError();}constinstance=newC(···);

15.7.5Overriding the default species in subclasses

This is the default getter for the property[Symbol.species]:

staticget[Symbol.species](){returnthis;}

This default getter is implemented by the built-in classesArray,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 withnull.

15.7.5.1Setting the species to a constructor of your choosing

You can override the default species via a static getter (line A):

classMyArray1extendsArray{staticget[Symbol.species](){// (A)returnArray;}}

As a result,map() returns an instance ofArray:

constresult1=newMyArray1().map(x=>x);console.log(result1instanceofArray);// true

If you don’t override the default species,map() returns an instance of the subclass:

classMyArray2extendsArray{}constresult2=newMyArray2().map(x=>x);console.log(result2instanceofMyArray2);// true
15.7.5.1.1Specifying the species via a data property

If you don’t want to use a static getter, you need to useObject.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 ofMyArray1 toArray:

Object.defineProperty(MyArray1,Symbol.species,{value:Array});
15.7.5.2Setting the species tonull

If you set the species tonull 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).

classMyArray3extendsArray{staticget[Symbol.species](){returnnull;}}constresult3=newMyArray3().map(x=>x);console.log(result3instanceofArray);// true

15.8The pros and cons of classes

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.

15.8.1Complaint: ES6 classes obscure the true nature of JavaScript inheritance

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 beconstructor objects, not constructor functions. I explore that approach intheProto.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).

15.8.2Complaint: Classes provide only single inheritance

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 outtraits.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.

15.8.3Complaint: Classes lock you in, due to mandatorynew

If you want to instantiate a class, you are forced to usenew 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:

Therefore, classes dosomewhat limit you syntactically, but, once JavaScript has traits, they won’t limit youconceptually (w.r.t. object-oriented design).

15.9FAQ: classes

15.9.1Why can’t classes be function-called?

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.

15.9.2How do I instantiate a class, given an Array of arguments?

What is the analog ofFunction.prototype.apply() for classes? That is, if I have a classTheClass and an Arrayargs of arguments, how do I instantiateTheClass?

One way of doing so is via the spread operator (...):

functioninstantiate(TheClass,args){returnnewTheClass(...args);}

Another option is to useReflect.construct():

functioninstantiate(TheClass,args){returnReflect.construct(TheClass,args);}

15.10What is next for classes?

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).

15.11Further reading

The following document is an important source of this chapter:

Next:16. Modules

[8]ページ先頭

©2009-2025 Movatter.jp