3w long text takes you from JS objects all the way to classes

ECMA-262 defines an object as an unordered set of attributes. We can think of objects in JS as a hash list, where the content is a set of key-value pairs, and the type of value can be data or function.

1. Understanding Objects

The usual way to create a custom object is to create a new instance of a new Object, then add properties and methods to that instance.

let person = new Object();
person.name = 'Macc';
person.age = 18;
person.sayHi = function(){
    console.log('hi');
}

⭐ More popular now is defining objects literally

//Object Literal
let person = {
    name:'Macc',
    age:16,
    sayHi:function(){
        console.log('hi');
    }
}

The attributes and methods of the above two objects are the same and can be considered equivalent (note that they are equivalent rather than the same or the same object).

Objects have their own properties that determine their behavior in JS.

(1) Types of attributes

ECMA-262 describes the characteristics of attributes with some internal characteristics. Since they are internal, that is, they are not directly accessible by developers in JS.

The specification identifies an attribute as an internal attribute by enclosing its name in two square brackets. For example: [[Enumberable]]

Objects have two kinds of properties: data properties and accessor properties.

1. Data Properties

Data attributes have four attributes that describe their behavior

Attribute NameEffectDefault value
[[Configurable]]Indicates whether attributes can be deleted and redefined by delete, whether attributes of attributes can be modified, and whether attribute types can be changed to accessor attributes.true
[[Enumberable]]Whether the property can be returned through a for-in loop.true
[[Writable]]Whether the value of an attribute can be modified.true
[[value]]Where the actual value of a property is stored, and where it is read and written.undefined

Modify default (internal) attributes

As mentioned above, developers cannot directly access internal features in js. To modify the internal properties of attributes, you must use Object.defineProperty() method.

Object.defineProperty(obj,'propertyName', decriptionObj) method
Parameter NameParameter typedescribe
objObjectObject to add/modify properties
propertyNameStringThe name of the property to be added or modified
decriptionObjObjectdescriptor object

An attribute on a descriptor object can contain four internal attribute names (that is, the name of the internal attribute is the attribute name).

let person = {};//Define a person object
//Add Properties to Objects
Object.defineProperty(person,'name',{
    writable:false, //Property value is not modifiable
    value:'Macc'  //Actual value of property
});
console.log(person.name);//Access property values, output'Macc'
person.name = 'GaGa';//Attempt to modify the value of a property
console.log(person.name);//Output'Macc'

Since the internal property writable of the name property of the person object was changed to false, indicating that the value of the property cannot be modified, subsequent attempts to change it to GaGa Ga ignore the modification behavior (errors occur in strict mode) and still output the original value.

If the property's configurable property is set to false, it cannot be changed back to true and Object is called again. DefneProperty() method and modifying any non-writable properties will cause an error.

Call Object. When defineProperty (), the values of configurable, enumerable, and writable default to false if not specified.

let person = {
    name: 'Macc',
    age: 18
}
Object.defineProperty(person, 'hair', {
    value: 'black',
    //No other value specified, other values default to false
});

let _p = Object.getOwnPropertyDescriptor(person, 'hair');
console.log(_p);

2. Accessor Properties

Accessor properties do not contain data values.

The accessor property contains a get function getter and a set function setter.

  • When reading an accessor property, a getter is called, and it is the responsibility of the getter to return a valid value.
  • When an accessor property is written, a setter is called and a new value is passed in, and the setter decides what to change to the data (the value of the property).

Accessor properties also have four attributes describing their behavior

Attribute NameEffectDefault value
[[Configurable]]Indicates whether attributes can be deleted and redefined by delete, whether attributes can be modified, and whether attribute types can be changed to data attributes.true
[[Enumberable]]Whether the property can be returned through a for-in loop.true
[[Get]]getter, called when reading a property.undefined
[[Set]]setter, called when a property is written.undefined

⭐ Object is also used to modify accessor properties. DefineProperty() method.

let book = {
    year_: 2017,//Private Members
    edition: 1  //Public Members
}
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); //Output 2

The code above is a typical scenario for accessor properties: setting the value of a property can cause some other changes to occur.

Both getter s and setter s need not be defined:

  • Defining a getter only means that the property is read-only and attempts to modify the property are ignored.
  • Only defining a setter to read an attribute in non-strict mode returns undefined.

There were no Objects before ES5. DefineProperty() method.

3. Define multiple attributes simultaneously

Use Object.defineProperties(obj,descriptionObj) method

Parameter NameParameter typedescribe
objObjectObject to add/modify properties
decriptionObjObjectdescriptor object
let person = {}
Object.defineProperties(person, {
    name: {
        value: 'Macc'
    },
    age: {
        value: 18
    },
    hair: {
        get() {
            return 'black';
        },
        set(newValue) {
            //....
        }
    }
});

4. Features of reading attributes

4.1 Read an attribute of an attribute

Use method Object. GetOwnPropertyDescriptor (the object in which the property is located, the name of the property), which return s an object.

4.2 Characteristic of reading all of an object's own properties

Use method Object. GetOwnPropertyDescriptors (Object), which also return s an object that includes the attributes of all its own properties of the specified object, or an empty object if the object has no attributes.

This method actually calls Object on each of its own properties. The getOwnPorpertyDescriptor() method and returns them in a new object.

(2) Consolidated objects

Copying the desired local attributes of the source object together onto the target object is called merge, or mixin.

The method used to merge objects is Object. Assign (target object, source object 1,..., source object n).
This method copies all enumerable and self-contained attributes from the source object to the target object.

The so-called enumerable property refers to the call to Object.prpertyIsEnumerable() returns the property of true;
The so-called own property refers to the call to Object.hasOwnProperty() returns the property of true;

During copying, property values are obtained using [[Get]] on the source object and set using [[Set]] on the target object.

Object.assign() performs a shallow copy, copying only references to objects.

If multiple source objects have the same attributes, use the last replicated value (that is, which source object will use later) (override)

Getter and setter functions cannot be transferred between two objects. Values derived from the source object accessor properties, such as the getter function, are assigned to the target object as a static value.

If an error occurs during assignment, the operation is aborted and exited with an error. However, this method does not roll back; it is a best-effort method that may only partially replicate.

(3) Identification of objects and determination of equality

Prior to ES6, there were cases where using the full equals (===) could not help:

  1. As expected
ExpressionResult
true === 1false
{} === {}false
"2" === 2false

2. Different js engines behave differently but are still considered equal

ExpressionResult
+0 === -0true
+0 === 0true
-0 === 0true
  1. To determine the equality of NaNs, the isNaN function must be used
ExpressionResult
NaN === NaNfalse
isNaN(NaN)true

Method Object was added in ES6. Is(), which is similar to the equality but takes into account the above boundary conditions. This method must receive two parameters.

ExpressionResult
Object.is(+0,-0)false
Object.is(+0,0)true
Object.is(-0,0)false
Object.is(NaN,NaN)true

If you want to use Object.is() checks for more than two values, which can be recursively implemented using equality:

function recursivelyCheckEqual(x, ...rest) {
    return Object.is(x, rest[0]) &&
        (rest.length < 2 || recursivelyCheckEqual(...rest));
}

