Classes are the new foundational syntactic sugar structure in ECMAScript, so they can be a little awkward at first. While the ECMAScript 6 classes appear to support formal object-oriented programming, the concept of prototypes and constructors is still behind them.

The class definition

Like function types, there are two main ways to define classes: class declarations and class expressions. Both methods use the class keyword opening parentheses:

/ / the class declaration
class Person {} 
// Class expression
const Animal = class {};
Copy the code

Like function expressions, class expressions cannot be referenced until they are evaluated. However, unlike function definitions, while function declarations can be promoted, class definitions cannot:

console.log(FunctionExpression); // undefined 
var FunctionExpression = function() {}; 
console.log(FunctionExpression); // function() {} 
console.log(FunctionDeclaration); // FunctionDeclaration() {} 
function FunctionDeclaration() {} 
console.log(FunctionDeclaration); // FunctionDeclaration() {} 
console.log(ClassExpression); // undefined 
var ClassExpression = class {}; 
console.log(ClassExpression); // class {} 
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined 
class ClassDeclaration {} 
console.log(ClassDeclaration); // class ClassDeclaration {}
Copy the code

Another difference from function declarations is that functions are limited by function scope, while classes are limited by block scope:

{ 
 function FunctionDeclaration() {} 
 class ClassDeclaration {}}console.log(FunctionDeclaration); // FunctionDeclaration() {} 
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
Copy the code

The composition of the class

Classes can contain constructor methods, instance methods, fetch functions, set functions, and static class methods, but none of these are required. Empty class definitions work just as well. By default, the code in a class definition is executed in strict mode.

As with function constructors, most programming styles recommend that the first letter of the class name be uppercase to distinguish it from instances created with it (for example, instance Foo created with class Foo {}) :

// Empty class definition, valid
class Foo {} 
// Classes with constructors are valid
class Bar { 
  constructor(){}}// Class that has a fetching function, valid
class Baz { 
  get myBaz() {}}// Classes with static methods, valid
class Qux { 
  static myQux(){}}Copy the code

The name of the class expression is optional. After assigning a class expression to a variable, you can get the name string of the class expression through the name attribute. However, this identifier cannot be accessed outside of the class expression scope.

let Person = class PersonName { 
  identify() { 
  console.log(Person.name, PersonName.name); }}let p = new Person(); 
p.identify(); // PersonName PersonName 
console.log(Person.name); // PersonName 
console.log(PersonName); // ReferenceError: PersonName is not defined
Copy the code

Class constructor

The constructor keyword is used to create a constructor for a class within a class definition block. The method name constructor tells the interpreter that this function should be called whenever a new instance of a class is created using the new operator. The definition of the constructor is not required; not defining the constructor is equivalent to defining the constructor as an empty function.

1. The instantiation

Instantiating Person with the new operator is equal to calling its constructor with new. The only perceptible difference is that the JavaScript interpreter knows that using new and classes means instantiating with the constructor function.

Calling the class’s constructor with new does the following.

  • (1) Create an object in memory.
  • (2) The [[Prototype]] pointer inside the new object is assigned to the constructor’s Prototype property.
  • (3) This inside the constructor is assigned to the new object (i.e. this refers to the new object).
  • (4) Execute the code inside the constructor (adding properties to the new object).
  • (5) If the constructor returns a non-empty object, return that object; Otherwise, the newly created object is returned.

Here’s an example:


class Animal {} 
class Person { 
 constructor() { 
 console.log('person ctor'); }}class Vegetable { 
 constructor() { 
 this.color = 'orange'; }}let a = new Animal(); 
let p = new Person(); // person ctor 
let v = new Vegetable(); 
console.log(v.color); // orange

Copy the code

The arguments passed in when the class is instantiated are used as arguments to the constructor. The parentheses after the class name are optional if no arguments are required:


class Person { 
  constructor(name) { 
  console.log(arguments.length); 
  this.name = name || null; }}let p1 = new Person; / / 0
console.log(p1.name); // null 
let p2 = new Person(); / / 0
console.log(p2.name); // null 
let p3 = new Person('Jake'); / / 1
console.log(p3.name); // Jake
Copy the code

