JavaScript Advanced Programming (4th edition) Reading Notes

Chapter 8 _ Objects, Classes, and Object-oriented Programming

8.1 Understanding Objects

The usual way to create a custom Object is to create a new instance of Object and then add properties and methods to it, or use Object literals to write:

let person = new Object(a); person.name ="Nicholas"; 
person.age = 29; 
person.job = "Software Engineer"; 
person.sayName = function() {  
  console.log(this.name);
}; 
// the name of the object is
let person = {    
  name: "Nicholas".age: 29.job: "Software Engineer".sayName() {    
    console.log(this.name); }};Copy the code

8.1.1 Types of attributes

Ecma-262 uses a number of internal features to describe the characteristics of attributes. These features are defined by the specification for the JavaScript implementation engine. Therefore, developers cannot access these features directly in JavaScript. To identify a feature as an internal feature, the specification uses two brackets to enclose the name of the feature, such as [[Enumerable]]. There are two types of attributes: data attributes and accessor attributes.

1. Data attributes

Data properties contain a location to hold data values. Values are read from and written to this location. Data attributes have four properties that describe their behavior.

  • [[64x]] : Indicates whether a property can be deleted and redefined through delete, whether its features can be modified, and whether it can be changed to an accessor property. By default, this property is true for all attributes directly defined on an object, as shown in the previous example.
  • [[Enumerable]] : Indicates whether a property can be returned through a for-in loop. By default, this property is true for all attributes directly defined on an object, as shown in the previous example.
  • [[Writable]] : indicates whether the value of an attribute can be modified. By default, this property is true for all attributes directly defined on an object, as shown in the previous example.
  • [[Value]] : contains the actual Value of the attribute. This is where the property values are read and written as mentioned earlier. The default value for this feature is undefined.

[[Enumerable]], [[Writable]] and [[Writable] are all set to true, and the [[Value]] feature is set to the specified Value.

To modify the default properties of a property, you must use the object.defineProperty () method. This method takes three parameters: the object to which the attribute is to be added, the name of the attribute, and a descriptor object. The property of the descriptor object can contain different, Enumerable, Writable, and Value, which corresponds to the name of the different feature. Depending on the feature you want to modify, you can set one or more of these values.

let person = {};  
Object.defineProperty(person, "name",
  {   
    writable: false.value: "Nicholas"});console.log(person.name); // "Nicholas"
person.name = "Greg"; 
console.log(person.name); // "Nicholas" 
Attempts to reassign this property in non-strict mode are ignored. In strict mode, trying to change the value of a read-only attribute throws an error.
Copy the code

Once a property is defined as unconfigurable, it cannot be changed back to configurable. Calling Object.defineProperty() again and modifying any non-writable properties results in an error, although object.defineProperty () can be called multiple times on the same property, However, if the 64x is set to false, no additional control is provided. :

let person = {};
Object.defineProperty(person, "name",
  {  
    configurable: false.value: "Nicholas"});// Throw an error
Object.defineProperty(person, "name", {   configurable: true.value: "Nicholas" }); 
Copy the code

2. Accessor properties

Accessor properties do not contain data values. Instead, they contain a getter function and a setter function, but these are not required. When the accessor property is read, the fetch function is called, whose responsibility is to return a valid value. When the accessor property is written, the Settings function is called and the new value is passed in, and this function must decide what changes to make to the data. Accessor properties have four properties that describe their behavior. Accessor properties cannot be defined directly; object.defineProperty () must be used.

  • [[64x]] : Indicates whether a property can be deleted and redefined through delete, and whether its features can be modified, and whether it can be changed to a data property. By default, this property is true for all attributes directly defined on an object.
  • [[Enumerable]] : Indicates whether a property can be returned through a for-in loop. By default, this property is true for all attributes directly defined on an object.
  • [[Get]] : Gets the function, called when the property is read. The default value is undefined.
  • [[Set]] : Set function, called when writing properties. The default value is undefined
// Define an object containing pseudo-private members year_ and public members edition
let book = { 
  year_: 2017.edition: 1 
}; 
 
Object.defineProperty(book, "year",  
  {   
    get() {  
      return this.year_;   
    },   
    set(newValue) {   
      if (newValue > 2017) {      
        this.year_ = newValue;       
        this.edition += newValue - 2017; }}}); book.year =2018;
console.log(book.edition); / / 2
Copy the code

8.1.2 Defining multiple Properties

It is very possible to define multiple properties on an object at the same time. For this purpose, ECMAScript provides the object.defineProperties () method. This method can define multiple attributes at once with multiple descriptors. It takes two parameters: the object to which attributes are to be added or modified, and another descriptor object whose attributes correspond to the attributes to be added or modified.

let book = {}; 
Object.defineProperties(book, 
  {   
    year_: {     value: 2017   }, 
 
    edition: {     value: 1   }, 
 
    year: {     
      get() {       return this.year_;     }, 
    
      set(newValue) {      
        if (newValue > 2017) {     
          this.year_ = newValue;    
          this.edition += newValue - 2017; }}}});Copy the code

8.1.3 Reading properties

The use of the Object. GetOwnPropertyDescriptor () method can obtain the attributes of the specified descriptor. This method takes two parameters: the object of the property and the name of the property whose descriptor is to be retrieved. The return value is an object containing different, Enumerable, GET, and set properties for the accessor property, and any data property containing different, Enumerable, Writable, and value.

let book = {};
Object.defineProperties(book, 
  {   
    year_: {     value: 2017   }, 
 
    edition: {     value: 1   }, 
 
    year: {    
      get: function() {       return this.year_;     }, 
 
      set: function(newValue){     
        if (newValue > 2017) {   
          this.year_ = newValue;     
          this.edition += newValue - 2017; }}}});let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value);          / / 2017
console.log(descriptor.configurable);   // false 
console.log(typeof descriptor.get);     // "undefined" 
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value);          // undefined 
console.log(descriptor.enumerable);     // false
console.log(typeof descriptor.get);     // "function" 
Copy the code

The ECMAScript 2017 new Object. GetOwnPropertyDescriptors () static method. This method will actually call Object on each has its own properties. The getOwnPropertyDescriptor () and return them in a new Object.

