Prototyping Inheritance in JavaScript

Reprinting is allowed, but please indicate the source: http://blog.csdn.net/sysuzjz/article/details/42291399

JavaScript is an object-oriented language. There is a classic saying in JavaScript that everything is an object. Since it is object-oriented, it has three characteristics of object-oriented: encapsulation, inheritance, and polymorphism. Here is the inheritance of JavaScript, and the other two will be discussed later.

Inheritance in JavaScript is not the same as inheritance in C++. Inheritance in C++ is class-based, while inheritance in JavaScript is prototype-based.

Now comes the question.

What is a prototype?

We can refer to the class in C++ for the prototype, which also saves the properties and methods of the object. For example we write a simple object
function Animal(name) {
    this.name = name;
}
Animal.prototype.setName = function(name) {
    this.name = name;
}
var animal = new Animal("wangwang");


We can see that this is an object Animal, which has a property name and a method setName. It should be noted that once the prototype is modified, such as adding a method, all instances of the object will share this method. E.g
function Animal(name) {
    this.name = name;
}
var animal = new Animal("wangwang");

At this time animal only has the name attribute. If we add the sentence,
Animal.prototype.setName = function(name) {
    this.name = name;
}
At this time, animal will also have a setName method.

Inherit this copy - start with an empty object

We know that one of the basic types of JS is called object, and its most basic instance is an empty object, that is, an instance generated by directly calling new Object(), or declared with a literal { }. Empty objects are "clean objects" with only predefined properties and methods, while all other objects inherit from empty objects, so all objects have these predefined properties and methods.
A prototype is actually an instance of an object. The meaning of the prototype means: if the constructor has a prototype object A, the instances created by the constructor must be copied from A. Since the instance is copied from the object A, the instance must inherit all the properties, methods and other properties of A.
So, how is replication achieved?

Method 1: Construct copy

Each time an instance is constructed, an instance is copied from the prototype, and the new instance occupies the same memory space as the prototype. Although this makes obj1 and obj2 "exactly identical" to their prototypes, it is also very uneconomical - the consumption of memory space will increase rapidly. As shown in the figure:

Method 2: Copy-on-write

This strategy comes from a technique that consistently deceives systems: copy-on-write. A typical example of this kind of deception is a dynamic link library (DDL) in an operating system, whose memory area is always copy-on-write. As shown in the figure:

We only need to specify that obj1 and obj2 are equivalent to their prototypes in the system, so when reading, we only need to follow the instructions to read the prototype. When we need to write the properties of an object (such as obj2), we copy an image of a prototype and make subsequent operations point to the image. As shown in the figure:

The advantage of this method is that we do not need a lot of memory overhead when creating instances and reading properties, and only use some code to allocate memory when writing for the first time, and bring some code and memory overhead. But after that there is no longer this overhead, because the efficiency of accessing the image is the same as accessing the prototype. However, this method is not more economical than the previous method for systems that do frequent writes.

Method 3: Read Traversal

This approach changes the granularity of replication from prototypes to members. The characteristics of this method are: only when writing a member of an instance, copy the member's information to the instance image. When writing an object property, such as (obj2.value=10), a property value named value is generated and placed in the member list of the obj2 object. Look at the picture:

It can be found that obj2 is still a reference to the prototype, and no object instance of the same size as the prototype is created during the operation. In this way, the write operation does not result in a large amount of memory allocation, so the use of memory is economical. The difference is that obj2 (and all object instances) needs to maintain a list of members. This member list follows two rules:
  1. Guaranteed to be accessed first when reading
  2. If no property is specified in the object, attempts to traverse the entire prototype chain of the object until the prototype is empty or the property is found.
The prototype chain will be discussed later.
Obviously, among the three methods, read traversal is the best performance. So, JavaScript's prototypal inheritance is read traversal.

constructor

