preface

JavaScript is an object-oriented language, and all objects inherit properties and methods from stereotypes, so what is a stereotype? How is inheritance implemented between objects?

This article gives you an in-depth look at prototypes in JavaScript, and you are welcome to read this article if you are interested.

The principle of analytic

Let’s step through the relationship between prototypes and objects.

A prototype object

When we use the function keyword to create a function, an object containing the Prototype property is created in memory, which points to the function’s prototype object as follows:

function Person() {
    
}
Person.prototype // {constructor: Person(), __proto__}
Copy the code

In the above code we create a function named Person:

  • The prototype property points to the prototype object of Person.
  • Each object “inherits” properties from the stereotype
  • The prototype object containsconstructorwith__proto__Properties.

Let’s draw a diagram to illustrate the relationship between Person and Prototype

The function called with the new operator is the constructor, and it is recommended that constructors be named with a capital letter.

The relationship between a function instance and a prototype object

In the previous section, we clarified the relationship between constructors and prototype objects. Now let’s look at the relationship between function instances and prototype objects.

We instantiate the Person function created in the previous section with operator new to get the Person instance as follows:

// instantiate the object
const person = new Persion();
Copy the code

In the previous section, we learned that a prototype object has two properties, __proto__ is a property that every JavaScript object except NULL has, and it points to the prototype object of that object’s constructor.

If person.__proto__ is equal to Persion. Prototype

function Person() {}const person = new Persion();
console.log("Function instance's __proto__ pointer points to the constructor's prototype object:", person.__proto__ === Person.prototype);
Copy the code

The result is as follows:

In addition to using __proto__ to access prototype objects, we can also get them using Object.getProtoTypeof ().

After proving that they are equal, combine the constructor with the prototype object to know the relationship between the three of them as follows:

When we instantiate a constructor, we also create a __proto__ property for the instance, which is a pointer to the constructor’s prototype object.

As a result of all instances of the same constructor creates __proto__ properties are pointing to the constructor of the prototype object, so all the instance objects will share the constructor prototype on the object properties and methods, as a result, once the prototype on the object properties or methods change, all instances of the object will be affected.

Let’s consider the question, since every JS object other than NULL has a __proto__ attribute, then Person is an object that also contains a __proto__ attribute. Where does it point?

The answer is obvious, as we said above: __proto__ points to the constructor’s prototype object.

Let’s take an example:

function Person() {}
Person.__proto__ === Person.constructor.prototype // true
Copy the code
  • In the code above we usedconstructorProperty, which points to the object’s constructor, which we’ll cover in more detail in the next section.

The result is as follows:

The relationship between stereotype objects and constructors

In the previous section we looked at the __proto__ direction of a prototype object. Now we’ll look at the constructor direction. Each stereotype object has a constructor property that points to the object’s constructor.

Next, we to prove that the next Person. Prototype. If the constructor and the Person is equal, the code is as follows:

function Person() {}const person = new Person();
console.log("Prototype object equal to constructor:", Person.prototype.constructor === Person);
Copy the code

The result is as follows:

After proving that they are equal, we can see the relationship between them by combining the constructor, function instance, and prototype object as follows:

Get the Object prototype. In addition to accessing its prototype, we can also get it using Object.getProtoTypeof ().

The order in which instance properties are read

When reading an attribute in an instance, if it can’t find it, it looks for the attribute in the prototype of that object. If it can’t find it, it looks for the prototype’s prototype until it reaches the top level.

Let’s take an example to verify the above statement:

function Person() {

}
Person.prototype.name = "Name property on prototype";
const person = new Person();
person.name = "Name property on instance";
console.log(person.name) // The name attribute on the instance

delete person.name;
console.log(person.name); // The name attribute on the prototype

delete Person.prototype.name;
console.log(person.name); // undefined
Copy the code

Let’s take a look at these examples:

  • The name attribute is added to the stereotype
  • The name attribute is added to the instance
  • In this case, the value of the name attribute is the Name attribute on the instance
  • Removed the name attribute on the instance
  • At this point, it looks for the name property on the stereotype, so the value is the name property on the stereotype
  • Removed the name attribute on the stereotype
  • At this point, he looks for the name value of the prototype. The prototype does not have a name attribute, so returns undefined

In the above analysis, we did not find the name attribute in the prototype of the Person, so what is the prototype of the Person? Let’s print it in the Browser console, as shown below:

As you can see from the result above, the prototype of Person is an object. Proving that the prototype is also an object, we can create it in the most primitive way, as shown below:

const object = new Object(a); object.name ="Properties in objects";
console.log(object.name); // Attributes in the object
console.log("__proto__ of an object instance points to an object instance object", object.__proto__ === Object.prototype);
console.log("The prototype Object of Object is equal to the constructor".Object.prototype.constructor === Object);
Copy the code