By default, the class constructor returns this object after execution. The object returned by the constructor is used as the instantiated object, and if there is no reference to the newly created this object, it is destroyed. However, if something other than this is returned, the object will not be associated with the class through the instanceof operator because the object’s prototype pointer has not been modified.

class Person { 
 constructor(override) { 
 this.foo = 'foo'; 
    if (override) { 
        return { 
            bar: 'bar'}; }}}let p1 = new Person(), 
 p2 = new Person(true); 
console.log(p1); // Person{ foo: 'foo' } 
console.log(p1 instanceof Person); // true 
console.log(p2); // { bar: 'bar' } 
console.log(p2 instanceof Person); // false
Copy the code

The main difference between class constructors and constructors is that the new operator must be used to call the class constructor. Normal constructors, if not called by new, take the global this (usually window) as the internal object. Calling a class constructor that forgets to use new throws an error:

function Person() {} 
class Animal {} 
// Build an instance with window as this
let p = Person(); 
let a = Animal(); 
// TypeError: class constructor Animal cannot be invoked without 'new'
Copy the code

There is nothing special about a class constructor; when instantiated, it becomes a normal instance method (but as a class constructor, it is still called with new). Therefore, it can be referenced on the instance after instantiation:

class Person {} 
// Create a new instance using the class
let p1 = new Person(); 
p1.constructor(); 
// TypeError: Class constructor Person cannot be invoked without 'new' 
// Create a new instance using a reference to the class constructor
let p2 = new p1.constructor();
Copy the code

2. Treat classes as special functions

There is no formal class type in ECMAScript. In many ways, ECMAScript classes are special functions. After a class is declared, the class identifier is checked by the Typeof operator to indicate that it is a function:

class Person {} 
console.log(Person); // class Person {} 
console.log(typeof Person); // function
Copy the code

Class identifiers have a Prototype property, which in turn has a constructor property pointing to the class itself:

class Person{} 
console.log(Person.prototype); // { constructor: f() } 
console.log(Person === Person.prototype.constructor); // true
Copy the code

As with normal constructors, we can use the instanceof operator to check whether the constructor stereotype exists in the instance stereotype chain:

class Person {} 
let p = new Person(); 
console.log(p instanceof Person); // true
Copy the code

From this, we can use the instanceof operator to check an object and a class constructor to determine whether the object is an instanceof the class. Except in this case, the class constructor uses the class identifier, such as checking for P and Person in the previous example. As mentioned earlier, the class itself has the same behavior as a normal constructor. In the context of the class, the class itself is treated as a constructor when called with new. It is important to note that the constructor method defined in the class will not be treated as a constructor and will return false when using the Instanceof operator against it. However, the return value of the instanceof operator is reversed if the class constructor is used directly as a normal constructor when the instance is created:

class Person {} 
let p1 = new Person(); 
console.log(p1.constructor === Person); // true 
console.log(p1 instanceof Person); // true 
console.log(p1 instanceof Person.constructor); // false 
let p2 = new Person.constructor(); 
console.log(p2.constructor === Person); // false 
console.log(p2 instanceof Person); // false 
console.log(p2 instanceof Person.constructor); // true
Copy the code

Classes are first-class citizens of JavaScript, so you can pass them as arguments just like any other object or function reference:


A class can be defined anywhere like a function, such as in an array
let classList = [ 
 class { 
 constructor(id) { 
 this.id_ = id; 
 console.log(`instance The ${this.id_}`); }}];function createInstance(classDefinition, id) { 
 return new classDefinition(id); 
} 
let foo = createInstance(classList[0].3141); // instance 3141

Copy the code

Similar to calling a function expression immediately, a class can be instantiated immediately:

// Since this is a class expression, the class name is optional
let p = new class Foo {
     constructor(x) { 
  console.log(x); 
  } 
}('bar'); // bar 
console.log(p); // Foo {}
Copy the code

Instances, prototypes, and class members

The syntax of a class makes it very convenient to define members that should exist on instances, members that should exist on stereotypes, and members that should exist on the class itself.

1. Instance members