Those who are familiar with C++ will definitely be puzzled after reading the code of the top object. It is easy to understand without the class keyword. After all, there is the function keyword, and the keywords are different. But what about constructors?
In fact, JavaScript also has a similar constructor, just called a constructor. When using the new operator, the constructor has actually been called, and this is bound to the object. For example, we use the following code
var animal = Animal("wangwang");
animal will be undefined. Some would say that no return value is of course undefined. Then if you change the object definition of Animal:
function Animal(name) {
    this.name = name;
    return this;
}
Guess what animal is now?
At this time, the animal becomes a window, the difference is that the window is extended, so that the window has a name attribute. This is because if this is not specified, it points to window by default, that is, the top-level variable. The constructor can only be called correctly if the new keyword is called. So, how to prevent users from missing the new keyword? We can make a small modification:
function Animal(name) {
    if(!(this instanceof Animal)) {
        return new Animal(name);
    }
    this.name = name;
}
That way, nothing is lost.
Constructors are also useful for indicating which object the instance belongs to. We can use instanceof to judge, but instanceof will return true for ancestor objects and real objects when inheriting, so it is not suitable. When the constructor is called by new, it defaults to the current object.
console.log(Animal.prototype.constructor === Animal); // true
We can think differently: prototype has no value at the beginning of the function, and the implementation may be the following logic
// set __proto__ is a built-in member of the function, and get_prototyoe() is its method
var __proto__ = null;
function get_prototype() {
    if(!__proto__) {
        __proto__ = new Object();
        __proto__.constructor = this;
    }
    return __proto__;
}
The advantage of this is to avoid creating an object instance every time a function is declared, saving overhead.
The constructor can be modified, which will be discussed later.

Prototype-Based Inheritance

I believe everyone almost knows what inheritance is, so there is no lower limit of IQ.
There are several types of JS inheritance, here are two

1. Method 1

This method is the most commonly used and has better security. We first define two objects
function Animal(name) {
    this.name = name;
}
function Dog(age) {
    this.age = age;
}
var dog = new Dog(2);

To construct inheritance is simple, point the prototype of the child object to the instance of the parent object (note that it is an instance, not an object)
Dog.prototype = new Animal("wangwang");
At this point, dog will have two properties, name and age. And if you use the instanceof operator on dog
console.log(dog instanceof Animal); // true
console.log(dog instanceof Dog); // true
This achieves inheritance, but there is a small problem
console.log(Dog.prototype.constructor === Animal); // true
console.log(Dog.prototype.constructor === Dog); // false
It can be seen that the object pointed to by the constructor has changed, which is not in line with our purpose, and we cannot judge who our new instance belongs to. Therefore, we can add a sentence:
Dog.prototype.constructor = Dog;
Look again:
console.log(dog instanceof Animal); // false
console.log(dog instanceof Dog); // true
done. This method is part of the maintenance of the prototype chain, which will be explained in detail below.

2. Method 2

This approach has its pros and cons, but it does more harm than good. Look at the code first
<pre name="code" class="javascript">function Animal(name) {
    this.name = name;
}
Animal.prototype.setName = function(name) {
    this.name = name;
}
function Dog(age) {
    this.age = age;
}
Dog.prototype = Animal.prototype;
 
    
   
   This achieves the copy of the prototype. 
  
The advantage of this method is that there is no need to instantiate objects (compared to method 1), saving resources. The disadvantages are also obvious, except for the same problem as above, that is, the constructor points to the parent object, and can only copy the properties and methods declared by the parent object with the prototype. That is to say, in the above code, the name property of the Animal object cannot be copied, but the setName method can be copied. The most fatal thing is that any modification to the prototype of the child object will affect the prototype of the parent object, that is, the instances declared by the two objects will be affected. Therefore, this method is not recommended.

Prototype chain