console.log(Object.getOwnPropertyDescriptors(book));
/ / {
// edition: {
// configurable: false,
// enumerable: false,
// value: 1,
// writable: false
/ /},
// year: {
// configurable: false,
// enumerable: false,
// get: f(),
// set: f(newValue),
// }, // year_: {
// configurable: false,
// enumerable: false,
// value: 2017,
// writable: false
/ /}
// } 
Copy the code

8.1.4 Merging Objects

ECMAScript 6 provides the object.assign () method specifically for merged objects. This method takes a target object and one or more source objects as parameters, Then each source objects can be enumerated (Object) propertyIsEnumerable () returns true) and has its own (Object. The hasOwnProperty () returns true) attributes are copied to the target Object. Properties with strings and symbols as keys are copied. For each qualifying attribute, the method uses [[Get]] on the source object to Get the value of the attribute, and then uses [[Set]] on the target object to Set the value of the attribute.

Object.assign() actually performs a shallow copy of each source Object. If multiple source objects have the same property, the value of the latter copy is used. In addition, the value obtained from the source object accessor property, such as the fetch function, is assigned to the target object as a static value. In other words, you cannot transfer the get and set functions between two objects.

If an error occurs during assignment, the operation is aborted and exits, throwing an error. Object.assign() has no concept of “rolling back” previous assignments, so it is a method that does its best and may only do a partial copy.

8.1.5 Object identification and equality determination

Object.is() is similar to ===, but also allows for boundary cases. This method must accept two arguments:

console.log(Object.is(true.1));  // false 
console.log(Object.is({}, {}));   // false 
console.log(Object.is("2".2));   // false 
 
// Correct 0, -0, +0 are equal/unequal
console.log(Object.is(+0, -0));   // false
console.log(Object.is(+0.0));    // true 
console.log(Object.is(-0.0));    // false 
 
// Correct NaN equality determination
console.log(Object.is(NaN.NaN)); // true 
Copy the code

8.1.6 Enhanced object syntax

ECMAScript 6 adds a number of extremely useful syntactic sugar features for defining and manipulating objects. None of these features change the behavior of existing engines, but they greatly improve the ease of handling objects.

1. Shorthand for attribute values

Shorthand property names are automatically interpreted as property keys with the same name simply by using the variable name (no colons are needed). If no variable with the same name is found, a ReferenceError is raised.

let name = 'Matt';  
 
let person = {   name }; 
 
console.log(person); // { name: 'Matt' } 
Copy the code

2. Computable properties

Before the introduction of computable properties, if you wanted to use the value of a variable as an attribute, you had to declare the object and then add the attribute using the parenthesis syntax. In other words, attributes cannot be named dynamically directly in object literals.

const nameKey = 'name';  
const ageKey = 'age'; 
const jobKey = 'job'; 
 
let person = {};
person[nameKey] = 'Matt'; 
person[ageKey] = 27; 
person[jobKey] = 'Software engineer'; 
 
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' } 


// Because they are evaluated as JavaScript expressions, computable attributes can themselves be complex expressions that are evaluated at instantiation time
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job'; 
let uniqueToken = 0; 
 
function getUniqueKey(key) {  
  return `${key}_${uniqueToken++}`;
} 
 
let person = {   
  [getUniqueKey(nameKey)]: 'Matt',
  [getUniqueKey(ageKey)]: 27,  
  [getUniqueKey(jobKey)]: 'Software engineer' 
}; 
 
console.log(person);  // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' } 
Copy the code

3. Abbreviate method name

When defining methods on objects, you usually write a method name, a colon, and a reference to an anonymous function expression. The syntax of the new shorthand method follows the same pattern, but the developer has to give up naming function expressions (although naming functions as methods usually doesn’t work). This, in turn, can significantly shorten method declarations.

// Normal declaration:
let person = { 
  sayName: function(name) {   
    console.log(`My name is ${name}`); }};/ / abbreviations:
let person = {
  sayName(name) {    
    console.log(`My name is ${name}`); }};// The shorthand method name is also available for fetching and setting functions:
let person = {   
  name_: ' '.get name() {   
    return this.name_;   
  },  
  set name(name) {   
    this.name_ = name; 
  },  
  sayName() {    
    console.log(`My name is The ${this.name_}`); }};Copy the code

8.1.7 Object Deconstruction

ECMAScript 6 adds object destructuring syntax that enables one or more assignment operations using nested data in a single statement. Simply put, object deconstruction is the assignment of object attributes using structures that match the object.

// Use object destructuring
let person = {   name: 'Matt'.age: 27 }; 
 
let { name: personName, age: personAge } = person; 
 
console.log(personName);  // Matt
console.log(personAge);   / / 27
Copy the code

Destructuring allows you to declare multiple variables and perform multiple assignments simultaneously in a structure that resembles an object literal. If you want the variable to use the name of the attribute directly, you can use the shorthand syntax, and the destruct assignment may not necessarily match the attribute of the object. Some attributes can be ignored when assigning, and the value of the variable is undefined if the referenced attribute does not exist:

let person = {   name: 'Matt'.age: 27 }; 
 
let { name, job } = person; 
 
console.log(name);  // Matt
console.log(job);   // undefined 
// You can also define default values while deconstructing assignments
let { name, job='Software engineer' } = person; 
Copy the code

Destructuring does not require variables to be declared in destructuring expressions. However, if you are assigning to a previously declared variable, the assignment expression must be enclosed in a pair of parentheses:

let personName, personAge; 
 
let person = {   name: 'Matt'.age: 27 }; 
 
({name: personName, age: personAge} = person); 
 
console.log(personName, personAge); // Matt, 27 
Copy the code

1. Nested deconstruction

Destructuring has no restrictions on referencing nested properties or assignment targets. To do this, object attributes can be copied by destructuring:

let person = {   
  name: 'Matt'.age: 27.job: {    
    title: 'Software engineer'}};let personCopy = {}; 
 
 
({   name: personCopy.name,   age: personCopy.age,   job: personCopy.job } = person); 
 
// Since a reference to an object is assigned to personCopy, modifying the attributes of the // Person.job object also affects personCopy person.job. Title = 'Hacker'
 
console.log(person); // { name: 'Matt', age: 27, job: { title: 'Hacker' } } 
 
console.log(personCopy); // { name: 'Matt', age: 27, job: { title: 'Hacker' } } 
Copy the code