The class constructor is executed each time the class identifier is called through new. Inside this function, you can add “own” properties to the newly created instance (this). There are no restrictions on what attributes to add. In addition, after the constructor completes, you can still add new members to the instance.

Each instance corresponds to a unique member object, which means that none of the members are shared on the prototype:

class Person { 
 constructor() { 
 // This example defines a string using an object wrapper type
 // In order to test the equality of two objects below
 this.name = new String('Jack'); 
 this.sayName = () = > console.log(this.name); 
 this.nicknames = ['Jake'.'J-Dog']}}let p1 = new Person(), 
p2 = new Person(); 
p1.sayName(); // Jack 
p2.sayName(); // Jack 
console.log(p1.name === p2.name); // false 
console.log(p1.sayName === p2.sayName); // false 
console.log(p1.nicknames === p2.nicknames); // false 
p1.name = p1.nicknames[0]; 
p2.name = p2.nicknames[1]; 
p1.sayName(); // Jake 
p2.sayName(); // J-Dog
Copy the code

2. Prototype methods and accessors

To share methods between instances, the class definition syntax treats methods defined in a class block as prototype methods.

class Person { 
 constructor() { 
 // Everything added to this will exist on a different instance
    this.locate = () = > console.log('instance'); 
 }

// Everything defined in the class block is defined in the class stereotype
locate() { 
 console.log('prototype'); }}let p = new Person(); 
p.locate(); // instance 
Person.prototype.locate(); // prototype
Copy the code

We can define methods in class constructors or in class blocks, but we cannot add primitive values or objects to stereotypes as member data in class blocks:

class Person { 
  name: 'Jake' 
} 
// Uncaught SyntaxError: Unexpected token
Copy the code

Class methods are equivalent to object properties, so you can use strings, symbols, or computed values as keys:

const symbolKey = Symbol('symbolKey'); 
class Person { 
 stringKey() { 
 console.log('invoked stringKey'); 
 } 
 [symbolKey]() { 
 console.log('invoked symbolKey'); 
 } 
 ['computed' + 'Key'] () {console.log('invoked computedKey'); }}let p = new Person(); 
p.stringKey(); // invoked stringKey 
p[symbolKey](); // invoked symbolKey 
p.computedKey(); // invoked computedKey
Copy the code

The class definition also supports getting and setting accessors. Syntax and behavior are the same as normal objects:


class Person { 
 set name(newName) { 
 this.name_ = newName; 
 } 
 get name() { 
 return this.name_; }}let p = new Person(); 
p.name = 'Jake'; 
console.log(p.name); // Jake

Copy the code

3. Static class methods

You can define static methods on a class. These methods are typically used to perform operations that are not instance specific and do not require an instance of the class to exist. Like prototype members, there can be only one static member per class.

Static class members are prefixed with the static keyword in the class definition. In a static member, this refers to the class itself. All other conventions are the same as for prototype members:

class Person { 
 constructor() { 
 // Everything added to this will exist on a different instance
 this.locate = () = > console.log('instance'.this); 
 } 
 // Define on the prototype object of the class
 locate() { 
 console.log('prototype'.this); 
 } 
 // Define on the class itself
 static locate() { 
 console.log('class'.this); }}let p = new Person(); 
p.locate(); // instance, Person {} 
Person.prototype.locate(); // prototype, {constructor: ... } 
Person.locate(); // class, class Person {}
Copy the code

Static class methods are ideal as instance factories:

class Person { 
 constructor(age) { 
 this.age_ = age; 
 } 
 sayAge() { 
 console.log(this.age_); 
 } 
 static create() { 
 // Create and return a Person instance using a random age
 return new Person(Math.floor(Math.random()*100)); }}console.log(Person.create()); // Person { age_: ... }
Copy the code

4. Non-functional prototypes and class members

While the class definition does not explicitly support adding member data to a stereotype or class, it can be added manually outside of the class definition:

class Person { 
 sayName() { 
 console.log(`${Person.greeting} The ${this.name}`); }}// Define data members on the class
Person.greeting = 'My name is';
// Define the data members on the prototype
Person.prototype.name = 'Jake'; 
let p = new Person(); 
p.sayName(); // My name is Jake
Copy the code