console.log(recursivelyCheckEqual(1, 2, 3, 4)); //false

(4) Enhanced Object Grammar (Grammatical Sugar)

1. Abbreviation of attribute values

Short-form attribute values are automatically interpreted as property keys of the same name as long as the variable name is used, and errors are thrown if no variable of the same name is found.

let name = 'Macc',
    age = 18;
let person = {
    name,  //Abbreviation
    //Here's how it was written
    age:age
};
console.log(person);//{name:"Macc",age:18}

2. Computable properties

Before introducing calculable attributes, if you want to use variable values as attributes (names), you must first declare objects, then add attributes using the bracket syntax. That is, you cannot directly dynamically name attributes in the literal amount of an object.

const nameKey = 'name';
let person = {};//Declare Object First
person[nameKey] = 'Macc';//Add attributes using bracket syntax

With the introduction of calculable attributes, dynamic attribute assignment can be accomplished in object literals.

let person = {
    [nameKey]:'Macc'
}

Any error thrown in a computable property expression interrupts the creation of the object and does not roll back.

const nameKey = 'Macc';
const ageKey = 'age';

let person = {
    [nameKey]: 'Macc',
    [jobKey]: 'Yanong', //Errors will occur here
    [ageKey]: 18
}
console.log(person);//This cannot be printed because the creation of the object has been interrupted.

3. Short method name

Previously, when defining a method for an object, it was in the following format:

let person = {
    //Method Name Colon Anonymous Function Expression
    sayHi:function(){
        //...
    }
}

Now it is:

let person = {
    sayHi(){
        //...
    }
}

Furthermore, the abbreviated method name is compatible with the calculable property.

const methodKey = 'sayHi';
let person = {
    [methodKey](name){
        //...
    }
}

(5) Object deconstruction

Implement one or more assignment operations in a statement using nested data.
In short, an object property is assigned using a structure that matches the object.

Matching structure: A bit of a sign-on seat.

  1. You can use abbreviation syntax
let person = {
    name:'Macc',
    job:'Yanong'
};
let {name,job} = person;
console.log(name,job);//Macc, Menon
  1. Deconstruction assignments do not necessarily match the object's attributes (some attributes can be ignored when assigning, no one-to-one correspondence is required)
let {name,age} = person;//No one-to-one correspondence is required
console.log(name,age);//"Macc",undefined
  1. Default values can be set while deconstructing assignments
let {name,age:18} = person;
console.log(name,age);//"Macc",18

Deconstruction internally uses the ToObject() method to deconstruct the source data into objects, that is, in the context of object deconstruction, the original values are treated as objects. null and undefined cannot be deconstructed, otherwise an error is reported.

let { _ } = null;//Report errors
let { _ } = undefined;//Report errors

Deconstruction does not require variables to be declared in a deconstruction expression, but if a previously declared variable is assigned a value, the expression must be enclosed in a pair of parentheses.

let personName,personAge;//Declare variables in advance
({name:personName,age:personAge} = person);

1. Nested Deconstruction

First, you can use deconstruction to copy the properties of an object:

let person = {
    name: 'Macc',
    age: 18,
};

let personCopy = {}; //Make sure you add this semicolon here, or you will get an error

({ name: personCopy.name, age: personCopy.age } = person);
console.log(personCopy);
//{name: 'Macc', age: 18}

Then think about it, what if the copied attribute is a nested structure? Is deconstruction still available? The answer is yes, a nested structure can be used for deconstruction assignments, but not when the outer properties are undefined.

let person = {
    job: {
        title: 'Yanong'
    }
};

let personCopy = {}; //Make sure you add this semicolon here, or you will get an error

let { job: { title } } = person;
console.log(title); //Yanong

//foo undefined on source object, error
({ foo: { bar: person.bar } } = person);
//job undefined on source object, error
({ job: { title: person.job.title } } = person);

2. Partial Deconstruction

If a deconstruction expression designs multiple assignments, the initial assignment succeeds and the subsequent assignment fails, the entire deconstruction assignment only partially completes.

3. Parameter Context Matching

Deconstruction assignments can also be made in the list of function parameters without affecting the arguments object.

let person = {
    name: 'Macc',
    age: 18
};

function printPerson(foo, { name, age }, bar) {
    console.log(arguments);
}
printPerson('1st', person, '2nd');

2. Creating Objects

Disadvantages of creating objects using Object constructors and object literals:
Creating multiple objects with the same interface requires a lot of code to be written repeatedly.

let person1 = {
    name:'Macc',
    age:18,
    sayHi(){
        console.log('hi');
    }
};

let person2 = {
    name:'Deing',
    age:16,
    sayHi(){
        console.log('hi');
    }
}

(1) Factory mode

This is a design pattern used to abstractly create specific objects. This pattern is described in more detail later.

function PersonFactory(){
    //Create a new object
    let o = new Object();
    //Add common properties and methods to new objects
    o.sayHi = function (){
        console.log('hi');
    }
    //return this object
    return o;
}
let person1 = PersonFactory();
let person2 = PersonFactory();

person1.sayHi();//'hi'
person2.sayHi();//'hi'

(2) Constructor mode

Constructors in JS are used to create specific types of objects. For example, if I want to get an object person1 of type Person, I use the Person constructor.

Features of the constructor pattern:

  1. No explicit creation of objects
  2. Attributes and methods are assigned directly to this
  3. No return

By convention, the first letter of the name of the constructor is capitalized.

//Person type constructor
function Person(){
    this.sayHi = function(){
        console.log('hi');
    }
}
//Create instance objects of type Person
let p1 = new Person();
let p2 = new Person;  //Parentheses can be omitted without passing arguments, and the new operator is essential.

To create an instance using the new operator, calling the constructor with new does the following:

  1. Create a new object in memory;
  2. The [[Prototype]] attribute inside the new object is assigned the prototype attribute of the constructor;
  3. The this inside the constructor is assigned to this object (changing the this direction);
  4. Execute constructor internal code (add attributes, methods to new objects);
  5. If the constructor returns a non-empty object, it returns the non-empty object, otherwise it returns the new object just created.

In the code above, p1 and p2 hold different instances of Person. Both p1 and p2 have a constructor attribute that points to Person.

console.log(p1.constructor == Person); //true

The constructor attribute is used to identify object types, but instanceof operators are generally considered more reliable.

console.log(p1 instanceof Person);//true
console.log(p1 instanceof Object);//true

All custom objects are instances of Object because all custom objects inherit from Object.

Constructors can also be written as function expressions:

let Person = function(){
    this.sayHi = function(){
        console.log('hi');
    }
}

1. Constructors are also functions

The only difference between constructors and normal functions is the way they are called.

Any function that is called with the new operator is a constructor.