2. Partial deconstruction

A deconstructive assignment involving multiple attributes is an output-independent sequential operation. If a deconstructed expression involves multiple assignments and the initial assignment succeeds and the subsequent assignment fails, then only part of the deconstructed assignment will be completed:

let person = { 
  name: 'Matt'.age: 27 
}; 
 
let personName, personBar, personAge; 
 
try {   // Person. foo is undefined, so an error will be thrown
  ({name: personName, foo: { bar: personBar }, age: personAge} = person);
} catch(e) {

} 
 
console.log(personName, personBar, personAge); // Matt, undefined, undefined 
Copy the code

3. The parameter context matches

Destructuring assignments can also be done in function argument lists. Destructuring assignments to arguments does not affect the arguments object, but can be declared in the function signature to use local variables in the function body:

let person = {   name: 'Matt'.age: 27 }; 
 
function printPerson(foo, {name, age}, bar) {  
  console.log(arguments);   
  console.log(name, age);
} 
 
function printPerson2(foo, {name: personName, age: personAge}, bar) { 
  console.log(arguments);  
  console.log(personName, personAge);
} 
 
printPerson('1st', person, '2nd'); // ['1st', { name: 'Matt', age: 27 }, '2nd'] // 'Matt', 27 
 
printPerson2('1st', person, '2nd'); // ['1st', { name: 'Matt', age: 27 }, '2nd'] // 'Matt', 27 
Copy the code

8.2 Creating An Object

While it is easy to create objects using Object constructors or Object literals, these methods have a significant disadvantage: creating multiple objects with the same interface requires a lot of repetitive code.

8.2.1 overview

ECMAScript 6 officially supports classes and inheritance. The classes in ES6 are designed to fully cover the prototype-based inheritance pattern designed by the previous specification. For all intents and purposes, however, ES6 classes are just syntactic sugar that encapsulates ES5.1 constructors plus stereotype inheritance.

8.2.2 Factory Mode

The factory pattern is a well-known design pattern widely used in software engineering to abstract the process of creating specific objects.

function createPerson(name, age, job) { 
  let o = new Object(a); o.name = name; o.age = age; o.job = job; o.sayName =function() {    
    console.log(this.name);   
  }; 
  return o; 
} 
 
let person1 = createPerson("Nicholas".29."Software Engineer"); 
let person2 = createPerson("Greg".27."Doctor"); 
Copy the code

8.2.3 Constructor pattern

Custom constructors that define properties and methods for their own object types in the form of functions.

function Person(name, age, job){ 
  this.name = name;  
  this.age = age;  
  this.job = job;   
  this.sayName = function() {   
    console.log(this.name); 
  };
} 
 
let person1 = new Person("Nicholas".29."Software Engineer"); 
let person2 = new Person("Greg".27."Doctor"); 
 
person1.sayName();  // Nicholas
person2.sayName();  // Greg 
Copy the code

In this example, the Person() constructor replaces the createPerson() factory function. In fact, the code inside Person() is basically the same as that inside createPerson(), with the following differences.

  • No object is explicitly created.
  • Properties and methods are assigned directly to this.
  • There is no return

By convention, constructor names begin with a capital letter, and non-constructors begin with a lowercase letter. This is borrowed from object-oriented programming languages and helps distinguish constructors from ordinary functions in ECMAScript. After all, ECMAScript constructors are functions that create objects.

To create an instance of Person, use the new operator. Calling the constructor in this way does the following.

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

Constructors need not be written as function declarations. Function expressions assigned to variables can also represent constructors:

let Person = function(name, age, job) {   
  this.name = name;   
  this.age = age;   
  this.job = job;   
  this.sayName = function() { 
    console.log(this.name);  
  };
} 
 
let person1 = new Person("Nicholas".29."Software Engineer"); 
let person2 = new Person("Greg".27."Doctor"); 
 
person1.sayName();  // Nicholas
person2.sayName();  // Greg 
 
console.log(person1 instanceof Object);  // true
console.log(person1 instanceof Person);  // true
console.log(person2 instanceof Object);  // true 
console.log(person2 instanceof Person);  // true 
Copy the code

1. Constructors are functions

The only difference between a constructor and a normal function is how it is called. In addition, constructors are functions. There is no special syntax for defining a function as a constructor. Any function called with the new operator is a constructor, and any function called without the new operator is a normal function.

// as a constructor
let person = new Person("Nicholas".29."Software Engineer");
person.sayName();    // "Nicholas" 
 
// called as a function
Person("Greg".27."Doctor");   // Add to the window object
window.sayName();    // "Greg" 
 
// call in the scope of another object
let o = new Object(a); Person.call(o,"Kristen".25."Nurse");
o.sayName();   // "Kristen" 
Copy the code

2. Constructor problems

. The main problem with constructors is that they define methods that are created on each instance. So for the previous example, person1 and person2 both have methods named sayName(), but they are not the same Function instance. As we know, functions in ECMAScript are objects, so each time a function is defined, an object is initialized. Logically, the constructor actually looks like this:

function Person(name, age, job){ 
  this.name = name;   
  this.age = age;
  this.job = job; 
  this.sayName = new Function("console.log(this.name)"); // Logical equivalence
} 
/ /. So functions on different instances are not equal even though they have the same name
//console.log(person1.sayName == person2.sayName); // false 
Copy the code

To solve this problem, move the function definition outside the constructor:

function Person(name, age, job){ 
  this.name = name;  
  this.age = age; 
  this.job = job;  
  this.sayName = sayName;
} 
 
function sayName() {  
  console.log(this.name);
} 
 
let person1 = new Person("Nicholas".29."Software Engineer"); 
let person2 = new Person("Greg".27."Doctor"); 
 
person1.sayName();  // Nicholas
person2.sayName();  // Greg 
Copy the code

8.2.4 Prototype mode

Understanding the prototype and the prototype chain is a good way to avoid looking at this.

Each function creates a Prototype property, which is an object containing properties and methods that should be shared by instances of a particular reference type. In effect, this object is a prototype of the object created by calling the constructor. The advantage of using a stereotype object is that the properties and methods defined on it can be shared by the object instance. Values originally assigned directly to object instances in constructors can be assigned directly to their prototypes

function Person() {} 
 