Note that there is no explicit support for adding data members in the class definition because adding mutable (modifiable) data members to shared targets (stereotypes and classes) is an anti-pattern. In general, the object instance should own the data referenced by this alone.

Iterator and generator methods

The class definition syntax supports defining generator methods on both the prototype and the class itself:

class Person { 
 // Define generator methods on the prototype
 *createNicknameIterator() { 
 yield 'Jack'; 
 yield 'Jake'; 
 yield 'J-Dog'; 
 } 
 // Define generator methods on the class
 static *createJobIterator() { 
 yield 'Butcher'; 
 yield 'Baker'; 
 yield 'Candlestick maker'; }}let jobIter = Person.createJobIterator(); 
console.log(jobIter.next().value); // Butcher 
console.log(jobIter.next().value); // Baker 
console.log(jobIter.next().value); // Candlestick maker 
let p = new Person(); 
let nicknameIter = p.createNicknameIterator(); 
console.log(nicknameIter.next().value); // Jack 
console.log(nicknameIter.next().value); // Jake 
console.log(nicknameIter.next().value); // J-Dog
Copy the code

Because generator methods are supported, class instances can be made iterable by adding a default iterator:

class Person { 
 constructor() { 
 this.nicknames = ['Jack'.'Jake'.'J-Dog']; } * [Symbol.iterator]() { 
 yield *this.nicknames.entries(); }}let p = new Person(); 
for (let [idx, nickname] of p) { 
 console.log(nickname); 
}

// Jack 
// Jake 
// J-Dog
Copy the code

It is also possible to return only an iterator instance:


class Person { 
 constructor() { 
 this.nicknames = ['Jack'.'Jake'.'J-Dog']; 
 } 
 [Symbol.iterator]() { 
 return this.nicknames.entries(); }}let p = new Person(); 
for (let [idx, nickname] of p) { 
 console.log(nickname); 
} 
// Jack 
// Jake 
// J-Dog

Copy the code

inheritance

One of the best new features in ECMAScript 6 is native support for class inheritance. Although class inheritance uses the new syntax, the prototype chain is still used behind it.

1. Inherit the foundation

ES6 classes support single inheritance. Using the extends keyword, you can inherit any object that has [[Construct]] and a stereotype. To a large extent, this means that you can inherit not only a class, but also the normal constructor (to maintain backward compatibility) :

class Vehicle {} 
/ / a derived class
class Bus extends Vehicle {} 
let b = new Bus(); 
console.log(b instanceof Bus); // true 
console.log(b instanceof Vehicle); // true 
function Person() {} 
// Inherit from the normal constructor
class Engineer extends Person {} 
let e = new Engineer(); 
console.log(e instanceof Engineer); // true 
console.log(e instanceof Person); // true
Copy the code

Derived classes access both the class and the methods defined on the stereotype through the stereotype chain. The value of this reflects the instance or class that called the corresponding method:

class Vehicle { 
 identifyPrototype(id) { 
 console.log(id, this); 
 }

  static identifyClass(id) { 
  console.log(id, this); }}class Bus extends Vehicle {} 
let v = new Vehicle(); 
let b = new Bus(); 
b.identifyPrototype('bus'); // bus, Bus {} 
v.identifyPrototype('vehicle'); // vehicle, Vehicle {} 
Bus.identifyClass('bus'); // bus, class Bus {} 
Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}
Copy the code

Note that the extends keyword can also be used in class expressions, so let Bar = class extends Foo {} is a valid syntax.

2. Constructor, HomeObject, and super()

Methods of derived classes can refer to their archetypes through the super keyword. This keyword can only be used in derived classes, and only inside class constructors, instance methods, and static methods. Using super in a class constructor can call the superclass constructor.