⭐ This always points to a global object when a function is called without explicitly setting this value (that is, no method as an object is called or is not called with call()apply().

//This is the global scope
var name = 'Macc';
function sayHi(){
    console.log('hi,' + this.name);
}
//Call the sayHi function, note that the sayHi function here is called directly, and no method as an object is called (that is, not through xxx.sayHi())
sayHi();// 'hi,Macc'

2. Disadvantages of constructor mode

The main problem with a constructor is that the method it defines is created once on each instance. Therefore, functions on different instances have the same name but are not equal.

function Person() {
    this.hair = 'black';
    this.sayHi = function() {
        console.log('hi');
    }
}

let p1 = new Person();
let p2 = new Person();

console.log(p1.sayHi === p2.sayHi);//false

But normally, functions with the same name do the same thing, so it's not necessary to define two different Function instances.

Just like Xiao Ming and Xiao Hong (two examples), both of them have lost their keys. To find a lock master, normally only one lock master is enough. There is no need to train a lock master specially for Xiao Ming and a lock master specially for Xiao Hong.

One way to solve this problem is to transfer the definition of a function outside the constructor. Then the method property in the instance only contains a pointer to an external function, so the instance shares the function defined on the outside (generally the global scope).

function Person() {
    this.hair = 'black';
    this.sayHi = sayHi;
}

function sayHi() {
    console.log('hi');
}

let p1 = new Person();
let p2 = new Person();

console.log(p1.sayHi === p2.sayHi);//true

This idea, while resolving the problem of duplicate definitions of functions with the same logic, contaminates the global scope. Therefore, we can introduce a prototype model to better solve this problem.

(3) Prototype Model

Each function creates a prototype property, which is an object. This object contains properties and methods shared by instances of a specific reference type.

We call this object (the prototype property) the prototype of the object (instance) that we create by calling the constructor.

Properties and methods defined on the prototype object are shared by instances of the object.

1. Understanding prototypes

(1) whenever a function is created, a prototype attribute is created for the function, which points to the prototype object;

(2) By default, all prototype objects automatically get a constructor property that refers back to the constructor associated with them;

(3) When you customize a constructor, the prototype object only gets the constructor property by default, and all other methods inherit from Object;

//Create a function
function Person() {}
/*A prototype property is created for the function.
 *This property is an object, we call it a prototype, this object is a prototype object
 */
console.log(Person.prototype); //{constructor: ƒ}
//Prototype objects automatically get a constructor property by default
//The attribute refers back to the constructor associated with it
console.log(Person.prototype.constructor === Person); //true

(4) Each time a constructor is called, a new instance is created, and the [[Prototype]] pointer inside the instance is assigned to the prototype object of the constructor.

There is no standard way to access [[Prototype]] attributes in JS, but Firefox, Safari, Chrome expose u on each object proto_u Property through which the prototype of the object can be accessed.

Different instances created by the same constructor share the same prototype object.

//Create a constructor
function Person() {
    this.hair = 'black';
}

let p1 = new Person(); //Create an instance
let p2 = new Person();

//Prototype object whose [[pPrototype]] pointer inside the instance is assigned a constructor
console.log(p1.__proto__ === Person.prototype); //true

console.log(p1 === p2); //The false description is a different instance
//Different instances created by the same constructor share the same prototype
console.log(p1.__proto__ === p2.__proto__); //true

(5) The normal prototype chain terminates on the prototype object of Object, while the prototype of Object is null.

(6) The core point to understand is that there is a direct relationship between instances and constructor prototypes, but there is no relationship between instances and constructors.

I don't understand this sentence. Didn't the instance come from calling the constructor? Why is there no relationship between an instance and a constructor?

Think about what you did when you called the constructor with the new operator.

(7) The isPrototypeOf() method can be used to determine the relationship between two objects (the relationship between the instance and the constructor).

Essentially, this method returns true when the [[Prototype]] pointer of the incoming parameter points to the object calling it.

//Constructor
function Person() {}
//Create an instance
let p1 = new Person;
//Using the isPrototypeOf method
console.log(Person.prototype.isPrototypeOf(p1)); //true

⭐ (8) There is a method called Object in JS. GetPrototypeOf() returns the value of the parameter's internal attribute [[Prototype]].

console.log(Object.getPrototypeOf(p1) === Person.prototype); //true

⭐ (9) get and set often appear in pairs, so there is another method called Object.setPrototypeOf(), which writes a new value to the private attribute [[Prototype]] of the instance. This overrides the prototype chain relationship of an object.

let biped = {
    numLegs: 2
};
let person = {
    name: 'Macc'
};
Object.setPrototypeOf(person, biped);
console.log(person);
console.log(person.numLegs);

The screenshot above shows that the person object does not have a numLegs attribute, but through Object. The setPrototypeOf() method changes person's prototype object to a biped object, then follows the prototype chain and finds the numLegs attribute with an output of 2.

This method can seriously affect the performance of your code, and its impact is deep, so it is generally not recommended.

⭐ (10) If there is a problem, solve it to avoid using Object.setPrototypeOf() can cause performance degradation by using Object. The create() method creates a new object and specifies a prototype object for it.

let biped = {
    numLegs: 2
};
let person = Object.create(biped)
console.log(person); //{}
console.log(person.numLegs); //2

2. Prototype Level

This part of the word is more, but it's easy to understand.

A prototype is used to share properties and methods among multiple object instances. When accessing properties through an object, the search starts with the name of the property.

  • Search begins with the object instance itself. If a corresponding attribute is found in itself, the value of the corresponding attribute is returned and the search is stopped.
  • If not found, the search will follow the [[Prototype]] pointer into the prototype object to search, and if found, return a value;

As we can see from the above process, we can read the values on the prototype object through the instance, but the other thing is that we cannot modify/override these values through the instance. (Readable and Writable)

If an attribute with the same name as the one in the prototype object is added to the instance, it will be created on the instance, which will obscure the properties on the prototype object. (That is, as long as an attribute is added to the object instance, it will obscure the property with the same name on the shadow prototype object. Although it will not be modified, access to it will be blocked.)

Even if this property on the instance is set to null, its connection to the original cannot be restored. However, using the delete operator, you can completely delete this property on the instance and resume the search process.

2.1 hasOwnProperty()

Used to determine whether an attribute is on an instance or on a prototype object, the method inherits from Object and returns true if the attribute exists on the object instance calling it.

function Person(){}
Person.prototype.name = 'Macc';

let p1 = new Person();
p1.age = 18;

p1.hasOwnProperty('name');//false
p1.hasOwnProperty('age');//true

Here is a complement to the previous points:

Object. The getOwnPropertyDescriptor() method only works on instance properties. To get a descriptor of a prototype property, the method must be called directly on the prototype object.

3. Prototype and in operator

The in operator is used in two ways: individually and in a for - in loop.

3.1 Use alone

When used alone, in returns true when a specified property can be accessed through an object, whether it is on an instance or a prototype.

let person = {
    name:'Macc'
};

console.log('name' in person);//true

As shown below, using both hasOwnProperty() and in operators can determine whether a property exists on the prototype.

function hasPrototypeProperty(obj,name){
    return !obj.hasOwnProperty(name) && (name in obj);
}
//As long as hasOwnProperty returns false;
//in returns true
//Indicates that the property is a prototype property

I don't really understand what this function means. Didn't hasOwnProperty() alone tell whether it is an instance property or a prototype property?

3.2 for-in cycle

When used in a for-in loop, properties that can be accessed by objects and enumerated are returned, including instance and prototype properties.

Instance properties of non-enumerable attributes ([[Enumerable]] attribute is false) in the masking prototype are also returned because, by default, developer-defined attributes are enumerable attributes.

(1) To get all enumerable instance properties on an object, use Object.keys(obj) method.

  • This method takes an object as a parameter.
  • Returns an array of strings containing all enumerable property names of the object.

(2) To list all instance properties, use Object whether or not they can be enumerated. GetOwnPropertyNames (obj).

Non-enumerable property: constructor property

(3) With the addition of the Symbol type to ES6, since properties with Symbol as the key have no concept of name, Object. The getOwnPropertySymbols () method appears, which only targets Symbols.

let k1 = Symbol('k1'),
    k2 = Symbol('k2');

let o = {
   //This uses the calculable properties mentioned earlier
   [k1]: 'k1',
   [k2]: 'k2',
}

console.log(Object.getOwnPropertySymbols(o)); //[Symbol(k1), Symbol(k2)]

4. Attribute Enumeration Order

(1) for-in loop, Object. The enumeration order of keys () is indeterminate;

(2) Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), Object. The enumeration order of assign () is deterministic.

  • Enumerate numeric keys in ascending order first;
  • Then the strings and symbol keys are enumerated in insertion order;