Person.prototype.name = "Nicholas";
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {   console.log(this.name);  }; 
 
let person1 = new Person();
person1.sayName(); // "Nicholas" 
 
let person2 = new Person(); 
person2.sayName(); // "Nicholas" 
 
console.log(person1.sayName == person2.sayName); // true 
Copy the code

1. Understand the prototype

Whenever a function is created, a Prototype attribute (pointing to the prototype object) is created for the function according to specific rules. By default, all stereotype objects automatically get a property called constructor that refers back to the construction function associated with them. For the previous example, a Person. The prototype. The constructor to the Person. Then, depending on the constructor, additional properties and methods may be added to the prototype object.

When you customize a constructor, the stereotype Object acquires only the constructor attribute by default; all other methods inherit from Object. Each time the constructor is called to create a new instance, the inner [[Prototype]] pointer of that instance is assigned to the constructor’s Prototype object. There is no standard way to access the [[Prototype]] feature in scripts; Firefox, Safari, and Chrome expose the __proto__ property on each object, which allows access to the object’s Prototype. In other implementations, this feature is completely hidden. The key is to understand that there is a direct connection between the instance and the constructor stereotype, but not between the instance and the constructor.

While not all implementations expose [[Prototype]], you can use the isPrototypeOf() method to determine this relationship between two objects. In essence, isPrototypeOf() returns true if the [[Prototype]] of the passed argument points to the object on which it was called, as follows:

console.log(Person.prototype.isPrototypeOf(person1));  // true 
console.log(Person.prototype.isPrototypeOf(person2));  // true 
Copy the code

The ECMAScript Object type has a method called Object.getProtoTypeof () that returns the value of the parameter’s internal property [[Prototype]]. Such as:

console.log(Object.getPrototypeOf(person1) == Person.prototype);  // true 
console.log(Object.getPrototypeOf(person1).name);                 // "Nicholas" 
Copy the code

The Object type also has a setPrototypeOf() method that writes a new value to the instance’s private property [[Prototype]]. This overrides the stereotype inheritance of an object:

let biped = {    numLegs: 2 }; 
let person = {   name: 'Matt' }; 
 
Object.setPrototypeOf(person, biped); 
 
console.log(person.name);                              // Matt 
console.log(person.numLegs);                           / / 2
console.log(Object.getPrototypeOf(person) === biped);  // true 
Copy the code

Object.setprototypeof () can seriously affect code performance. The Mozilla documentation makes it clear: “In all browsers and JavaScript engines, the effects of changing inheritance relationships are subtle and profound. This affects not just the execution of object.setPrototypeof () statements, but all code that accesses the [[Prototype]] modified objects.”

To avoid the potential performance degradation caused by using Object.setPrototypeof (), you can create a new Object with object.create () and specify a prototype for it:

let biped = {    numLegs: 2 };
let person = Object.create(biped);
person.name = 'Matt'; 
 
console.log(person.name);                              // Matt 
console.log(person.numLegs);                           / / 2
console.log(Object.getPrototypeOf(person) === biped);  // true 
Copy the code

2. Prototype level

When a property is accessed through an object, a search is initiated by the name of the property. The search begins with the object instance itself. If a given name is found on this instance, the value of that name is returned. If the property is not found, the search follows the pointer into the prototype object, and when the property is found on the prototype object, the corresponding value is returned. Therefore, when person1.sayName() is called, a two-step search occurs. First, the JavaScript engine asks, “Does the Person1 instance have the sayName attribute?” The answer is no. Then, go ahead and ask, “Does the prototype for Person1 have the sayName attribute?” The answer is yes. The function saved on the prototype is returned. When person2.sayname () is called, the same search process occurs and the same results are returned. This is how stereotypes are used to share properties and methods across multiple object instances.

3. Stereotypes and in operators

4. Attribute enumeration order

8.2.5 Object Iteration

ECMAScript 2017 adds two static methods for converting object content to a serialized — and more importantly, iterable — format. The two static methods Object.values() and object.entries () receive an Object and return an array of their contents. Object.values() returns an array of Object values, and object.entries () returns an array of key/value pairs.

const o = {    foo: 'bar'.baz: 1.qux: {}};console.log(Object.values(o)); 
// ["bar", 1, {}] 
 
console.log(Object.entries((o))); 
// [["foo", "bar"], ["baz", 1], ["qux", {}]] 


// Note that non-string attributes are converted to string output. In addition, these two methods perform a shallow copy of the object:
const o = {    qux: {}};console.log(Object.values(o)[0] === o.qux); // true 
 
console.log(Object.entries(o)[0] [1] === o.qux); // true


// Symbolic attributes are ignored:
const sym = Symbol(a);const o = {    [sym]: 'foo' }; 
 
console.log(Object.values(o)); / / []
 
console.log(Object.entries((o))); / / []
Copy the code

1. Other prototype syntax

In the previous example, person. prototype was rewritten each time an attribute or method was defined. To reduce code redundancy and to visually encapsulate prototype functionality, it is common practice to rewrite a prototype directly through an object literal that contains all properties and methods.

function Person() {} 
 
Person.prototype = {  
  name: "Nicholas".age: 29.job: "Software Engineer".sayName() {     console.log(this.name); }};Copy the code

In this example, Person.prototype is set to equal a new object created from an object literal. The end result is the same, except for one problem: after this rewriting, the Constructor property of Person.prototype no longer points to Person. When a function is created, its Prototype object is also created and its constructor property is automatically assigned. This overrides the default Prototype Object, so its constructor property also points to a completely different new Object (Object constructor) than the original constructor. While the instanceof operator can still reliably return values, we can no longer rely on the constructor attribute to identify types.

let friend = new Person(); 
 
console.log(friend instanceof Object);      // true
console.log(friend instanceof Person);      // true 
console.log(friend.constructor == Person);  // false 
console.log(friend.constructor == Object);  // true
Copy the code

If constructor’s value is important, you can set its value specifically when rewriting the prototype object as follows:

function Person() {  } 
 
