Before ECMASCript 6, constructor patterns and prototype patterns and their combinations were used to simulate the behavior of classes. However, these strategies have their own problems and corresponding compromises. The implementation of inheritance can also be very lengthy and confusing. Therefore, ECMASCript5 introduces a new class keyword to define classes, but in fact, the concepts of prototype and constructor are still used behind it.
Class definition
Classes are "special functions", so there are two ways to define classes. The first way to define a class is to declare it.
class Person {}
Another way to define a class is a class expression.
const Person = class {};
The name of the class expression is optional. After assigning a class expression to a variable, you can obtain the name string of the class expression through the name attribute. However, this identifier cannot be accessed outside the scope of a class expression.
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
There are important differences between the two. Function declarations can be promoted, but class declarations will not.
let p = new Person(); // ReferenceError: Cannot access 'Person' before initialization class Person {} let s = new Student(); // ReferenceError: Cannot access 'Student' before initialization let Student = class {};
Classes can contain constructor methods, instance methods, get functions, set functions, and static class methods, but these are not required. Empty class definitions are still valid. By default, the code in the class definition is executed in strict mode.
class Person { // Constructor constructor() {} // Get function get name() {} // Static method static of() {} }
Construction method
The constructor method is a constructor in the class definition. This method is called when the class creates an instance. The definition of construction method is not necessary. Not defining construction method is equivalent to defining construction function method as empty function.
When the constructor method is defined in the class and the new instance object is used, the constructor method will be called for instantiation, and the following operations will be performed.
- Create a new object in memory.
- The [[prototype]] pointer inside the new object is assigned to the prototype attribute of the constructor.
- this inside the constructor points to the new object.
- Execute the code inside the constructor.
- If the constructor returns a non empty object, the object is returned; Otherwise, the new object just created is returned.
class Person { constructor(name) { console.log(arguments.length); this.name = name || null; } } class Student { constructor() { this.name = "default name"; } }
When the class is instantiated, the parameters passed in are used as parameters of the constructor. If no parameters are required, the parentheses after the class name are also optional.
let p1 = new Person; // 0 let p2 = new Person(); // 0 let p3 = new Person("Xiao Gang"); // 1 console.log(p1.name); // null console.log(p2.name); // null console.log(p3.name); // Xiao Gang let s2 = new Student(); console.log(s2.name); // default name
By default, the class constructor returns this object after execution. The object returned by the constructor will be used as the instantiated object. If there is no reference to the newly created this object, the object will be destroyed. However, if the returned object is not this object, but other objects, the object will not be associated with the class through the instanceof operator, because the prototype pointer of the object has not been modified.
class Person { constructor(override) { this.name = 'Richie '; if (override) { return { nickname: "nickname" }; } } } let p1 = new Person(), p2 = new Person(true); console.log(p1); // Person {Name: 'Xiao Qi'} console.log(p1 instanceof Person); // true console.log(p2); // {nickname: 'nickname'} console.log(p2 instanceof Person); // false
The main difference between a class constructor and a constructor is that the new operator must be used to call the class constructor. If the ordinary constructor does not use the new call, it will take the global this (usually window) as the internal object. If you forget to use new when calling the class constructor, an error will be thrown:
function Person() {} class Student {} // Use window as this to build an instance let p = Person(); let a = Student(); // TypeError: Class constructor Student cannot be invoked without 'new'
There is nothing special about the class constructor. After instantiation, it will become an ordinary instance method (but as a class constructor, it still needs to use new call). Therefore, it can be referenced on the instance after instantiation:
class Person {} // Create a new instance using 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(); // You can pass here
In ECMAScript, the class defined by class is detected by typeof. Its essence is a function:
class Person {} console.log(Person); // [class Person] console.log(typeof Person); // function
The class identifier has a prototype attribute, and the prototype also has a constructor attribute pointing to the class itself:
class Person{} console.log(Person.prototype); // Person {} console.log(Person === Person.prototype.constructor); // true
Like ordinary constructors, you can use the instanceof operator to check whether the constructor prototype exists in the prototype chain of the instance:
class Person {} let p = new Person(); console.log(p instanceof Person); // true
Class is a first-class citizen of JavaScript, so it can be passed as a parameter like other object or function references:
// Classes can be defined anywhere like functions, such as in arrays let classes = [ class { constructor(id) { this.id = id; console.log(`instance ${this.id}`); } } ]; function tryInstance(classDefinition, id) { return new classDefinition(id); } let instance = tryInstance(classes[0], 3.14); // instance 3.14
Similar to calling a function expression immediately, a class can also be instantiated immediately:
// Because it is a class expression, the class name is optional let p = new class Person { constructor(x) { console.log(x); } }('cockroach'); // cockroach console.log(p); // Person {}
Example method
Each time an instance is created, the class constructor is executed. In the construction method of a class, you can add instance properties to the class through this. Each instance corresponds to a unique member object, which means that all members will not be shared on the prototype:
class Person { constructor() { // This example first defines a string using the object wrapper type // To test the equality of the two objects below this.name = new String("Xiao Li"); this.sayName = () => console.log(this.name); this.nicknames = ['Section', 'Small point'] } } let p1 = new Person(), p2 = new Person(); p1.sayName(); // Xiao Li p2.sayName(); // Xiao Li 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(); // Section p2.sayName(); // Small point
Static properties or prototype data properties must be defined outside the class definition.
class Person { sayName() { console.log(`${Person.greeting} ${this.name}`); } } // Defining data members on a class Person.greeting = 'My name is'; // Define data members on prototypes Person.prototype.name = 'Colin'; let p = new Person(); p.sayName(); // My name is Colin
Note: the reason why adding data members is not supported in the class definition is that adding variable data members on the shared target is an anti pattern. In general, an object instance should have its own data referenced through this.
Prototype method
In order to share methods between instances, class definition syntax takes the methods defined in class blocks as prototype methods.
class Person { constructor() { // Everything added to this will exist on different instances this.locate = () => console.log('instance'); } // Everything defined in the class block is defined on the prototype of the class locate() { console.log('prototype'); } } let p = new Person(); p.locate(); // instance Person.prototype.locate(); // prototype
Static method
Static methods can be defined on a class using the staitc keyword. You do not need to instantiate this class to call a static method, but you cannot call a static method through a class instance. In a static method, this refers to the class itself.
class Person { constructor() { // Everything added to this will exist on different instances this.locate = () => console.log('instance', this); } // Defined on the prototype object of the class locate() { console.log('prototype', this); } // Defined on the class itself static locate() { console.log('class', this); } } let p = new Person(); p.locate(); // instance Person { locate: [Function] } Person.prototype.locate(); // prototype Person {} Person.locate(); // class [class Person]
Iterators and generators
The class definition syntax supports defining generator methods on the prototype and the class itself:
class Person { // Define generator methods on prototypes * createNicknameIterator() { yield 'Jack dog'; yield 'Jack mouse'; yield 'Jack cat'; } // Define generator methods on classes static* createJobIterator() { yield 'Bond one'; yield 'Bond II'; yield 'Bond III'; } } let jobIter = Person.createJobIterator(); console.log(jobIter.next().value); // Bond one console.log(jobIter.next().value); // Bond II console.log(jobIter.next().value); // Bond III let p = new Person(); let nicknameIter = p.createNicknameIterator(); console.log(nicknameIter.next().value); // Jack dog console.log(nicknameIter.next().value); // Jack mouse console.log(nicknameIter.next().value); // Jack cat
Because the generator method is supported, you can turn a class instance into an iteratable object by adding a default iterator:
class Person { constructor() { this.nicknames = ['Jack dog', 'Jack mouse', 'Jack cat']; } *[Symbol.iterator]() { yield *this.nicknames.entries(); } } let p = new Person(); for (let [idx, nickname] of p) { console.log(nickname); } // Jack dog // Jack mouse // Jack cat
You can also return only iterator instances:
class Person { constructor() { this.nicknames = ['Jack dog', 'Jack mouse', 'Jack cat']; } [Symbol.iterator]() { return this.nicknames.entries(); } } let p = new Person(); for (let [idx, nickname] of p) { console.log(nickname); } // Jack dog // Jack mouse // Jack cat
inherit
ECMAScript 6 implements a class inheritance mechanism with any object with [[Construct]] and prototype through the syntax sugar provided by the extends keyword.
class Person {} class Student extends Person{} let s = new Student(); console.log(s instanceof Student); // true console.log(s instanceof Person); // true
However, this inheritance method can also inherit ordinary constructors.
function Person() {} class Student extends Person{} let s = new Student(); console.log(s instanceof Student); // true console.log(s instanceof Person); // true
This inheritance can be used on class expressions.
let Student = class extends Person {}
Subclasses can also reference their prototypes through the super keyword and can only be used in subclasses. The constructor is defined in the subclass. You must call super() before you can use this. Calling super() here will call the constructor of the parent class and assign the returned instance to this.
class Person { constructor(name) { this.name = name; } } class Student extends Person{ constructor(name) { super(name); console.log(this); } } let s = new Student("Xiao Wang"); // Student {Name: 'Xiao Wang'} console.log(s.name); // Xiao Wang
In the static method, you can call the static method defined on the parent class through super:
class Person { constructor(name) { this.name = name; } static of(name) { return new Person(name); } } class Student extends Person{ constructor(name) { super(name); console.log(this); } static of(name) { return super.of(name); } } let s = Student.of("Xiaojun"); console.log(s); // Person {Name: 'Xiaojun'} console.log(s instanceof Student); // false console.log(s instanceof Person); // true
ECMAScript can implement an abstract base class inherited by other classes through new. Target, but it will not be instantiated itself. New.target saves the class or function called through the new keyword. By detecting whether new.target is an abstract base class during instantiation, instantiation of the abstract base class can be prevented:
class Person { constructor(name) { if (new.target === Person) { throw new Error("Person Cannot be instantiated directly"); } this.name = name; } } class Student extends Person { constructor(name) { super(name); } } let s = new Student("Xiao Ping"); let p = new Person("Xiao Ling"); // Error: Person cannot be instantiated directly
You can also check whether a subclass defines a method in the constructor of the abstract base class. Because the prototype method already exists before calling the class construction method, you can check the corresponding method through this:
class Person { constructor(name) { if (new.target === Person) { throw new Error("Person Cannot be instantiated directly"); } if (!this.action) { throw Error("Inherited classes must be defined action()method"); } this.name = name; } } class Student extends Person { constructor(name) { super(name); } action() { console.log("Student behavior"); } } class Employee extends Person { constructor(name) { super(name); } } let s = new Student("Xiao Ping"); let e = new Employee("Xiao Hong"); // Error: the inherited class must define the action() method
The newly added class es and extensions in ES6 can smoothly extend functions for built-in reference types.
class MoreArray extends Array { first() { return this[0]; } last() { return this[this.length - 1]; } } let arr = new MoreArray(20, 92, 15, 40); console.log(arr.first()); // 20 console.log(arr.last()); // 40
Class blending
ECMAScript 6 supports single inheritance, but multiple inheritance can be simulated through existing features.
The extends keyword can be followed by a JavaScript expression. Any expression that can be resolved to a class or a constructor is valid.
class Person {} function getPerson() { console.log("Object operation"); return Person; } class Student extends getPerson() {} // Object operation
The mixed mode can be realized by concatenating multiple mixed elements in an expression, which will eventually resolve to A class that can be inherited. If the Person class needs to combine A, B and C, it needs some mechanism to realize that B inherits A, C inherits B, and Person inherits C, so as to combine A, B and C into Person. There are different strategies for implementing this pattern.
One strategy is to define a group of "nested" functions. Each function receives a parent class as a parameter, and the mixed class is defined as a subclass of this parameter and returns this class. These composite functions can be called in series and finally combined into superclass expressions:
class Person {} let Action = (Superclass) => class extends Superclass { action() { console.log('action'); } }; let Face = (Superclass) => class extends Superclass { face() { console.log('expression'); } }; let Sex = (Superclass) => class extends Superclass { sex() { console.log('Gender'); } }; class Student extends Sex(Face(Action(Person))) {} let s = new Student(); s.action(); // action s.face(); // expression s.sex(); // Gender
You can also expand nested calls by writing an auxiliary function:
You can expand nested calls by writing an auxiliary function:
class Person {} let Action = (Superclass) => class extends Superclass { action() { console.log('action'); } }; let Face = (Superclass) => class extends Superclass { face() { console.log('expression'); } }; let Sex = (Superclass) => class extends Superclass { sex() { console.log('Gender'); } }; function mix(BaseClass, ...Mixins) { return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass); } class Student extends mix(Person, Action, Face, Sex) {} let s = new Student(); s.action(); // action s.face(); // expression s.sex(); // Gender
Note: many JavaScript frameworks have abandoned mixed mode and moved to composite mode. This reflects the software design principle of "combination is better than inheritance" and provides great flexibility.
summary
The new class syntax in ECMAScript 6 is largely based on the existing prototype mechanism of the language. However, this syntax can gracefully define backward compatible classes, which can inherit both built-in types and custom types. Classes effectively bridge the gap between object instances, prototypes, and classes.
More content, please pay attention to the official account of the sea.