(3) Keys defined in object literals are inserted in their comma-separated order;

How does this enumeration order reflect? Is it not the order of traversal? I'm sure with the for-in and keys methods. First the numeric keys, then the insertion order. Why?

(4) Iteration of Objects

ES6 adds two static methods for converting object content to a serialized format. These two methods are Object.values(obj) and Object.entries(obj). Both receive an object as a parameter and return an array of object contents.

  • Object.values(obj) returns an (one-dimensional) array of object values;
  • Object.entries(obj) returns a (two-dimensional) array of key-value pairs;
const o = {
    foo:'bar',
    qux:{},
    baz:1,
};

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

(1) Both methods perform shallow duplication;

const o = {
    foo: 'bar',
    obj: {
        name: 'Macc'
    }
};

let o1 = Object.values(o);
let o2 = Object.entries(o);

console.log(o1[1] === o2[1][1]); //true shallow copy

(2) Non-string attributes are converted to string output;

(3) Symbol attributes are ignored;

const sym = Symbol();
const o = {
    [sym]: 'foo',
};

console.log(Object.values(o)); //[]
console.log(Object.entries(o)); //[]

1. Other prototype grammars

There are two ways to add attributes and methods to a prototype object.

1.1 Add directly

function Person() {}

Person.prototype.name = 'Macc';
Person.prototype.sayHi = function() {
    console.log('hi');
}

1.2 Rewrite Prototype

Rewrite the prototype directly by an object literal quantity that contains all the attributes and methods.

function Person() {}

Person.prototype = {
    name: 'Macc',
    sayHi: function() {
        console.log('hi');
    }
}

In this writing, Person. The prototype is set to a new object, which has one problem: Person. The constructor property of prototype no longer points to the constructor Person.

console.log(Person.prototype.constructor === Person); //false

Because of this writing, the default prototype object is completely overridden, so its constructor property also points to a completely different new object (the Object constructor).

Although the instanceof operator still returns reliable values, you can no longer rely on the constructor property to identify types.

If the value of the constructor attribute is important, we can set its value specifically in the object literal.

function Person() {}

Person.prototype = {
    constructor: Person, //It's set up specifically, but that's also a problem
    name: 'Macc',
    sayHi: function() {
        console.log('hi');
    }
}

Although the problem with the constructor property has been restored using the above method, there is still a problem with the enumerated property [[Enumerable]] of the restored constructor property that is now true.

However, the native constructor attribute is not enumerable, so we are more likely to use Object after rewriting the prototype object. The defineProperty() method restores the constructor property.

function Person() {}

Person.prototype = {
    name: 'Macc',
    sayHi: function() {
        console.log('hi');
    }
}

Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
})

console.log(Person.prototype.constructor === Person); //true

2. Dynamics of prototypes

Because the process of searching for values from a prototype is dynamic, changes made to the prototype object at any time will be reflected in the instance, even if the instance existed before the prototype was modified.

function Person() {}
//Create an instance before modifying the prototype
let p1 = new Person();
//Modify Prototype
Person.prototype.sayHi = function() {
    console.log('hi');
};
//Modifications will be reflected on the instance
p1.sayHi(); //hi

Although properties and methods can be added to a prototype at any time and are immediately reflected in all instances, modifying and overwriting prototype objects are two different things.

Rewriting the entire prototype disconnects the original prototype from the constructor (because the constructor property has changed), but the instance still references the original prototype, which does not necessarily have the methods we defined and can cause errors.

function Person() {}
//Create an instance before modifying the prototype
let p1 = new Person();
//Rewrite Prototype
Person.prototype = {
    name: 'Macc',
    sayHi: function() {
        console.log('hi');
    }
};
//Error will occur here
p1.sayHi();

Instances have only pointers to prototypes, not constructors.

Instances created after the prototype is rewritten will reference the new prototype.

function Person() {}
//Create an instance before modifying the prototype
let p1 = new Person();
//Rewrite Prototype
Person.prototype = {
    name: 'Macc',
    sayHi: function() {
        console.log('hi');
    }
};
//p2 created after override
let p2 = new Person();
p2.sayHi();//'hi'

3. Prototypes of native objects

All constructors of native reference types (Array, Object, String, and so on) define instance methods on the prototype, which allows you to obtain references to all default methods or define new methods for instances of native types.

But this is not recommended:

  • May cause naming conflicts (due to different browsers);
  • Possibly unexpected override of native methods;

The recommended practice is to create a custom class and inherit the native type.

4. Prototype problems (drawbacks)

(1) The ability to pass initial parameters to the constructor is weakened, resulting in all instances getting the same attribute values by default;

(2) The main problem of the prototype comes from its sharing characteristics. It is primarily a property that shares reference values.

function Person() {}

Person.prototype = {
    arrName: ['Macc', 'Deing']
}

let p1 = new Person();
let p2 = new Person();

p1.arrName.push('Gaga');  //Modifying the properties of instance p1 will be reflected in p2
console.log(p2.arrName); //['Macc', 'Deing', 'Gaga']

In general, however, different instances should have their own copies of attributes.

3. Inheritance

Many object-oriented languages support two kinds of inheritance: interface inheritance and implementation inheritance.

Interface inheritance inherits only method signatures, which is not possible in ECMAScript because functions do not have signatures.

Implement the actual method of inheritance inheritance, which is the only inheritance supported by ECMAScript, primarily through a prototype chain.

By looking at the definition, you can compare the following JS and Java code:

(1) Prototype chain

ECMA-262 defines the prototype chain as the primary inheritance of ECMAScript.

The basic idea is to inherit the properties and methods of multiple reference types through the prototype chain.

1. Basic idea of prototype chain