The result is as follows:

Knowing that the prototype is also an object, and combining what we have proved above, the relationship between them is as follows:

Prototype chain

From the previous analysis, we know that everything is based on Object, so what is the prototype of Object? The answer is null

We verified it on the Browser console and the result is as follows:

Taken together, their final relationship is as follows:

The chain of orange lines is the prototype chain.

Overwrite prototype object

When we implement something, we often rewrite the entire prototype object with an object literal containing all the properties and methods.

As follows:

Person.prototype = {
    name: "The Amazing Programmer.".age: "20".job: "Web Front-end Development".sayName: function () {
        console.log(this.name); }}Copy the code
  • Points the prototype of Person to a new object
  • The prototype has three properties and a method
  • No constructor property exists in the object

Since the overridden Object does not have a constructor property, its constructor property will point to Object.

Let’s verify that the code looks like this:

console.log("The constructor of Person's prototype object is equal to the Person constructor.", Person.prototype.constructor === Person)
Copy the code

The result is as follows:

If the constructor value is important, then we need to explicitly change the constructor point to the constructor, as shown below:

Person.prototype = {
    name: "The Amazing Programmer.".age: "20".job: "Web Front-end Development".sayName: function () {
        console.log(this.name);
    },
    constructor: Person
}
console.log("The prototype object of Person is equal to the Person constructor.", Person.prototype.constructor === Person)
Copy the code

The result is as follows:

Prototype chain inheritance

In the previous chapter of principle analysis, we saw the appearance of prototype chain intuitively in the last schematic diagram. Next, we will outline the specific concept of prototype chain.

  • Each constructor has a prototype object
  • Each prototype object contains a pointer to the constructor (constructor)
  • Each constructor instance contains an internal pointer to the prototype object (__proto__)
  • If the stereotype object is equal to an instance of another constructor, the stereotype object will contain a pointer to the other constructor stereotype
  • Correspondingly, the other constructor’s prototype contains a pointer to the other constructor
  • If the other stereotype is an instance of another constructor, the relationship still holds
  • This is a chain of instances and prototypes, the orange line we see in the diagram, which is the basic concept of a prototype chain

Next, we use an example to explain the inheritance of the prototype chain, the code is as follows:

function Super() {
    this.property = true;
}
Super.prototype.getSuperValue = function() {
    return this.property;
}
function Sub() {
    this.subProperty = false;
}

// The Sub prototype points to the Super instance, while constructor is overwritten to point to Super
Sub.prototype = new Super();
Sub.prototype.getSubValue = function () {
    return this.subProperty;
}

let sub = new Sub();
console.log("Get the property value of Super", sub.getSuperValue());
console.log("The prototype object of the sub instance is equal to the prototype object of the sub constructor", sub.__proto__ === Sub.prototype);
console.log("The prototype object of the Sub constructor is equal to the prototype object of the Super constructor.", Sub.prototype.__proto__ === Super.prototype)
console.log("Sub constructor prototype object constructor pointing to Super constructor", Sub.prototype.constructor === Super)
Copy the code

The running results are as follows:

  • First, we create a function called Super and add an internal property called Property with a value of true
  • It was then added to the Super prototype objectgetSuperValueMethod that returns the inside of the functionpropertyattribute
  • We then create a function called Sub with an internal property called subProperty with a value of false
  • We then point the Sub prototype object to the Super instance, at which point inheritance is implemented, and the Sub prototype will have the methods on the Super prototype
  • We then added in the Sub prototype objectgetSubValueMethod that returns the inside of the functionsubPropertyattribute
  • Finally, we instantiate the Sub object, which forms a prototype chain with the Super object, following the relationship described in the principle analysis

Next, let’s graph the above analysis to make it easier to understand, as shown below (the orange line is the prototype chain) :

Existing problems

When we use stereotype chains to implement inheritance, if we inherit a reference type on a stereotype object, that reference type is shared by all instances.

Let’s use an example to illustrate this problem:

function Super() {
    this.list = ["a"."b"."c"];
}

function Sub() {

}
Sub.prototype = new Super();
const sub1 = new Sub();
sub1.list.push("d");
console.log(sub1.list);
const sub2 = new Sub();
console.log(sub2.list);
Copy the code

In the above code:

  • First, we declare two constructors, Super and sub
  • Then, the prototype object of Sub points to an instance of Super for inheritance
  • Then, the Sub object is instantiatedsub1The instance
  • Add an element D to the list of sub1 instances
  • In this case, the list array element of sub1 instance is[ 'a', 'b', 'c', 'd' ]
  • Then, instantiate the Sub object again to getsub2The instance
  • At this point, the list array element of the sub2 instance is[ 'a', 'b', 'c', 'd' ]

