This article gradually visualizes the abstract concepts of prototyping and inheritance from the shallow to the deep, and each knowledge point is combined with the corresponding examples, as much as possible to make it as common as possible, and the biggest advantage of this article is: long (for more details).

A prototype,

First, let’s talk about prototypes, but when we talk about prototypes we have to start with functions, because prototype objects are the prototype properties that functions have.

1.1 the function

Functions are objects and have properties just like objects. For example:

function F(a, b) {
    return a * b;
}

F.length   // 2 refers to the number of function arguments
F.constructor   // function Function() { [native code] }
typeof F.prototype  // "object"
Copy the code

Functions have the same properties as objects, and we’re talking about the prototype property.

Prototype is also an object. For the sake of more vivid understanding, I personally interpret the above as:

// This function object has a Prototype property
F = {
    prototype: {}}Copy the code

Let’s take a look at this prototype object property.

1.2 Properties of the Prototype object

Prototype is an object with a default property (constructor) that points to the current function.

F = {
    prototype: {
        constructor: F    // Point to the current function}}Copy the code

Since Prototype is an object, we can also add properties to it. For example:

F.prototype.name = 'BetterMan';

// Then F becomes the following:
F = {
    prototype: {
        constructor: F,
        name: 'BetterMan'}}Copy the code

Prototype is here. Let’s talk about objects and then string them together.

1.3 Creating An Object

There are many ways to create objects, and this article is focused on prototypes, so I’ll talk about using constructors to create objects. The above F function is actually a constructor (the default name of a constructor is capitized for easy identification), so we use it to create objects.

let f = new F();
console.log(f)  / / {}
Copy the code

Now that we have an “empty” object, let’s go through how the constructor creates the object:

  1. Create a new object;
  2. Assigning the scope of the constructor to the new object refers this to the new object (there is also a procedure where the __proto__ attribute of the new object refers to the ptotoType attribute of the constructor, which is explained later).
  3. Execute the code inside the function, which adds properties to the new object.
  4. Return the new object (no need to write, default returns this, this refers to the new object).

Let’s modify the F constructor:

function F(age) {
    this.age = age;
}
Copy the code

Use F to create an instance object:

let f1 = new F(18);  // 18 years old, have a good day
console.log(f1); // {age: 18}
Copy the code

So we’ve got an F1 object with an age property in it, but is it really just an age property? The constructor creates an object, creates an object, adds a property to the object, and returns a new object. We can see all of that, and then we have a new __proto__ attribute that points to the ptotoType attribute of the constructor.

Let’s print it out and see:

console.log(f1.__proto__);  // {constructor: F}
Copy the code

This is the F constructor’s prototype object. This reference process is also equivalent to F1.__proto__ === f.prototype, which is important to understand!

__proto__ we can called implicit stereotype (not all browsers support this attribute, so Google approach), it is much, since it refers to the constructor’s prototype, that we will get to it can get to the constructor of the prototype, but usually we don’t use this method to get the prototype, behind will introduce other methods).

The constructor property in the constructor object refers to its own function. This property is called __proto__.

console.log(f1.__proto__.constructor);  // F(age) {this.age = age; }
__proto__ === f.protototype. constructor === f.protototype. constructor
Copy the code

Well, good, good, looks good!

At present, it should be easy to understand, so let’s see:

console.log(f1.constructor);  // F(age) {this.age = age; }
Copy the code

Well, what the hell is this? Does the instance object F1 have a constructor property that points to the constructor just like the constructor of the constructor prototype? Now that’s interesting.

F1’s mysterious attribute __proto__ refers to F. protototype, which is a reference to f. protototype. If you want to be more pictorial, you can think of it as “sharing” f. protototype to F1, but this is a dynamic “sharing”. If f. Rototype is changed later, the “shared” properties of F1 will also change. It’s important to understand! Important things say three times! Important things say three times! Important things say three times!

Let’s visualize the code again:

F = {
    prototype: {
        constructor: F
    }
};

f1 = {
    age: 18.__proto__: {    // Since we've already visualized this as a "shared" property, let's visualize it a bit more
        constructor: F}} //18.// This is a property of the F1 object itself
    constructor: F // This attribute is "shared" from the stereotype}Copy the code

Since we’re talking about dynamic “shared” properties, let’s change the constructor’s prototype property to see if f1 follows:

// Before the change
console.log(f1.name);  // undefined

// After the modification
F.prototype.name = 'BetterMan';
console.log(f1);   // {age: 18}
console.log(f1.name);  // 'BetterMan'
Copy the code

A(read A no.2)… We can only get the name attribute from the constructor prototype, instead of making it a property of the instance object itself. This brings us to the attributes of the object itself and the attributes of the stereotype (the attributes derived from the stereotype).

1.4 Object properties and stereotype properties

We create an instance object f1, which has its own property, age, and a property, name, found in the stereotype. We can check this using the hasOwnProperty method:

console.log(f1.hasOwnProperty('age'));  // True indicates a property of itself
console.log(f1.hasOwnProperty('name')); // false indicates that it is not a property of itself
Copy the code

So since it’s an object property, you can add and remove it, right? Let’s try:

delete f1.age;
console.log(f1.age); // undefined

delete f1.name;
console.log(f1.name); // 'BetterMan'
Copy the code

Well, the age attribute has been removed, but the name attribute seems to have no effect. This shows that the F1 object can control its own attributes, which can be deleted or added, but the name attribute is derived from the prototype, which is someone else’s attribute, and you have no right to change it.

When we access the name attribute of an object, the js engine will query all the attributes on the f1 object in turn, but it can’t find the attribute, and then it will go to the prototype of the constructor that created the f1 instance (this is due to the mysterious __proto__ attribute, which connects the instance object with the constructor stereotype). Then you find it (and if you can’t find it again, you look up, which is where the prototype chain comes in, which we’ll get to later). The age attribute is found in F1, so you don’t have to go anywhere else.

By now you have a general idea of the prototype, but what does it do? It’s so useful that we use it all the time, so let’s move on.

Second, inheritance,

Talk about prototype, that is definitely inseparable from inheritance this topic, when it comes to inheritance is very busy, what prototype pattern inheritance, constructor pattern inheritance, object pattern inheritance, attribute copy pattern inheritance, multiple inheritance, parasitic inheritance, combination inheritance, parasitic combinatorial inheritance…… What the hell is this? So much. Does it look like a headache?

I personally classify them as prototype, constructor and object, and the other inheritance methods are based on a combination of these three methods, but this is just my personal understanding, so let’s start.

2.1 prototype chain

When it comes to inheritance, it’s definitely the prototype chain, because the prototype chain is the primary method of inheritance.

Let’s briefly review the relationship between constructors, prototypes, and instances: each constructor has a prototype object that contains a pointer to the constructor, and the instance contains an internal pointer to the prototype object (__proto__). So, what if we set the prototype object equal to another instance object? Obviously, the stereotype object will now contain a pointer to another stereotype (__proto__), which in turn will contain a pointer to another constructor (constructor). If another prototype is another instance of an object, then the relationship is still true, and so on and so on, forming a chain of instances and prototypes. This is called the prototype chain, as shown here:

So don’t mess it up here, just make sure you understand this, but if you go down here, you’re actually assigning somebody else’s instance to the prototype of our constructor, so that’s level one, and then if the prototype of somebody else’s instance is another person’s instance, wouldn’t that be the same thing? So that’s the second layer, and then if there’s a third person, there’s another layer, and you have a chain of archetypes that are connected.

All right, if you’ve seen this, you’ve understood the chain, so let’s start with inheritance.

2.2 Inheritance Mode

There are multiple forms of inheritance. Let’s take one by one and compare the advantages and disadvantages of each.

Note: Since most inheritance relies on prototypes and chains of prototypes, I’ll name the inheritance this way when it relies on other methods so it doesn’t look too complicated.

1. Construction-based

Let’s define three constructors:

// Constructor A
function A() {
    this.name = 'A';
};
A.prototype.say = function() {
    return this.name;
};
// Constructor B
function B() {
    this.name = 'B';
};
// Constructor C
function C(width, height) {
    this.name = 'C';
    this.width = width;
    this.height = height;
    this.getArea = function() {
        return this.width * this.height;
    };
};
Copy the code

Let’s try inheritance:

B.prototype = new A();
C.prototype = new B();
Copy the code

The stereotype chain: the stereotype of constructor B is assigned an instance of constructor A, and then the stereotype of C is assigned an instance of constructor B.

Then we use the C constructor to create an instance object:

let c1 = new C(2.6);
console.log(c1);   // {name: "C", width: 2, height: 6, getArea: ƒ}
console.log(c1.name);  // 'C'
console.log(c1.getArea()); / / 12
console.log(c1.say());  // 'C'
Copy the code

C1 has a “say” method. I’m glad to hear that. How did it do that? Let’s stroke this:

  • (1) first of all,CCreates an “empty” object;
  • (2) then this refers to the “empty” object;
  • 3 c1. __proto__ pointing C.p rototype;
  • (4) Assign a value to this objectname,width,height,getAreaThese four self attributes;
  • (5) Return this objectc1Instance object;
  • ⑥ Then printconsole.log(c1)andconsole.log(c1.name).console.log(c1.getArea())Are easy to understand;
  • 7) thenconsole.log(c1.say())We have to find itsayMethod, js engine firstc1I looked for him, didn’t find him, andc1.__proto__This mysterious link is toCConstructor of the prototype, and then goC.prototypeLook up, and then we haveC.prototype = new B()That is to say to goBConstructor instance object, or no, that continues, again throughnew B().__proto__Go to theBOn the prototype, and then we are written thereB.prototype = new A();That is to goACreated instance object to find, no, then run againAConstructor to find the prototype, OK! Found!