Here, starting from the relationship among constructors, prototypes, and instances, each constructor has a prototype object, and the prototype has an attribute (constructor) that refers back to the constructor. The instance has an internal pointer ([[Prototype],u proto_u) Points to the prototype of the constructor.

So what if the prototype is an instance of another class? This means that the prototype itself has an internal pointer to another prototype, and the corresponding other prototype has a pointer to the prototype of another constructor.

This creates a chain of prototypes between the instance and the prototype.

2. Default prototype

By default, all reference types inherit from Object. This is also achieved through the prototype chain, where the default prototype for any function is an Object instance.

3. The relationship between prototype and inheritance

There are two ways to determine the relationship between a prototype and an instance

3.1 instance operator

The instance operator returns true if the corresponding constructor appears in the prototype chain of an instance.

//Parent constructor
function Father() {}
//Subclass Constructor
function Child() {}
//Subclass inherits parent class
Child.prototype = new Father();
//Create an instance
let Macc = new Child();
//Determine the relationship between instances and prototypes
console.log(Macc instanceof Child); //true
console.log(Macc instanceof Father); //true
console.log(Macc instanceof Object); //true

Examine the prototype chain of the sample Macc:

  • First, since Macc is an instance of Child, Macc instanceof Child returns true;
  • The prototype of Child is then an instance of Father, which is Child. Prototype. Constructor == Father finds the constructor for Father, so Macc instanceof Father returns true.
  • Finally, the default prototype of any function is an instance of Object, so Macc instanceof Object is true;

3.2 isPrototypeOf method

Each prototype in the prototype chain can call this method, which returns true as long as the prototype chain contains it.

4. About Methods

Subclasses need to override methods of the parent class, or add methods that are not in the parent class. To do this, these methods must be assigned to the prototype before being added to the subclass prototype.

function Father() {
    this.name = 'Macc';
}
Father.prototype.getName = function() {
    return this.name;
}

function Child() {
    this.age = 18;
}
//Inherit parent class
Child.prototype = new Father();
//Override method of parent class
Child.prototype.getName = function() {
    console.log('I have no name');
};
//Add a new method that the parent class does not have
Child.prototype.getAge = function() {
    console.log(this.age);
};
//Create an instance
let Macc = new Child();
Macc.getName(); //I have no name
Macc.getAge(); //18

The main point of the code above is that both of the above methods define modifications after the Child's prototype is assigned an instance of Father.

Creating a prototype method in object literal quantities will destroy the previous prototype chain because it is equivalent to rewriting the prototype chain.

(2) Stealing constructors

Theft constructors are also known as object disguise and classical inheritance.

Role: To solve inheritance problems caused by prototypes containing reference values.

The basic idea is to call the parent class's constructor in the subclass's constructor.

function Father() {
    this.colors = ['red', 'blue', 'green'];
}

function Child() {
    Father.call(this); //Stealing constructors
}

let Macc = new Child();
let Deing = new Child();

console.log(Macc.colors === Deing.colors); //false

Because, after all, a function is a simple object that executes code in a specific context, you can use the apply or call method to execute a constructor for the context using a newly created object (the new execution process).

In the above code, using the call method, the Father constructor executes in the context of the new object created for the Child instance, which is equivalent to running all the initialization code in the Father function on the new Child object.

The result is that each instance will have its own copy of the attributes (you can see that the colors attributes of Macc and Deing are no longer equal).

1. Pass-through parameters

One advantage of stealing a constructor is that you can pass parameters to a parent constructor in the constructor of a subclass.

You just need to know how to use the abc function apply\bind\call\.

Father.call(this,'Macc'); //Pass-through parameters

To ensure that the parent constructor does not override the properties defined by the subclass, you can add additional properties to the instance after calling the parent constructor.

2. Problem of stealing constructors

The problem of stealing a constructor is the same as that of the constructor pattern: the method must be defined in the constructor, so the function cannot be reused.

Additionally, subclasses cannot access methods defined on the parent's prototype.

3. Review

The article is a bit long, so it would be better to go over the previous knowledge here and continue to see the combination inheritance below.

First, in the first section, we learned what objects are, that is, some concepts related to objects.

Then, in the second section, we learned how to create objects. There are three modes, one is

  • Factory Mode
  • Constructor Pattern
  • Prototype mode

Factory mode is not detailed, skipped, and in constructor mode, we assign attributes and methods to this and create instances through the new operator. The disadvantage of this mode is that the methods defined in it are created once in each instance, resulting in functions on different instances having the same name but not equal.

To solve this problem, we introduced a prototype pattern, which solves the problem of function reuse by defining some properties or methods that need to be shared between instances on the prototype of the constructor, but at the same time introduces new problems, if an attribute defined on the prototype is of reference type. This will be reflected on instance B when instance A modifies this property, but normally different instances should have copies of their own properties.

In order for different instances to have their own copies of attributes, the stealing constructor technology has been introduced, but the stealing constructor technology also brings the same problems as the constructor mode. Functions cannot be reused. Therefore, we will later combine the prototype mode and the stealing constructor technology that we learned earlier, that is, the combination inheritance that we want to learn below.

(3) Combinatorial Inheritance

Also known as pseudoclassical inheritance, it combines the advantages of both prototype chains and stolen constructors.

The basic idea is to inherit properties and methods on a prototype using a prototype chain and inherit properties of an instance by stealing a constructor.

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'yellow'];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
};

function SubType(name, age) {
    //Inheritance Properties
    SuperType.call(this, name); //Second call to parent class constructor
    this.age = age;
}

//Inheritance Method
SubType.prototype = new SuperType();  //First call to parent class's constructor (used here for parasitic combinatorial inheritance)
SubType.prototype.sayAge = function() {
    console.log(this.age);
}

//Create an instance
let instance1 = new SubType('Macc', 18);
let instance2 = new SubType('Deing', 16);

console.log(instance1.colors === instance2.colors); //false
console.log(instance1.sayName === instance2.sayName); //true
console.log(instance1.sayAge === instance2.sayAge); //true
instance1.sayAge(); //18
instance2.sayAge(); //16
instance1.sayName(); //Macc
instance2.sayName(); //Deing

This allows methods to be defined on the prototype for reuse and allows each instance to have its own properties.

Combinatorial inheritance makes up for the shortcomings of prototype chains and stolen constructors, is the most used inheritance mode in JS, and retains the ability of instanceof operator and isPrototypeOf methods to identify synthetic objects.

(4) prototype inheritance

The starting point of prototype inheritance is that you can share information between objects through prototypes even if you do not customize the type.

As you learned earlier, if you want to share information (attributes, methods) between different objects, you need to create a type of constructor, then put the information you want to share on the prototype of the constructor, and call the constructor through the new operator to create instances so that they can share the attributes and methods.

However, now that we don't want to create any additional constructors (types) but want to share information between objects, this situation is inherited using prototypes.
The following function is the basic idea to implement prototype inheritance

function object(o) {
    //Temporary constructor
    function F() {}
    F.prototype = o;
    return new F();
}

The object function above creates a temporary constructor, assigns the incoming object to the prototype of the temporary constructor, and returns an instance of the temporary type.

Essentially: the object function performs a shallow copy of the incoming object.

let person = {
    name: 'Macc',
    friendsList: ['Deing', 'GTR', 'GaGa'],
};

let anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friendsList.push('Rob');

let yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friendsList.push('Barbie');