class Vehicle { 
 constructor() { 
 this.hasEngine = true; }}class Bus extends Vehicle { 
 constructor() { 
 // Do not refer to this before calling super(), otherwise a ReferenceError will be raised
 super(a);// equivalent to super.constructor()
 console.log(this instanceof Vehicle); // true 
 console.log(this); // Bus { hasEngine: true } }}new Bus();
Copy the code

A static method defined on an inherited class can be called by super from a static method:

class Vehicle { 
 static identify() { 
 console.log('vehicle'); }}class Bus extends Vehicle { 
 static identify() { 
 super.identify(); 
 } 
} 
Bus.identify(); // vehicle
Copy the code

Note that ES6 adds an internal property [[HomeObject]] to class constructors and static methods. This property is a pointer to the object that defines the method. This pointer is automatically assigned and can only be accessed from within the JavaScript engine. Super will always be defined as the prototype of [[HomeObject]].

There are several issues to be aware of when using super.

  • Super can only be used in derived class constructors and static methods.
class Vehicle { 
 constructor() { 
 super(a);// SyntaxError: 'super' keyword unexpected }}Copy the code
  • The super keyword cannot be referred to in isolation, either as a constructor call or as a static method reference.
class Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
 console.log(super); 
 // SyntaxError: 'super' keyword unexpected here }}Copy the code
  • Calling super() calls the superclass constructor and assigns the returned instance to this.

class Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
 super(a);console.log(this instanceofVehicle); }}new Bus(); // true

Copy the code
  • Super () acts like a constructor call. If you need to pass arguments to the superclass constructor, you need to pass them manually.

class Vehicle { 
 constructor(licensePlate) { 
 this.licensePlate = licensePlate; }}class Bus extends Vehicle { 
 constructor(licensePlate) { 
 super(licensePlate); }}console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }

Copy the code
  • If no class constructor is defined, super() is called when a derived class is instantiated and all arguments passed to the derived class are passed.

class Vehicle { 
 constructor(licensePlate) { 
 this.licensePlate = licensePlate; }}class Bus extends Vehicle {} 
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }

Copy the code
  • In class constructors, you cannot refer to this before calling super().

class Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
 console.log(this); }}new Bus(); 
// ReferenceError: Must call super constructor in derived class 
// before accessing 'this' or returning from derived constructor

Copy the code
  • If a constructor is explicitly defined in a derived class, it must either call super() or return it

An object.


class Vehicle {} 
class Car extends Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
 super(); 
 } 
} 
class Van extends Vehicle { 
 constructor() { 
 return{}; }}console.log(new Car()); // Car {} 
console.log(new Bus()); // Bus {} 
console.log(new Van()); / / {}

Copy the code

3. Abstract base classes

Sometimes it may be necessary to define a class that can be inherited by other classes, but will not itself be instantiated. While ECMAScript does not specifically support syntax for such classes, it is easy to implement with new.target. New.target holds the class or function called with the new keyword. We can prevent instantiation of an abstract base class by checking whether new.target is an abstract base class at instantiation time:


// Abstract base class
class Vehicle { 
 constructor() { 
 console.log(new.target); 
 if (new.target === Vehicle) { 
 throw new Error('Vehicle cannot be directly instantiated'); }}}/ / a derived class
class Bus extends Vehicle {} 
new Bus(); // class Bus {} 
new Vehicle(); // class Vehicle {} 
// Error: Vehicle cannot be directly instantiated

Copy the code

In addition, by checking in the abstract base class constructor, you can require that a derived class must define a method. Because the prototype method already exists before the class constructor is called, we can check the corresponding method by using the this keyword:

// Abstract base class
class Vehicle { 
 constructor() { 
 if (new.target === Vehicle) { 
 throw new Error('Vehicle cannot be directly instantiated'); 
 } 
 if (!this.foo) { 
 throw new Error('Inheriting class must define foo()'); 
 } 
 console.log('success! '); }}/ / a derived class
class Bus extends Vehicle { 
 foo(){}}/ / a derived class
class Van extends Vehicle {} 
new Bus(); // success! 
new Van(); // Error: Inheriting class must define foo()
Copy the code

4. Inherit the built-in types

ES6 classes provide a smooth mechanism for inheriting built-in reference types, allowing developers to easily extend built-in types:


class SuperArray extends Array { 
 shuffle() { 
 // Shuffle algorithm
 for (let i = this.length - 1; i > 0; i--) { 
 const j = Math.floor(Math.random() * (i + 1)); 
 [this[i], this[j]] = [this[j], this[i]]; }}}let a = new SuperArray(1.2.3.4.5); 