Person.prototype = {  

  constructor: Person, 
  name: "Nicholas".age: 29.job: "Software Engineer".sayName() {     console.log(this.name); 
} 
Copy the code

Note, however, that restoring the constructor property this way creates an attribute that [[Enumerable]] is true. The native constructor property is not enumerable by default. Therefore, if you are using an ECMAScript compliant JavaScript engine, you might instead define the constructor property using the object.defineProperty () method:

function Person() {}  
 
Person.prototype = { 
  name: "Nicholas".age: 29.job: "Software Engineer".sayName() {     console.log(this.name); }};// Restore the constructor attribute
Object.defineProperty(Person.prototype, "constructor", {   enumerable: false.value: Person }); 
Copy the code

2. Prototype dynamics

Because the search for values from the stereotype is dynamic, any changes made to the stereotype object are reflected in the instance, even if the instance existed before modifying the stereotype. Here’s an example:

let friend = new Person(); 
 
Person.prototype.sayHi = function() {   console.log("hi");  }; 
 
friend.sayHi();   // "hi", no problem!
Copy the code

While you can add attributes and methods to your prototype at any time and immediately reflect them on all object instances, this is not the same thing as rewriting the entire prototype. Instance’s [[Prototype]] pointer, which is automatically assigned when the constructor is called, does not change even when the Prototype is changed to a different object. Rewriting the entire stereotype disconnects the original stereotype from the constructor, but the instance still references the original stereotype. Remember that instances only have Pointers to prototypes, not to constructors.

Instances created after overriding a stereotype on a constructor reference the new stereotype. Instances created before this point will still reference the original prototype.

function Person() {}  
 
let friend = new Person();
Person.prototype = {  
  constructor: Person,  
  name: "Nicholas".age: 29.job: "Software Engineer".sayName() {     console.log(this.name); }}; friend.sayName();/ / error
Copy the code

3. Native object prototypes

The stereotype pattern is important not only for custom types, but also because it is the pattern that implements all native reference types. All constructors of native reference types (Object, Array, String, and so on) define instance methods on stereotypes. For example, the sort() method on Array instances is defined on array.prototype, and the substring() method on string-wrapped objects is defined on String.prototype.

All default method references can be obtained from a prototype of a native object, and new methods can be defined for instances of a native type. Native object prototypes can be modified in the same way as custom object prototypes, so methods can be added at any time.

String.prototype.startsWith = function (text) {  
  return this.indexOf(text) === 0;  
}; 
 
let msg = "Hello world!"; 
console.log(msg.startsWith("Hello"));  // true 
Copy the code

4. Prototype problems

The prototype pattern is not without its problems. First, it weakens the ability to pass initialization parameters to constructors, resulting in all instances taking the same property values by default. While this can be inconvenient, it’s not a big problem with the prototype. The main problem with prototyping stems from its shared nature. All attributes on the stereotype are shared between instances, which is appropriate for functions. Properties that contain original values are also fine; as in the previous example, you can simply mask the properties on the prototype by adding attributes of the same name on the instance. The real problem comes from the attributes that contain the reference values.

function Person() {} 
 
Person.prototype = {  
  constructor: Person,  
  name: "Nicholas".age: 29.job: "Software Engineer".friends: ["Shelby"."Court"].sayName() {     console.log(this.name); }};let person1 = new Person(); 
let person2 = new Person(); 
 
person1.friends.push("Van"); 
 
console.log(person1.friends);  // "Shelby,Court,Van" 
console.log(person2.friends);  // "Shelby,Court,Van" 
console.log(person1.friends === person2.friends);  // true 
Copy the code

8.3 inheritance

Inheritance is a much-discussed topic in object-oriented programming. Many object-oriented languages support two types of inheritance: interface inheritance and implementation inheritance. The former inherits only method signatures; the latter inherits the actual methods. Interface inheritance is not possible in ECMAScript because functions are not signed. Implementing inheritance is the only way ECMAScript supports inheritance, and this is mostly done through a chain of stereotypes.

8.3.1 prototype chain

Ecma-262 defines the stereotype chain as the main inheritance of ECMAScript. The basic idea is to inherit properties and methods of multiple reference types through stereotypes. Review the relationship between constructors, stereotypes, and instances: each constructor has a stereotype object, the stereotype has a property that points back to the constructor, and the instance has an internal pointer to the stereotype. What if the stereotype is an instance of another type? That means that the stereotype itself has an internal pointer to another stereotype, which in turn has a pointer to another constructor. This creates a chain of stereotypes between the instance and the stereotype. This is the basic idea of a prototype chain.

function SuperType() {   this.property = true;  } 
 
SuperType.prototype.getSuperValue = function() {   return this.property; }; 
 
function SubType() {   this.subproperty = false; } 
 
/ / inherit the SuperType
SubType.prototype = new SuperType(); 
 
SubType.prototype.getSubValue = function () {  return this.subproperty; }; 
 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // true 
Copy the code

Prototype chains extend the prototype search mechanism described earlier. We know that when we read a property on an instance, we first search for that property on the instance. If not, the prototype of the search instance is inherited. After inheritance is implemented through the prototype chain, the search can then inherit up, searching the prototype of the prototype. For the previous example, calling instance.getsupervalue () went through a three-step search: instance, subtype.prototype, and supertype.prototype before finding the method in the last step. The search for properties and methods goes all the way to the end of the prototype chain.

1. Default prototype

In fact, there’s another link in the prototype chain. By default, all reference types inherit from Object, which is also implemented through the stereotype chain. The default prototype of any function is an instance of Object, which means that the instance has an internal pointer to Object.prototype. This is why custom types can inherit all default methods, including toString() and valueOf().

2. Relationship between prototype and inheritance

The relationship between prototypes and instances can be determined in two ways. The first is to use the instanceof operator, which returns true if the corresponding constructor appears in an instance’s prototype chain.

console.log(instance instanceof Object);     // true 
console.log(instance instanceof SuperType);  // true
console.log(instance instanceof SubType);    // true 
Copy the code

The second way to determine this relationship is to use the isPrototypeOf() method. This method can be called by each prototype in the prototype chain, and returns true as long as the prototype chain contains the prototype, as shown in the following example:

console.log(Object.prototype.isPrototypeOf(instance));     // true
console.log(SuperType.prototype.isPrototypeOf(instance));  // true 
console.log(SubType.prototype.isPrototypeOf(instance));    // true 
Copy the code

3. About methods

Subclasses sometimes need to override methods of the parent class, or add methods that the parent class does not have. To do this, these methods must be added to the prototype after it has been assigned.

function SuperType() {    this.property = true; } 
 
SuperType.prototype.getSuperValue = function() {   return this.property; }; 
 
function SubType() {   this.subproperty = false; } 
 
/ / inherit the SuperType
SubType.prototype = new SuperType(); 
 
/ / new method
SubType.prototype.getSubValue = function () {   return this.subproperty; }; 
 
// Override existing methods
SubType.prototype.getSuperValue = function () {   return false; }; 
 
let instance = new SubType();
console.log(instance.getSuperValue()); // false 
Copy the code

Another important point to understand is that creating a prototype as an object literal breaks the previous prototype chain, because this is equivalent to rewriting the prototype chain.

function SuperType() {    this.property = true; } 
 
SuperType.prototype.getSuperValue = function() {   return this.property; }; 
 
function SubType() {   this.subproperty = false; } 

/ / inherit the SuperType
SubType.prototype = new SuperType(); 
 
// Add a new method through an object literal, which invalidates the previous line
SubType.prototype = {  
  getSubValue() {     return this.subproperty;   }, 
  someOtherMethod() {     return false; }};let instance = new SubType(); 
console.log(instance.getSuperValue()); / / error!
Copy the code

In this code, the prototype of a subclass is overwritten by an object literal after being assigned an instance of SuperType. The overridden stereotype is an instance of Object, not SuperType. So the original prototype chain was broken. There is no relationship between SubType and SuperType.

4. Prototype chain problems

The main problem arises when the stereotype contains reference values. As mentioned earlier when talking about stereotypes, reference values contained in stereotypes are shared across all instances, which is why attributes are usually defined in constructors rather than stereotypes. When you implement inheritance using stereotypes, the stereotype actually becomes an instance of another type. This means that an instance property becomes a stereotype property.

The second problem with stereotype chains is that subtypes cannot take arguments to the parent type’s constructor when instantiated. In fact, we cannot pass arguments into the parent class’s constructor without affecting all object instances. This, combined with the problem of including reference values in the stereotypes mentioned earlier, results in the stereotype chain being rarely used alone.

8.3.2 Embezzled constructors

To solve inheritance problems caused by stereotypes containing reference values, a technique called constructor Constructor stealing has become popular in the development community (this technique is sometimes called object spoofing or classical inheritance). The basic idea is simple: call the superclass constructor in the subclass constructor. Because functions are, after all, simple objects that execute code in a particular context, you can use the apply() and call() methods to execute constructors in the context of the newly created object.

function SuperType() {    this.colors = ["red"."blue"."green"]; } 
 
function SubType() {   
  / / inherit the SuperType
  SuperType.call(this);
} 
 
let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
 
let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green" 
Copy the code

By using the call() (or apply()) method, the SuperType constructor is executed in the context of a new object created for an instance of SubType. This is equivalent to running all the initialization code in the SuperType() function on the new SubType object. The result is that each instance has its own Colors attribute.

1. Pass parameters

One advantage of using a stolen constructor over a prototype chain is that you can pass arguments to the superclass constructor in the subclass constructor.

function SuperType(name){   this.name = name;  } 
 
function SubType() {  
  // Inherit SuperType and pass the parameter
  SuperType.call(this."Nicholas"); 
 
  // Instance properties
  this.age = 29;
} 
 
let instance = new SubType();
console.log(instance.name); // "Nicholas"; 
console.log(instance.age);  / / 29
Copy the code

2. Embezzle constructors

The main drawback of embezzling constructors is also the problem of using constructor patterns to customize types: methods must be defined in constructors, so functions cannot be reused. In addition, subclasses do not have access to methods defined on the parent class prototype, so all types can only use the constructor pattern. Because of these problems, the stolen constructor is basically not used alone.

8.3.3 Combination Inheritance

Combinatorial inheritance (sometimes called pseudo-classical inheritance) combines the best of both archetypal chains and stolen constructors. The basic idea is to use stereotype chains to inherit properties and methods on stereotypes, and to inherit instance properties by stealing constructors. This allows methods to be defined on prototypes for reuse and allows each instance to have its own attributes.

function SuperType(name){  
  this.name = name;  
  this.colors = ["red"."blue"."green"]; 
} 
 
SuperType.prototype.sayName = function() {   console.log(this.name); }; 
 
function SubType(name, age){  
  // Inherit attributes
  SuperType.call(this, name); 
  this.age = age;
} 
 
// Inheritance method
SubType.prototype = new SuperType(); 
 
SubType.prototype.sayAge = function() {
  console.log(this.age); 
}; 
 
let instance1 = new SubType("Nicholas".29); 
instance1.colors.push("black"); 
console.log(instance1.colors);  // "red,blue,green,black" 
instance1.sayName();            // "Nicholas";
instance1.sayAge();             / / 29
 
let instance2 = new SubType("Greg".27);
console.log(instance2.colors);  // "red,blue,green" 
instance2.sayName();            // "Greg";
instance2.sayAge();             / / 27
Copy the code

In this example, the SuperType constructor defines two attributes, name and colors, and its prototype also defines a method called sayName(). The SubType constructor calls the SuperType constructor, passing in the name argument, and then defining its own attribute, age. In addition, subtype. prototype is also assigned an instance of SuperType. After the prototype is assigned, a new method sayAge() is added to the prototype. In this way, you can create two instances of SubType that have their own attributes, including colors, and share the same methods.

Composite inheritance makes up for the shortcomings of prototype chains and stolen constructors and is the most commonly used inheritance pattern in JavaScript. Composite inheritance also preserves the ability of the instanceof operator and isPrototypeOf() method to recognize composite objects.

8.3.4 Original type inheritance

It is possible to share information between objects through prototypes even without custom types.

function object(o) {  
  function F() {} 
  F.prototype = o;  
  return new F();
} 
let person = {    
  name: "Nicholas".friends: ["Shelby"."Court"."Van"]};let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob"); 
 
let yetAnotherPerson = object(person); 
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie"); 
 
console.log(person.friends);  // "Shelby,Court,Van,Rob,Barbie" 
Copy the code

ECMAScript 5 normalizes the concept of type-inheritance by adding the object.create () method. This method takes two parameters: the object to be the prototype for the new object and, optionally, the object to define additional properties for the new object. With only one argument, object.create () has the same effect as the Object () method here:

let person = {  
  name: "Nicholas".friends: ["Shelby"."Court"."Van"]};let anotherPerson = Object.create(person); 
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob"); 
 
let yetAnotherPerson = Object.create(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
 
console.log(person.friends);  // "Shelby,Court,Van,Rob,Barbie" 
Copy the code

The second argument to Object.create() is the same as the second argument to Object.defineProperties() : each new property is described by its own descriptor. Attributes added in this way overshadow attributes of the same name on the prototype object.

let person = {   
  name: "Nicholas".friends: ["Shelby"."Court"."Van"]};let anotherPerson = Object.create(person, {   name: {     value: "Greg"}});console.log(anotherPerson.name);  // "Greg" 
Copy the code

Old-style inheritance is ideal for situations where you don’t need to create a separate constructor, but you still need to share information between objects. Keep in mind, however, that reference values contained in attributes are always shared between related objects, just as with the stereotype pattern.

8.3.5 Parasitic inheritance

The idea behind parasitic inheritance is similar to the parasitic constructor and factory pattern: Create a function that implements inheritance, enhance an object in some way, and then return that object.

function createAnother(original){ 
  let clone = object(original);  Create a new object by calling the function
  clone.sayHi = function() {     // Enhance the object in some way
    console.log("hi");   
  };   
  return clone;           // Return this object
} 
let person = { 
  name: "Nicholas".friends: ["Shelby"."Court"."Van"]};let anotherPerson = createAnother(person);
anotherPerson.sayHi();  // "hi" 
Copy the code

Parasitic inheritance is also suitable for scenarios where the main focus is on objects, not types and constructors. The object() function is not required for parasitic inheritance, and any function that returns a new object can be used here.

8.3.6 Parasitic combination inheritance

Combinatorial inheritance also has efficiency problems. The main efficiency issue is that the superclass constructor is always called twice: once when the subclass stereotype is created and once in the subclass constructor. Essentially, the subclass stereotype will eventually contain all of the instance properties of the superclass object, and the subclass constructor simply overrides its own stereotype at execution time.

Parasitic combinatorial inheritance inherits attributes by stealing constructors, but uses a hybrid prototype chain inheritance approach. The basic idea is that instead of assigning a value to a subclass prototype by calling the superclass constructor, you get a copy of the superclass prototype. It boils down to using parasitic inheritance to inherit the parent prototype and then assigning the returned new object to the child prototype.

function inheritPrototype(subType, superType) {  
  let prototype = object(superType.prototype);  // Create an object
  prototype.constructor = subType;              // Enhance objects
  subType.prototype = prototype;                // Assign an object
}
function SuperType(name) {
  this.name = name;   
  this.colors = ["red"."blue"."green"]; 
} 
 
SuperType.prototype.sayName = function() { 
  console.log(this.name);
}; 
 
function SubType(name, age) {  
  SuperType.call(this, name); 
   this.age = age; 
  } 
 
inheritPrototype(SubType, SuperType); 
 
SubType.prototype.sayAge = function() {   console.log(this.age); }; 
Copy the code

The SuperType constructor is called only once, avoiding unnecessary and unnecessary attributes on subtype. prototype, so this example is arguably more efficient. Moreover, the prototype chain remains unchanged, so the instanceof operator and isPrototypeOf() methods work properly. Parasitic combinatorial inheritance is the best model for reference type inheritance.

8.4 class

The new class keyword introduced in ECMAScript 6 has the ability to formally define classes. Classes are the new basic syntactic sugar structure in ECMAScript, so you might not be comfortable with them at first. While the ECMAScript 6 class may appear to support formal object-oriented programming, the concepts of stereotypes and constructors are still behind it.

8.4.1 class definition

Like function types, there are two main ways to define classes: class declarations and class expressions. Both methods use the class keyword in 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. Another difference from function declarations is that functions are scoped by functions, while classes are scoped by blocks.

The composition of the class

Classes can contain constructor methods, instance methods, get functions, set functions, and static class methods, but none of these are required. An empty class definition is still valid. By default, the code in a class definition is executed in strict mode. The name of the class expression is optional. After assigning a class expression to a variable, the name string of the class expression can be obtained through the name attribute. This identifier cannot be accessed outside the scope of the class expression

// Empty class definition, valid
class Foo {} 
 
// class with constructor, valid
class Bar {   constructor(){}}// The class that gets the function is valid
class Baz {   get myBaz() {}}// Class with static methods, valid
class Qux {   static myQux(){}}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

8.4.2 Class constructor

The constructor keyword is used to create class constructors within the class definition block. The method name constructor tells the interpreter to call this function when using the new operator to create a new instance of a class. The definition of a constructor is not required, and not defining a constructor is equivalent to defining a constructor as an empty function.

1. The instantiation

Instantiating a Person with the new operator is equivalent to calling its constructor with new. The only appreciable difference is that the JavaScript interpreter knows that using new and classes means instantiation should be done using the constructor function. Calling the class constructor with new does the following.

  1. Create a new 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 (that is, this refers to the new object).
  4. Executes the code inside the constructor (adding attributes to the new object).
  5. If the constructor returns a non-empty object, that object is returned; Otherwise, the newly created object is returned.
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. If no arguments are required, the parentheses following the class name are optional:

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 nothing references the newly created this object, it is destroyed. However, if you return an object other than this, 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 class constructors must be called using the new operator. A normal constructor without a new call takes the global this (usually window) as its internal object. An error is thrown if you forget to use new when calling a class constructor.

There is nothing special about a class constructor; once instantiated, it becomes a normal instance method (but as a class constructor, you still use the new call). Therefore, it can be referenced on the instance after instantiation

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

2. Think of classes as special functions

ECMAScript has no formal class type. The ECMAScript class is a special kind of function in many ways. Once a class is declared, the class identifier is detected through the Typeof operator to indicate that it is a function. The class identifier has a Prototype attribute, which also has a constructor attribute pointing to the class itself. As with normal constructors, you can use the instanceof operator to check whether the constructor stereotype exists in the stereotype chain of the instance.

class Person {} 
 
console.log(Person);         // class Person {} 
console.log(typeof Person);  // function 
console.log(Person.prototype);                         // { constructor: f() }
console.log(Person === Person.prototype.constructor);  // true 


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

As mentioned earlier, the class itself has the same behavior as a normal constructor. In the context of a class, the class itself is treated as a constructor when called with new. Importantly, the constructor method defined in the class is not treated as a constructor and returns false when the instanceof operator is used on it. However, if the class constructor is used as a normal constructor when creating an instance, the return value of the instanceof operator is reversed:

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 they can be passed as arguments just like any other object or function reference

Classes can be defined anywhere like functions, such as in arrays
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 function expressions immediately, classes can be instantiated immediately:

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

8.4.3 Instances, stereotypes, and class members

1. Instance member

The class constructor is executed each time the class identifier is called through new. Inside this function, you can add “own” attributes to the newly created instance (this). There are no restrictions on what attributes to add. In addition, you can continue to add new members to the instance after the constructor completes execution. Each instance corresponds to a unique member object, which means that none of the members are shared on the stereotype:

class Person {  
  constructor() {    
    // This example starts by defining a string using the object wrapper type
    // To test the equality of the 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 methods are equivalent to object properties, so they can use strings, symbols, or computed values as keys. Class definitions also support getting and setting accessors. The syntax and behavior are the same as normal objects.

class Person {   
  constructor() {    
  // Everything added to this will exist on different instances
  this.locate = () = > console.log('instance');  
  } 
   // Everything defined in a class block is defined in the prototype of the class
  locate() {    
     console.log('prototype'); }}let p = new Person(); 
 
p.locate();                 // instance 
Person.prototype.locate();  // prototype 
Copy the code

Static class methods

Static methods can be defined on a class. These methods are typically used to perform operations that are not instance-specific and do not require the existence of an instance of the class. Like prototype members, static members can only have one on each class. Static class members are prefixed with the static keyword in the class definition. In static members, this refers to the class itself. All other conventions are the same as the prototype members. Static class methods are well suited 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-function archetypes 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 the class definition

Iterator and generator methods

Class definition syntax supports defining generator methods on both prototypes and classes themselves:

class Person {    
  // Define generator methods on prototypes
  *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

8.4.4 inheritance

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

1. Inheritance basis

ES6 classes support single inheritance. Using the extends keyword, you can inherit any object that has [[Construct]] and a stereotype. For the most part, this means that you can inherit not only from a class, but also from ordinary constructors (to remain backward compatible) :

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

2. Constructors, HomeObject, and super()

Methods of derived classes can reference 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 the class constructor calls 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

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

There are a few things to note when using super.

  • Super can only be used in derived class constructors and static methods.
  • You cannot refer to the super keyword alone, either by calling the constructor or by referring to static methods.
  • Calling super() calls the superclass constructor and assigns the returned instance to this.
  • Super () behaves like a constructor call; if you need to pass arguments to the parent constructor, you need to pass them manually.
  • If the class constructor is not defined, super() is called when the derived class is instantiated and all arguments passed to the derived class are passed.
  • In class constructors, you cannot refer to this before calling super().
  • If you explicitly define a constructor in a derived class, you must either call super() in it or return an object in it.

Abstract base classes

Sometimes you might want to define a class that can be inherited by other classes but will not be instantiated itself. Although ECMAScript does not specifically support the syntax for this class, it is easily implemented through new.target. New.target holds classes or functions called through the new keyword. You can prevent instantiation of an abstract base class by detecting whether new.target is an abstract base class at instantiation time. Alternatively, by checking in the abstract base class constructor, you can require that a derived class must define a method. Because the prototype method exists before the class constructor is called, the corresponding method can be checked with 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 built-in types

The ES6 class provides a smooth mechanism for inheriting built-in reference types, and developers can 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 that of the original instance. 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 different classes of behavior into a single class. While ES6 does not explicitly support multi-class inheritance, this behavior can be easily modeled 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 want to mix in multiple Object attributes, 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 is 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 you need some mechanism to combine A, B, and C into the superclass by having B inherit from A, C inherit from B, and Person inherit from C. There are different strategies for implementing this pattern.

8.5 summary

Objects can be created and enhanced at any time during code execution, are highly dynamic, and are not strictly defined entities. The following pattern applies to creating objects.

  • The factory pattern is a simple function that creates an object, adds properties and methods to it, and then returns the object. This pattern has rarely been used since the advent of the constructor pattern.
  • You can use the constructor pattern to customize reference types, and you can use the new keyword to create instances of custom types just as you create instances of built-in types. However, the constructor pattern has its drawbacks, mainly that its members cannot be reused, including functions. Given that functions themselves are loose and weakly typed, there is no reason why functions cannot be shared across multiple object instances.
  • The prototype pattern solves the problem of member sharing, as long as properties and methods added to the prototype constructor are shared. Composite constructors and stereotype patterns define instance properties through constructors and shared properties and methods through stereotypes.

JavaScript inheritance is implemented primarily through prototype chains. Stereotype chains involve assigning the stereotype of a constructor to an instance of another type. In this way, subclasses have access to all the attributes and methods of their parent class, just like class-based inheritance. The problem with stereotype chains is that all inherited properties and methods are shared between object instances and cannot be made private. The stolen constructor pattern avoids this problem by calling the superclass constructor in the subclass constructor. This allows each instance to inherit properties that are private, but requires that the type be defined only through the constructor schema (because subclasses cannot access methods on their parent class’s prototype). A popular inheritance pattern is composite inheritance, which inherits shared properties and methods through stereotype chains and instance properties through stolen constructors. In addition to the above patterns, there are several inheritance patterns.

  • Primitive inheritance can be implemented without explicitly defining a constructor, essentially making a shallow copy of a given object. The results of this operation can be further enhanced later.
  • Closely related to primitive inheritance is parasitic inheritance, which creates a new object based on an object, enhances the new object, and returns the new object. This pattern is also used in composite inheritance to avoid waste caused by repeated calls to superclass constructors.
  • Parasitic combinatorial inheritance is considered to be an effective way to implement type-based inheritance.

ECMAScript 6’s new classes are largely based on syntactic sugar from the existing prototype mechanism. The syntax of classes allows developers to elegantly define backward compatible classes that can inherit from both built-in and custom types. Classes effectively bridge the gap between object instances, object prototypes, and object classes.

References:

JavaScript Advanced Programming (Version 4)