The running results are as follows:

The problem is obvious, we are not adding elements to the list array of sub2, we want its value to be [” A “,”b”,” C “] as defined on the Super prototype.

Since the list attribute defined in the Super constructor is a reference type, it is shared on instantiation. When sub1 changes its value, sub2 gets the changed value of sub1, i.e. [‘a’, ‘b’, ‘c’, ‘d’].

In the next section, we will address the problem of reference types being shared by instances

Constructor inheritance

In the subclass constructor, we can use call to copy all the properties and methods of the parent constructor into the current constructor, so that after instantiation, we can modify the properties and methods and not affect the contents of the parent constructor.

Next, let’s use an example to illustrate the above words:

function Super() {
    this.list = ["a"."b"."c"];
}

function Sub() {
    Super.call(this)}const sub1 = new Sub();
sub1.list.push("d");
console.log("sub1" ,sub1.list);
const sub2 = new Sub();
console.log("sub2", sub2.list);
Copy the code

The code above is the same as the example in the previous section, but only the changes:

  • We use call in the Sub constructor to copy the properties and methods from Super, implementing inheritance

  • Since attributes are copied to the current instance each time they are instantiated, elements added to sub1 do not affect sub2

The running results are as follows:

Existing problems

We borrow constructors to implement inheritance, and cannot inherit methods and properties from the parent class stereotype, so the reusability of the function is lost.

Let’s continue with the code from the previous section:

function Super() {
    this.list = ["a"."b"."c"];
}

Super.prototype.newList = [];

function Sub() {
    Super.call(this)}const sub1 = new Sub();
console.log("sub1" ,sub1.newList);
Copy the code
  • We added it to the Super prototypenewListattribute
  • Does not exist when accessed in sub1 instance

The running results are as follows:

Combination of inheritance

Through the analysis of the former two chapters, we know the prototype chain inheritance way can inherit the prototype object properties and methods, inherited constructor can inherit properties and methods of the constructor, they complement each other, then we combine both of its strengths, to realize the combination of inheritance, too perfect to make up for their respective boards.

Next, let’s use an example to illustrate combinatorial inheritance:

// Combinatorial inheritance
function Super(name) {
    this.name = name;
    this.list = ["a"."b"."c"];
}
Super.prototype.getName = function () {
    return this.name;
}
function Sub(name, age) {
    // Constructor inherits, the parent constructor is called a second time
    Super.call(this,name);
    this.age = age;
}
// The parent constructor is called for the first time
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.getAge = function () {
    return this.age;
}

const sub1 = new Sub("The Amazing Programmer."."20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("White"."20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);
Copy the code

In the above code:

  • First, create a file namedSuperThe constructor takes a name argument and has two attributes name and list
  • Then we went toSuperTo the prototype objectgetNameMethod that returns the value of the name property in Super
  • Next, we create a file namedSubFunction that takes two arguments: name, age, add the age attribute to the constructor, inherit the attributes and methods in the parent constructor
  • We then rewrite the Sub constructor’s prototype object as an instance of Super, fixing the constructor reference
  • Then we went toSubTo the prototype objectgetAgeMethod to return the age property in Sub
  • Finally, we test the inherited method by instantiating two instances of the Sub constructor

The running results are as follows:

Parasitic combinatorial inheritance

When we implement composite inheritance, we call the parent class’s constructor twice.

The first time the parent constructor is called:

  • We rewrote the Sub prototype object to point to an instance of Super
  • At this point, attributes and methods on the parent constructor instance are assigned toSub.prototype

This call is what we talked about in the section on stereotype chain inheritance. Subclasses have inherited properties and methods from the parent constructor and properties and methods from the stereotype object.

The parent constructor is called the second time:

  • Inside the constructor of Sub, we usecallwillSuperAssigns properties and methods to instances of Sub
  • Properties on the instance mask properties on the stereotype chain when the stereotype chain searches for properties

Therefore, on the second call, there is no need to assign properties and methods from the constructor to instances of the Sub constructor, which is meaningless.

Next, let’s look at the optimized combinatorial inheritance:

function Super(name) {
    this.name = name;
    this.list = ["a"."b"."c"];
}

Super.prototype.getName = function () {
    return this.name;
}

function Sub(name, age){
    Super.call(this, name);
    this.age = age;
}

// Create an intermediate function that inherits Super's prototype object
function F() {}// Point F's prototype object to Super's prototype object
F.prototype = Super.prototype;
// Point the Sub prototype object to an instance of F
Sub.prototype = new F();
Sub.prototype.constructor = Sub;
Sub.prototype.getAge = function () {
    return this.age;
}

const sub1 = new Sub("The Amazing Programmer."."20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("White"."20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);
Copy the code