Anyone who has written inheritance knows that inheritance can be multi-layered. In JS, this constitutes the prototype chain. The prototype chain has been mentioned many times above, so what is the prototype chain?
An instance should at least have a proto property pointing to the prototype, which is the basis of the object system in JavaScript. However, this property is invisible, we call it the "internal prototype chain" to distinguish it from the "constructor prototype chain" (that is, what we usually call the "prototype chain") composed of the prototype of the constructor.
We first construct a simple inheritance relationship according to the above code:
function Animal(name) {
    this.name = name;
}
function Dog(age) {
    this.age = age;
}
var animal = new Animal("wangwang");
Dog.prototype = animal;
var dog = new Dog(2);

As a reminder, as mentioned earlier, all objects inherit from empty objects.
So, we construct a prototype chain:


We can see that the prototype of the child object points to the instance of the parent object, forming the constructor prototype chain. The internal proto object of the child instance is also an instance that points to the parent object, forming the internal prototype chain. When we need to find a property, the code is similar to

function getAttrFromObj(attr, obj) {
    if(typeof(obj) === "object") {
        var proto = obj;
        while(proto) {
            if(proto.hasOwnProperty(attr)) {
                return proto[attr];
            }
            proto = proto .__ proto__;
        }
    }
    return undefined;
}

In this example, if we look up the name attribute in dog, it will look in the member list in dog. Of course, it will not be found, because now the member list of dog only has the item age. Then it will continue to search along the prototype chain, that is, the instance pointed to by .proto, that is, in the animal, find the name attribute and return it. If it is looking for a non-existent property, when it cannot be found in animal, it will continue to search along .proto, find an empty object, and continue to search along .proto after it is not found, and the . proto points to null, looking for exit.

Prototype chain maintenance

We raised a question when we talked about prototypal inheritance just now. When using method 1 to construct inheritance, the constructor of the child object instance points to the parent object. The advantage of this is that we can access the prototype chain through the constructor property, and the disadvantage is obvious. an object whose instances should point to itself, that is
(new obj()).prototype.constructor === obj;
Then, when we override the prototype property, the constructor of the instance generated by the child object does not point to itself! This goes against the original intent of the constructor.
We mentioned a solution above:
Dog.prototype = new Animal("wangwang");
Dog.prototype.constructor = Dog;
It looks like there is no problem. But in fact, this brings a new problem, because we will find that we can't go back to the prototype chain, because we can't find the parent object, and the .proto property of the internal prototype chain is inaccessible.
Therefore, SpiderMonkey provides an improvement: add a property named __proto__ to any created object, which always points to the prototype used by the constructor. In this way, any modification to the constructor will not affect the value of __proto__, which is convenient for maintaining the constructor. However, there are two more problems:
  1. __proto__ is overridable, which means there are still risks when using it
  2. __proto__ is a special treatment of spiderMonkey and cannot be used in other engines (such as JScript).
We have another way, that is to keep the prototype's constructor properties, and initialize the instance's constructor properties in the subclass constructor function. code show as below:
Override child objects
function Dog(age) {
    this.constructor = arguments.callee;
    this.age = age;
}
Dog.prototype = new Animal("wangwang");
In this way, the constructors of all child object instances correctly point to the object, while the prototype constructor points to the parent object. Although this method is relatively inefficient, because the constructor property must be rewritten every time an instance is constructed, there is no doubt that this method can effectively solve the previous contradiction.
ES5 takes this into account and completely solves the problem: Object.getPrototypeOf() can be used to get the real prototype of an object at any time without having to access the constructor or maintain an external prototype chain. Therefore, looking for object properties as described in the previous section, we can rewrite it as follows:
function getAttrFromObj(attr, obj) {
    if(typeof(obj) === "object") {
        do {
            var proto = Object.getPrototypeOf(dog);
            if(proto[attr]) {
                return proto[attr];
            }
        }
        while(proto);
    }
    return undefined;
}
Of course, this method can only be used in browsers that support ES5. For backward compatibility, we still need to consider the previous method. A more appropriate method is to integrate and encapsulate these two methods, which I believe readers are very good at, so I won't make a fool of myself here.


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325687209&siteId=291194637