This process is equivalent to: c1 — → C. rototype — → new B() — → B. rototype — → new A() — → A. rototype

This is a construction-based inheritance process, which is actually a lookup process, but did you find anything?

There are two problems with the above approach: the first is the direction of constructor.

New A() (); b.constructor (); b.constructor (); b.constructor (); b.constructor (); Let’s see:

console.log(B.prototype.constructor);  / / ƒ (A) {}
let b1 = new B();
console.log(b1.constructor);   / / ƒ (A) {}
Copy the code

In this case, we find that not only b. constructor. Constructor points to A, but also to B1. Don’t forget that the constructor attribute in B1 is shared by B. Constructor.

But now why do they point to A? Because B. rototype is replaced by new A(), what’s in new A()? B. rototype and new A()

A = {
    prototype:{
        constructor: A
    }
};

new A() = {
    name: 'A'.say: function() {
        return this.name;
    },
    constructor: A       // shared by the reference to __proto__} B = {prototype: {prototype:constructor: B
    }
};

New A() = new A();B = {prototype: {prototype:name: 'A'.say: function() {
            return this.name;
        },
        constructor: A   // So it's going to be A}};Copy the code

So we need to manually correct the orientation of B. construct. constructor, as well as the orientation of C. construct. constructor:

B.prototype = new A();
B.prototype.constructor = B;
C.prototype = new B();
C.prototype.constructor = C;
Copy the code

The first problem solved, to the second problem: efficiency.

When we create an object using a constructor, its properties are added to this. And it can be inefficient when the added properties don’t actually change from instance to instance. For example, in the above example, the A constructor is defined as follows:

function A() {
    this.name = 'A';
    this.say = function() {
        return this.name;
    };
};
Copy the code

This implementation means that each instance we create with new A() will have A brand new name and say property and A separate storage space in memory. So we should consider putting these properties into a prototype and sharing them:

// Constructor A
function A() {};
A.prototype.name = 'A';
A.prototype.say = function() {
    return this.name;
};

// Constructor B
function B() {};
B.prototype.name = 'B';

// Constructor C
function C(width, height) {  // The width and height properties vary with the parameters, so there is no need to change to the shared properties
    this.width = width;
    this.height = height;
};
C.prototype.name = 'C';
C.prototype.getArea = function() {
    return this.width * this.height;
};
Copy the code

This way, some of the properties in the instance created by the constructor are no longer private, but are shared within the prototype. Now let’s try:

let test1 = new A();
let test2 = new A();
console.log(test1.say === test2.say);  // True is not equal until the shared property is changed
Copy the code

Although this is usually more efficient, it is only for immutable properties in the instance, so we should also consider which properties are suitable for sharing and which are suitable for private when defining the constructor (and be sure to inherit before extending and modifying the original constructor).

2. Prototype-based approach

As it did with the above, in the efficiency consideration, as much as possible, we should add some reusable properties and methods to the prototype, so we just rely on the prototype can complete build of inheritance, as a result of the prototype on attributes are reusable, it also means that inherit from the prototype is much better than on the instance inheritance, And since all the properties that need to be inherited are in the stereotype, why would it be inefficient to generate an instance and then inherit unwanted private properties from the generated instance? So we simply discard the instance and inherit from the prototype:

// Constructor A
function A() {};
A.prototype.name = 'A';
A.prototype.say = function() {
    return this.name;
};

// Constructor B
function B() {};
B.prototype = A.prototype;  // Constructor (); // constructor ()
B.prototype.constructor = B; 
B.prototype.name = 'B';

// Constructor C
function C(width, height) {  // The width and height properties vary with the parameters, so there is no need to change to the shared properties
    this.width = width;
    this.height = height;
};
C.prototype = B.prototype;
C.prototype.constructor = C; // Constructor (); // constructor ()
C.prototype.name = 'C';
C.prototype.getArea = function() {
    return this.width * this.height;
};
Copy the code

Well, it feels a lot more efficient, and a lot more pleasing to the eye, so let’s try it:

let b2 = new B();
console.log(b2.say());  // 'C'
Copy the code

(it’s even… Isn’t it supposed to print out B? How is it different from my inner little perfect?

A, B, and C all share the same stereotype, which causes A reference problem. The name attribute of A, B, and C is changed, so that the name attribute of A, B, and C is ‘C’.

There is no way to have the best of both worlds, I want efficiency, and do not want to be controlled by others, pa! Why don’t you combine the two methods? !

3. Combine the constructor approach with the prototype approach

I want to be quick, but I don’t want to be caught. How about a third party? (It sounds weird). We use a temporary constructor (so also called a temporary constructor) between them as a bridge to break the relationship between the younger brother and the older brother (broken legs), and then we can work together efficiently:

// Constructor A
function A() {};
A.prototype.name = 'A';
A.prototype.say = function() {
    return this.name;
};

// Constructor B
function B() {};
let X = function() {};   // Create a new constructor for the "empty" property
X.prototype = A.prototype;  // Point X's prototype to A's prototype
B.prototype = new X();  // the prototype of B points to the instance object created by X
B.prototype.constructor = B;  // Remember to correct the direction
B.prototype.name = 'B';       / / extension

// Constructor C
function C(width, height) {  // The width and height properties vary with the parameters, so there is no need to change to the shared properties
    this.width = width;
    this.height = height;
};
/ / same as above
let Y = function() {};  
Y.prototype = B.prototype;
C.prototype = new Y();
C.prototype.constructor = C;
C.prototype.name = 'C';
C.prototype.getArea = function() {
    return this.width * this.height;
};
Copy the code

Now how about this:

let c3 = new C;
console.log(c3.say());  // C
Copy the code

Steady! So we are neither directly inherited instance attributes, but inherit prototype Shared properties, but also through the X and Y the two “empty” attribute constructor to take A and B on the Shared attribute to filter out (as new X than new A () () the generated instances, because X is empty, so the generated objects no private property, However, new A() may have private properties. Since it is private, it does not need to be inherited, so new A() may have efficiency problems and unnecessary inherited properties.

4. Object-based approach

This object-based approach actually includes several ways, because they are related to the object, so I collectively called the object approach, as follows:

① In the manner of receiving objects

function create(o) {  // o is the parent object to inherit
    function F() {};
    F.prototype = o;
    return new F();  // Return an instance object
};
let a = {
    name: 'better'
};
console.log(create(a).name);  // 'better'
Copy the code

This method is to accept a parent object and return an instance, and then achieve the effect of inheritance, any sense of deja vu? Isn’t that the lower version of object.create ()? Those who are interested can learn more about it. So this approach should also be called “prototype inheritance,” because it’s based on modifying prototypes, but it’s related to objects, so I’m going to put it in the object approach, so it’s easier to classify.

(2) Copy object properties

/ / father prototype properties that are directly related to the copied, benefit is the Child. The prototype. The constructor is not reset, but this approach applies only to contain only the basic data types of objects, and the parent object overrides Child objects of the same attributes
function extend(Child, Parent) {   // Child and Parent are constructors
    let c = Child.prototype;
    let p = Parent.prototype;
    for (let i inp) { c[i] = p[i]; }};Copy the code
// This is a simple way to copy the properties directly, but there is a problem with the reference type
function extendCopy(p) {   // p is an inherited object
    let c = {};
    for (let i in p) {
        c[i] = p[i];
    }
    return c;
};
Copy the code
// The above extendCopy is called a shallow copy and does not solve the problem of reference types. Now we use a deep copy, which solves the problem of reference type attributes because no matter how many reference types you have, they are copied one by one
function deepCopy(p, c) {  // C and p are objects
    c = c || {};
    for (let i in p) {
        if (p.hasOwnProperty[i]) {   // Exclude inherited attributes
            if (typeof p[i] === 'object') {  // Resolve the reference type
                c[i] = Array.isArray(p[i]) ? [] : {};
                deepCopy[p[i], c[i]];
            } else{ c[i] = p[i]; }}}return c;
}
Copy the code

(3) Copying properties of multiple objects

// In this way, you can copy multiple object properties at once. This is also called multiple inheritance
function multi() {
    let n = {},
    stuff,
    j = 0,
    len = arguments.length;
    for (j = 0; j < len; j++) {
        stuff = arguments[j];
        for (let i in stuff) {
            if(stuff.hasOwnProperty(i)) { n[i] = stuff[i]; }}}return n
};
Copy the code

(4) The way to absorb object attributes and expand them

This approach should actually be called “parasitic inheritance”, which at first glance seems abstract, but is really just that, so I’ll put it in the object approach as well:

// The function that created the object absorbs the properties of other objects, then extends them and returns them
let parent = {
    name: 'parent'.toString: function() {
        return this.name; }};function raise() {
    let that = create(parent);  // Use the create function we wrote earlier
    that.other = 'Once in a blue moon! '; // What I learned today is to show off the ugliness
    return that;
}
Copy the code

Isn’t there a lot of ways to relate objects? But it’s all about the properties of the object, so that makes sense, so let’s move on.

5. Constructor borrowing

This method can also be classified as the constructor method, but it’s a bit slippery, so let’s take it out on its own (this is the last one, I promise).

Let’s take the old function A that we defined before and stir it:

// Constructor A
function A() {
    this.name = 'A';
};
A.prototype.say = function() {
    return this.name;
};

// Constructor D
function D() {
    A.apply(this.arguments);  // This is equivalent to borrowing the constructor from A to create the attributes in A to D, namely the name and say attributes
};
D.prototype = new A();  // This is responsible for getting the attributes on the A prototype
D.prototype.name = 'D';  // Extend after inheritance
Copy the code

In this way, the two steps have solved both the self property and the prototype property of A? Simple and perfect!

A.ply (this, arguments) has perfectly changed A attribute to D, but d.prototype = new A() inherits A attribute again. This is useless, since we only want the attribute on the stereotype. So why don’t we just copy it?

// Constructor A
function A() {
    this.name = 'A';
};
A.prototype.say = function() {
    return this.name;
};

// Copy the previously defined property function
function extend2(Child, Parent) {
    let c = Child.prototype;
    let p = Parent.prototype;
    for (let i inp) { c[i] = p[i]; }};// Constructor D
function D() {
    A.apply(this.arguments);  // This is equivalent to borrowing the constructor from A to create the attributes in A to D, namely the name and say attributes
};
extend2(D, A);  // Copy the attributes of stereotype A to stereotype D
D.prototype.name = 'D';  // Extend after inheritance

let d1 = new D();
console.log(d1.name);  // 'A'
console.log(d1.__proto__.name)  // This indicates that the name attribute is created, not inherited
Copy the code

(it’s even… In fact, there are other inheritance methods, or not to write, for fear of being beaten, but actually come and go is based on prototypes, constructors, objects and these several ways to do it, I personally classify them this way, after all, seven seconds memory can not put, embarrassed.

The last

Here, finally swallowed the last breath, bah, a sigh of relief. Thank you for seeing the end, hope to help you, have written wrong place also please give advice, like to follow a wave of it, the follow-up will continue to update.

Making the source code