console.log(person.friendsList); // ['Deing', 'GTR', 'GaGa', 'Rob', 'Barbie']

In the code above, the person object defines information that another object should also share. Passing it to the object function returns a new object whose prototype is person, which means that it has both original value and reference value attributes on its prototype. That is, person's friendsList attribute is not only its own attribute, but also follows
anotherPerson and yetAnotherPerson share.

Prototype inheritance works when you have an object on which you want to create a new one. You need to pass this object to the object function before making the appropriate modifications to the returned object.

ES5 adds Object. The create () method normalizes the concept of prototype inheritance.

1.Object.create

This method receives two parameters.

  • First parameter: the object that is the prototype of the new object;
  • Second parameter: Object that defines additional properties for the new object (optional)

This method works the same as the object function above when there is only one parameter.

let person = {
    name: 'Macc',
    friendsList: ['Deing', 'GTR', 'GaGa'],
};

let anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friendsList.push('Rob');

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friendsList.push('Barbie');

console.log(person.friendsList); // ['Deing', 'GTR', 'GaGa', 'Rob', 'Barbie']

Object. The second parameter of create and Object. The second parameter of defineProperties is the same: each new attribute is described by its own descriptor.

let person = {
    name: 'Macc',
    friendsList: ['Deing', 'GTR', 'GaGa'],
};

let anotherPerson = Object.create(person, {
    name: {
        value: 'Deing'
    }
});

console.log(anotherPerson.name); //Deing

Prototype inheritance is ideal when you don't need to create a constructor separately, but you still need to share information between objects.

(5) Parasitic inheritance

The idea behind parasitic inheritance is similar to parasitic constructors and factory patterns: create a function that implements inheritance, somehow enhance an object, and then return it.

The basic parasitic inheritance pattern is as follows

//Prototype Inheritance
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function createAnother(original) {
    let clone = object(original); //Create a new object by calling a function
    clone.sayHi = function() { //Enhance this object in a way, such as adding methods or attributes
        console.log('hi');
    };
    return clone; //Return this object
}

In the above code, the createAnother function, which creates a function that implements inheritance, receives a parameter that is the base object of the new object. This object original is passed to the objectfunction, and the new object returned by the objectfunction is assigned to clone. Then add a new method, saiHi, to the clone object (to enhance it in some way), and return the object (and then the object).

The object function is not required for parasitic inheritance, and any function that returns a new object can be used here.

Here are the scenarios for using parasitic inheritance

let person = {
    name: 'Macc',
    friends: ['Shelly', 'Court', 'Deing'],
};

let anotherPerson = createAnother(person);
anotherPerson.sayHi(); //hi

Adding functions to objects through parasitic inheritance can make functions difficult to reuse, similar to the constructor pattern.

let p1 = createAnother(person);
let p2 = createAnother(person);

console.log(p1.sayHi === p2.sayHi); //false

(6) Parasitic combination inheritance

Combinatorial inheritance has efficiency problems. The main efficiency problem is that parent constructors are always called twice.

  • Called once when a subclass prototype is created;
  • Called once in a subclass constructor;

Essentially, a subclass prototype ultimately consists of all the instance properties of the superclass object, and the subclass constructor simply rewrites its own prototype on execution.

Parasitic combinatorial inheritance inherits attributes by stealing constructors, but uses a mixed prototype chain inheritance method.

Combinatorial inheritance: Inherit properties of instances by stealing constructors using prototype chains to inherit properties and methods on prototypes.

The basic idea is to get a copy of the parent prototype instead of assigning a value to the child prototype by calling the parent constructor.

This means inheriting the parent's prototype using parasitic inheritance and assigning the returned new object to the child's prototype.

The following code is the core logic of parasitic combinatorial inheritance

function inheritPrototype(subType, superType) {
    let prototype = object(superType.prototype); //create object
    prototype.constructor = subType; //Enhance objects to resolve the loss of constructor properties due to overriding prototypes.
    subType.prototype = prototype; //Assignment Object
}

Here is the use of parasitic combinatorial inheritance

//Prototype Inheritance
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
//Parasitic Combinatorial Inheritance
function inheritPrototype(subType, superType) {
    let prototype = object(superType.prototype); //create object
    prototype.constructor = subType; //Enhanced Objects
    subType.prototype = prototype; //Assignment Object
}
//Parent Class
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
};

SuperType.prototype.sayName = function() {
    console.log(this.name);
}
//Subclass
function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    console.log(this.age);
}

In the example above, the SuperType constructor is called only once, avoiding unnecessary and unnecessary attributes on the prototype of the subclass, so it can be said that this example is more efficient. Furthermore, the prototype chain remains unchanged, so instanceof and isPrototypeOf remain valid.

Parasitic combinatorial inheritance is considered the best mode of reference type inheritance.

IV. Classes

The new class keyword introduced by ES6 has the ability to formally define classes. Class is a new basic grammatical sugar structure in ES6.

It appears to support formal object-oriented programming, but in fact it still uses the concepts of prototypes and constructors behind it.

(1) Definition of classes

Classes are defined in two ways: class declarations and class expressions.

Both are bracketed using the class keyword.

//Class declaration
class Person {}
//Class Expression
let Animal = class {}

Similar to function expressions, class expressions cannot be referenced until they are evaluated. However, unlike function definitions, function declarations can be promoted and class definitions cannot.

Another difference from functions is that functions are limited by the function scope and classes by the block scope.

1. Composition of classes

Classes can contain constructor methods, instance methods, get functions, set functions, static class methods. However, none of these are required, and empty class definitions are still valid.

By default, the code in the class definition is executed in strict mode.

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 property. However, this identifier cannot be accessed outside the scope of a class expression.

let Animal = class {};//The name of the class expression is empty here, so it is optional.
let Person = class PersonName { //PersonName is the name of the class expression
    identify() {
        console.log(Person.name, PersonName.name);
    }
}

let p = new Person();

p.identify(); //PersonName PersonName

console.log(Person.name);  //PersonName PersonName
console.log(PersonName); //Error, undefined, PersonName is not accessible outside the scope of the class expression.

(2) Class constructors

The constructor keyword is used to create a constructor for a class within the class definition block.

The method name constructor tells the interpreter that this function should be called when creating an instance of a class using the new operator.

The definition of a class's constructor is not required, and not defining a constructor is equivalent to defining a constructor as an empty function.

1. Instantiation

Using a new call to the class's constructor does the following:

  • (1) Create a new object in memory;
  • (2) The [[Prototype]] pointer inside the new object is assigned the prototype attribute of the constructor;
  • (3) The this inside the constructor is assigned to this new object (this points to the new object);
  • (4) Execute the code inside the constructor (add attributes to the new object);
  • (5) If the constructor returns a non-empty object, it returns the non-empty object, otherwise it returns the new object just created.
class Person {
   constructor(name) {
       console.log(arguments.length);
       this.name = name || null;
   }
}

let p1 = new Person; //0
let p2 = new Person('Macc'); //1

console.log(p1.name); //null
console.log(p2.name); //Macc

The parameter passed in when the class is instantiated is used as a parameter to the constructor. If no arguments are required, the parentheses after the class name are optional.

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 will be destroyed.