In the above code:

  • We created an intermediate function firstF
  • Then, the prototype object of F was rewritten to point directly to the prototype object of FSuperThe prototype object of
  • Finally, we point the prototype object of Sub to an instance of F to achieve prototype chain inheritance.

His efficiency is that the Super constructor is called only once on instantiation, and the prototype chain remains the same.

The running results are as follows:

Optimized combinatorial inheritance is also known as parasitic combinatorial inheritance. In the implementation code above, we use an intermediate function to implement the inheritance of the prototype chain. This intermediate function can also be replaced by object.create ().

Prototype = object. create(super. prototype, {constructor: {value: Sub}})

Modify the prototype object to implement inheritance

In the last section, we used an intermediate function to implement inheritance of the prototype chain. We can also direct the protoobject of a subclass to the protoobject of its parent class via its __proto__ attribute. This method does not change the protoobject of a subclass, so the constructor attribute on the protoobject of a subclass points to the parent class’s constructor.

Next, let’s use an example to illustrate the above words:

function Super(name) {
    this.name = name;
    this.list = ["a"."b"."c"];
}

Super.prototype.getName = function () {
    return this.name;
}

function Sub(name, age){
    Super.call(this, name);
    this.age = age;
}
// Change the Sub constructor's prototype object to point to Super
Sub.prototype.__proto__ = Super.prototype;
Sub.prototype.getAge = function () {
    return this.age;
}

const sub1 = new Sub("The Amazing Programmer."."20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("White"."20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);
Copy the code

In the above code:

  • We use the__proto__Property to modify Sub prototype object pointing

In the principles section, we learned that every javascript object other than NULL has a __proto__ attribute that points to the object’s prototype by default, so we can use this attribute to modify the prototype object it points to.

We can also modify the prototype of an Object using the object.setPrototypeof () method in ES6.

Object. SetPrototypeOf (sub.prototype, super.prototype)

Constructor static method inheritance

Add a method directly to the constructor, which is a static method.

None of the previous inheritance methods implement static method inheritance on constructors, whereas in ES6 class inheritance, subclasses can inherit static methods from their parent classes.

We can implement static method inheritance through the object.setPrototypeof () method.

Next, let’s explain it through a specific example:

function Super(name) {
    this.name = name;
    this.list = ["a"."b"."c"];
}

Super.prototype.getName = function () {
    return this.name;
}

// Add static methods
Super.staticFn = function () {
    return "Static method of Super";
}

function Sub(name, age) {
    // Inherit the data from the Super constructor
    Super.call(this, name);
    this.age = age;
}

// Modify the prototype pointing of Sub
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// Inherit static attributes and methods from the parent class
Object.setPrototypeOf(Sub, Super);

Sub.prototype.getAge = function () {
    return this.age;
}

console.log(Sub.staticFn());
const sub1 = new Sub("The Amazing Programmer."."20");
sub1.list.push("d");
console.log("sub1", sub1.list);
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
const sub2 = new Sub("White"."20");
console.log("sub2", sub2.list);
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
Copy the code

The running results are as follows:

The rest of the code above is the same as the previous example. Let’s examine the differences:

  • First of all, let’s go toSuperThe constructor adds a name namedstaticFnStatic method of
  • Then we passObject.setPrototypeOf(Sub, Super)Function inheritsSuperConstructor static properties and methods

At this point, we have a perfect inheritance, which is the underlying implementation of ES6’s Class syntax sugar.

The class syntactic sugar

ES6 has a new class modifier, and when we create two objects with the class modifier, we can use the extends keyword to implement inheritance. The underlying implementation is the combination of the above parasitic combinatorial inheritance and static method inheritance of the constructor.

Next, let’s look at the example from the previous section, using class implementation as follows:

class Super {
    constructor(name) {
        this.name = name;
        this.list = ["a"."b"."c"];
    }

    getName() {
        return this.name; }}// Add static methods to Super
Super.staticFn = function () {
    return "Static method of Super";
}

class Sub extends Super{
    constructor(name, age) {
        super(name);
        this.age = age;
    }

    getAge() {
        return this.age; }}console.log(Sub.staticFn())
const sub1 = new Sub("The Amazing Programmer."."20");
sub1.list.push("d");
console.log("sub1", sub1.list);
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
const sub2 = new Sub("White"."20");
console.log("sub2", sub2.list);
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
Copy the code

The running results are as follows:

The code address

For all the sample code in this series, go to jS-learning

Write in the last

This article is “JS principle learning” series of the second article, this series of complete route please move: JS principle learning (1) “learning route planning

  • If there are any errors in this article, please correct them in the comments section. If this article helped you, please like it and follow 😊
  • This article was first published in nuggets. Reprint is prohibited without permission 💌