console.log(a instanceof Array); // true 
console.log(a instanceof SuperArray); // true

console.log(a); // [1, 2, 3, 4, 5] 
a.shuffle(); 
console.log(a); // [3, 1, 4, 5, 2]

Copy the code

Some methods of built-in types return new instances. By default, the type of the returned instance is the same as the type of the original instance:

class SuperArray extends Array {} 
let a1 = new SuperArray(1.2.3.4.5); 
let a2 = a1.filter(x= >!!!!! (x%2)) 
console.log(a1); // [1, 2, 3, 4, 5] 
console.log(a2); / / [1, 3, 5]
console.log(a1 instanceof SuperArray); // true 
console.log(a2 instanceof SuperArray); // true
Copy the code

If you want to override this default behavior, you can override the Symbol. Species accessor, which determines the class to use when creating the returned instance:


class SuperArray extends Array { 
 static get [Symbol.species]() { 
 return Array; }}let a1 = new SuperArray(1.2.3.4.5); 
let a2 = a1.filter(x= >!!!!! (x%2)) 
console.log(a1); // [1, 2, 3, 4, 5] 
console.log(a2); / / [1, 3, 5]
console.log(a1 instanceof SuperArray); // true 
console.log(a2 instanceof SuperArray); // false

Copy the code

5. Class

It is a common JavaScript pattern to group the behavior of different classes into a single class. Although ES6 does not explicitly support multi-class inheritance, this behavior can be easily simulated with existing features.

The object.assign () method is designed to blend in Object behavior. It is only necessary to implement a mixin expression yourself if you need to mixin the behavior of the class. If you just need to mix in the properties of multiple objects, use object.assign ().

In the code snippet below, the extends keyword is followed by a JavaScript expression. Any expression that can be resolved to a class or a constructor is valid. This expression will be evaluated when the class definition is evaluated:


class Vehicle {} 
function getParentClass() { 
 console.log('evaluated expression'); 
 return Vehicle; 
} 
class Bus extends getParentClass(a){}

// An evaluable expression

Copy the code

The mixin pattern can be implemented by concatenating multiple mixin elements in an expression that eventually resolves into a class that can be inherited. If the Person class needs to combine A, B, and C, then there needs to be some mechanism for B to inherit from A, C to inherit from B, and Person to inherit from C to combine A, B, and C into this superclass. There are different strategies for implementing this pattern.

One strategy is to define a set of “nested” functions, each of which takes a superclass as an argument, and to mix in classes defined as subclasses of that argument, and return that class. These combinatorial functions can be called concatenated to form a superclass expression:

class Vehicle {} 
let FooMixin = (Superclass) = > class extends Superclass { 
 foo() { 
 console.log('foo'); }};let BarMixin = (Superclass) = > class extends Superclass { 
 bar() { 
 console.log('bar'); }};let BazMixin = (Superclass) = > class extends Superclass { 
 baz() { 
 console.log('baz'); }};class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {} 
let b = new Bus(); 
b.foo(); // foo 
b.bar(); // bar 
b.baz(); // baz
Copy the code

Nested calls can be expanded by writing a helper function

class Vehicle {} 
let FooMixin = (Superclass) = > class extends Superclass { 
 foo() { 
 console.log('foo'); }};let BarMixin = (Superclass) = > class extends Superclass { 
 bar() { 
 console.log('bar'); }};let BazMixin = (Superclass) = > class extends Superclass { 
 baz() { 
 console.log('baz'); }};function mix(BaseClass, ... Mixins) { 
 return Mixins.reduce((accumulator, current) = > current(accumulator), BaseClass); 
} 
class Bus extends mix(Vehicle.FooMixin.BarMixin.BazMixin) {} 
let b = new Bus(); 
b.foo(); // foo 
b.bar(); // bar 
b.baz(); // baz
Copy the code

Many JavaScript frameworks (React in particular) have abandoned the mixin pattern in favor of the composite pattern (extracting methods into separate classes and helper objects and then combining them without inheritance). This reflects the well-known principle of software design: “Composition over inheritance.” This design principle is followed by many people and provides great flexibility in code design.

The statement

The above content is carried from the Little Red Book