However, if the object returned is not this but another object, the returned object will not be detected as associated with the class by the instanceof operator because the object's prototype pointer has not been modified.

class Person {
    constructor(name) {
        console.log(arguments.length);
        this.name = name || null;
        return {
            name: 'GaGa',
            age: 36
        };
    }
}

let p1 = new Person('Macc');

console.log(p1 instanceof Person); //false

The difference between a class constructor and a constructor is that the class constructor must be called with the new operator. Ordinary constructors, unless called with the new operator, use this (usually window) globally as an internal object.

An error occurs if you forget to use the new operator when calling the class constructor.

Class constructors have nothing special about them, and once instantiated, they become common instance methods. (But as a constructor of a class, use the new operator even when called as an instance method).

It can be referenced on an instance after instantiation (constructor)

class Person {
    constructor() {
        console.log('Call Constructor');
    }
}
//Create an instance using a class
let p1 = new Person();  //Output: Call the constructor
//Create a new instance using a reference to the class's constructor
let p2 = new p1.constructor(); //Output: Call the constructor
//No new operator used, error reported
let p3 = Person.constructor(); //Report errors

2. Treat classes as special functions

In all respects, classes in ECMAScript are a special function.

After declaring a class, the class identifier is detected by the typeof operator, indicating that it is a function.

class Person {
    constructor() {
        console.log('Call Constructor');
    }
}
console.log(typeof Person); //function

Class tags also have a prototype attribute, and a constructor attribute on the prototype points to the class itself.

console.log(Person.prototype); //{constructor: ƒ}
console.log(Person.prototype.constructor === Person);  //true

As with normal functions, you can use the instanceof operator to check whether a constructor prototype exists in the prototype chain of an instance.

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 using a new call. The important thing is that 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 directly as a normal constructor when an instance is created, the result 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(); //Use class constructors directly as normal constructors
console.log(p2.constructor === Person); //false
console.log(p2 instanceof Person); //fasle
console.log(p2 instanceof Person.constructor); //true

The example above actually corresponds to the statement that the class itself is treated as a constructor when using a new call. The constructor method defined in the class is not treated as a constructor. Note here that when using new, it is the class itself that is treated as a constructor, not the constructor method defined in the class.

Classes are first-class citizens of JS, so they can be passed as parameters like other object or function references.

Classes can be defined anywhere, like functions, such as in arrays.

let classList = [
    class {
        constructor(id) {
            this.id_ = id;
            console.log(`instance ${this.id_}`);
        }
    }
];

function createInstance(classDefinition, id) {
    return new classDefinition(id);
}

let foo = createInstance(classList[0], 1433223);

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

let p = new class Foo {
    constructor(x) {
        console.log(x);
    }
}('bar'); //Instantiate Now

console.log(p); //Foo ()

(3) Instances, prototypes and class members

The syntax of a class makes it easy to define which members should exist on an instance, which should exist on a prototype, and which members should exist on the class itself.

1. Instance members

Inside the class constructor, you can add your own properties for the newly created instance (this). After the constructor is executed, new members can still be added to the instance.

Each instance corresponds to a unique member object, meaning that none of the members will be shared on the prototype.

class Person {
    constructor() {
        this.name = new String('Macc');
        this.sayName = () => console.log(this.name);
        this.nickname = ['mac', 'MC'];
    }
}

let p1 = new Person(),
    p2 = new Person();

console.log(p1.name === p2.name); //false
console.log(p1.sayName === p2.sayName); //false
console.log(p1.nickname === p2.nickname); //false

2. Prototype methods and accessors

To share methods between instances, the class definition syntax takes methods defined in the class definition block as prototypes.

class Person {
    constructor() {
            //All content 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 p1 = new Person();

p1.locate(); //An attribute on an instance instance obscures a property (method) with the same name on the prototype
Person.prototype.locate(); //prototype

You can define methods in class constructors or in class blocks, but you cannot add raw values or objects as member data to prototypes in class blocks.

class Animal {
    name: 'GTR'
    //Report errors
}

Writing like this will cause errors.

Class methods are equivalent to object properties, so you can also use strings, symbols, calculated values (calculable properties) as keys.

Class definitions also support getting and setting accessors, and the syntax behaves like normal objects.

class Person {
    set name(newName) {
        this.name_ = newName;
    }

    get name() {
        return this.name_;
    }
}

let p1 = new Person();
p1.name = 'Macc';

3. Static class methods

Static methods can be defined on classes. These methods are typically used to perform instance-independent operations and do not require instances of classes to exist.

There can only be one static member per class.

Static class members use the static keyword as a prefix in class definitions. In a static member, this refers to the class itself. All other conventions are the same as prototype members.

class Person {
    constructor() {
            this.locate = () => console.log('instance');
        }
        //Defined on the prototype object of a class
    locate() {
            console.log('prototype', this);
        }
        //Defined on the class itself
    static locate() {
        console.log('class', this);
    }
}

let p = new Person();
p.locate();
Person.prototype.locate();
Person.locate(); //class, Person {}

Static methods are well suited for instance factories.

class Person {
    constructor(age) {
        this.age = age;
    }

    static create() {
        //Create and return a Person instance using random age
        return new Person(Math.floor(Math.random() * 100));
    }
}

console.log(Person.create()); //Person {age: 65}

4. Non-functional prototypes and class members

Class definitions do not explicitly support adding member data on prototypes or classes, but they can be added manually outside the class definition.

class Person {
    sayName() {
        console.log(`${Person.greeting} ${this.name}`);
    }

    // name:'Macc', as mentioned above, it is incorrect to define attributes like this
}
//Manually add outside class definition
Person.greeting = 'My name is'; //Define data members on classes
Person.prototype.name = 'Macc'; //Define data members on class prototypes

let p = new Person();
p.sayName(); //My name is Macc

Why is adding data members not explicitly supported in class definitions?

Because adding variable (modifiable) data members to shared targets (prototypes and classes) is an antipattern.

In general, the object instance should own the data referenced through this on its own.

5. Iterators and Generator Methods

Class definition syntax supports defining generator methods on the prototype and on the class itself.

class Person {
    //Define generator methods on prototypes
    *createNickIterator() {
        yield 'Jack';
        yield 'Jake';
        yield 'J-Dog';
    }

    //Define generator methods on classes
    static *createJobIterator() {
        yield 'Butcher';
        yield 'Baker';
        yield 'Canlestic maker';
    }
}

let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); //Butcher
console.log(jobIter.next().value); //Baker
console.log(jobIter.next().value); //Canlestic maker

let p = new Person();
let nicknameIter = p.createNickIterator();
console.log(nicknameIter.next().value);
console.log(nicknameIter.next().value);
console.log(nicknameIter.next().value);

Because the Generator method is supported, you can make class instances Iterable by adding a default iterator.

class Person {
    constructor() {
        this.nickname = ['Jack', 'Jake', 'J-Dog'];
    }

    *[Symbol.iterator]() {
        yield *this.nickname.entries();
    }
}

let p = new Person();
//Turning instance p of a class into an iterative object
for (let [idx, nickname] of p) {
    console.log(nickname);
}

Or just return an iterator instance

class Person {
    constructor() {
        this.nickname = ['Jack', 'Jake', 'J-Dog'];
    }


    [Symbol.iterator]() {
        //Return only iterator instances
        return this.nickname.entries();
    }
}

let p = new Person();
for (let [idx, nickname] of p) {
    console.log(nickname);
}

(4) Inheritance

1. Inheritance Base

ES6 supports single inheritance. With the extends keyword, you can inherit any object that has [[Constructor]] and the prototype. This means that not only can you inherit a class, but you can also inherit normal constructors (keep backwards compatible)

//class
class Vehicle {}
//Inheritance Class
class Bus extends Vehicle {}

let bus = new Bus();
console.log(bus instanceof Bus); //true
console.log(bus instanceof Vehicle); //true

//Constructor
function Person() {}
//Inherit normal constructor
class Chinese extends Person {}

let Macc = new Chinese();
console.log(Macc instanceof Person); //true
console.log(Macc instanceof Chinese); //true

Both classes and methods defined on the prototype bring to the derived class. The value of this will reflect the instance or class that calls 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');
v.identifyPrototype('vehicle');

Bus.identifyClass('Bus');
Vehicle.identifyClass('Vehicle');

The extends keyword can also be used in class expressions, such as let Bar = class extends Foo{}

2. Constructor, HomeObject, Super()

Methods of derived classes can reference their prototypes through the super keyword.

This keyword is used only in derived classes and is restricted to class constructors, instance methods, and static methods.

Use super in a class constructor to call a parent class constructor.

class Vehicle {
    constructor() {
        this.hasEngine = true;
    }
}

class Bus extends Vehicle {
    constructor() {
        //Do not refer to this before calling super again, otherwise you will get an error!!!
        super(); //Equivalent to super.constructor()

        console.log(this instanceof Vehicle); //true
        console.log(this); //Bus {hasEngine : true}
    }
}

new Bus();

Static methods can be defined on inherited classes through super calls in static methods.

class Vehicle {
    static identify() {
        console.log('vehicle');
    }
}

class Bus extends Vehicle {
    static identify() {
        super.identify();
    }
}

Bus.identify(); //vehicle

ES6 adds an internal attribute [[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 inside the JS engine. Sup is always defined as the prototype of [[HomeObject]].

2.1 super usage considerations

(1) super can only be used in constructors and static methods of derived classes.

(2) The super keyword cannot be referenced separately, either using it to invoke the constructor or using it to invoke static methods.

(3) Calling super() calls the parent constructor and assigns the returned instance to this.

(4) super() behaves like a constructor call, and if you need to pass parameters to the parent class's constructor, you need to pass them in manually.

class Vehicle {
    constructor(color) {
        this.color = color;
    }
}

class Bus extends Vehicle {
    constructor(color) {
        super(color); //Pass parameter to parent constructor
    }
}

console.log(new Bus('black')); //Bus {color: 'black'}

(5) If no (subclass) constructor is defined, super() is called when instantiating the derived class, and all parameters passed to the derived class are passed in.

class Vehicle {
    constructor(color) {
        this.color = color;
    }
}

//Subclass has no constructor defined
class Bus extends Vehicle {}

console.log(new Bus('black')); //Bus {color: 'black'}

(6) In a class constructor, you cannot call this before calling super().

(7) If a constructor is explicitly defined in a derived class, either super() must be called in it or an object must be returned in it.

class Vehicle {}
//car does not define a constructor
class Car extends Vehicle {}

class Bus extends Vehicle {
    constructor() {
        super(); //Either call super
    }
}

class Van extends Vehicle {
    constructor() {
        return {}; //Or return an object
    }
}

console.log(new Car()); //Car {}
console.log(new Bus()); //Bus {}
console.log(new Van()); //{}

3. Abstract Base Class

Sometimes it may be necessary to define a class that can be inherited by other classes but not instantiated itself.

In ECMAScript you can use new.target implementation, new.target holds a class or function that is called through the new keyword. By checking new when instantiating. Whether a target is an abstract base class or not prevents instantiation of the abstract base class.

//Abstract Base Class
class Vehicle {
    constructor() {
        console.log(new.target);
        if (new.target === Vehicle) {
            throw new Error('Abstract base class cannot be instantiated');
        }
    }
}

//Derived Class
class Bus extends Vehicle {}

//Instantiate derived classes
new Bus();
//Instantiate abstract base class
new Vehicle();

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, the method can be checked by the this keyword.

//Abstract Base Class
class Vehicle {
    constructor() {
        if (new.target === Vehicle) {
            throw new Error('Abstract base class cannot be instantiated');
        }

        if (!this.foo) {
            throw new Error('Derived classes must be defined foo Method');
        }

        console.log('success');
    }
}

//Defines the normal foo method
class Van extends Vehicle {
    foo() {}
}
//Derived class: no foo method defined, error will occur
class Bus extends Vehicle {}

new Van();
new Bus(); //Report errors

4. Inherit built-in types

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); //SuperArray(5) [1,2,3,4,5]
a.shuffle();
console.log(a); //SuperArray(5) [5, 1, 2, 3, 4]

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

class SuperArray extends Array {}

let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x % 2));

console.log(a1);
console.log(a2);
console.log(a1 instanceof SuperArray); //true
console.log(a2 instanceof SuperArray); //true

If you want to override this default behavior, you can override 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);
console.log(a2);
console.log(a1 instanceof SuperArray); //true
console.log(a2 instanceof SuperArray); //false notices that the results here are different from those above

5. Class Mixing

A common JS pattern for aggregating the behavior of different classes into one class.

Object. The assign() method is designed to blend in object behavior. If you just need to mix attributes of multiple objects, you can use this method.

In the code below, the extends keyword is followed by a JS expression. Any expression that can be parsed into a class or a constructor is valid. This expression is evaluated when evaluating the class definition.

class Vehicle {}

function getParentClass() {
    console.log('Evaluation expression');
    return Vehicle; //Return a class
}

class Bus extends getParentClass() {}

Mixing patterns can be achieved by concatenating multiple mixed elements in an expression that eventually resolves to a class that can be inherited.

If the Person class needs to combine A, B, C, then a mechanism is needed to implement B inheriting A, C inheriting B, and Person inheriting C to group ABC into this superclass.

One strategy is to define a set of "nestable" functions, each receiving a superclass as a parameter, while the mixed class is defined as a subclass of the parameter and returns the class. These composite functions can be called concatenatively and finally combined into superclass expressions.

class Vehicle {}
//A set of nestable functions
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 bus = new Bus();
bus.foo();
bus.bar();
bus.baz();

Nested calls can be expanded by writing an auxiliary function.

//auxiliary function
function mix(BaseClass, ...Mixins) {
    return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}

class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}

Many JS frameworks have abandoned the mixed mode and moved to the composite mode. Composite schemas extract methods into separate classes and auxiliary objects and combine them without inheritance. This reflects the principle of software design: composite is better than inheritance.

Hope to see you here and get something out of it.

End, scatter flowers...

Keywords: Javascript Front-end

Added by freakstyle on Sat, 12 Feb 2022 04:11:05 +0200