JavaScript is an object-oriented language. There is a very classic saying in JavaScript, everything is an object. Since it is object-oriented, it has three major characteristics of object-oriented: encapsulation, inheritance, and polymorphism. What we are talking about here is JavaScript inheritance, and we will talk about the other two later.
JavaScript inheritance is different from C inheritance. C inheritance is based on classes, while JavaScript inheritance is based on prototypes.
Now comes the problem.
What is a prototype? For the prototype, we can refer to the class in C, which also saves the properties and methods of the object. For example, let’s write a simple object
We can see that this is an object Animal, which has an attribute 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. For example
At this time, animal only has the name attribute. If we add a sentence,
At this time, animal will also have a setName method.
Inherit this copy - starting from the empty object. We know that among the basic types of JS, there is one called object, and its most basic instance is the empty object, that is, the instance generated by directly calling new Object(), or It is declared using the literal { }. An empty object is a "clean object" with only predefined properties and methods, and all other objects inherit from the empty object, so all objects have these predefined properties and methods. A prototype is actually an object instance. The meaning of prototype is: if the constructor has a prototype object A, then the instances created by the constructor must be copied from A. Since the instance is copied from object A, the instance must inherit all of A's properties, methods, and other properties. So, how is replication achieved? Method 1: Construction copying Each time an instance is constructed, an instance is copied from the prototype. The new instance and the prototype occupy the same memory space. Although this makes obj1 and obj2 "completely consistent" with their prototypes, it is also very uneconomical - the consumption of memory space will increase rapidly. As shown in the picture:
Method 2: Copy-on-write This strategy comes from a consistent trick to the system: copy-on-write. A typical example of this kind of deception is the dynamic link library (DDL) in the operating system, whose memory area is always copied on write. As shown in the picture:
We only need to indicate in the system that obj1 and obj2 are equal to their prototypes, so that when reading, we only need to follow the instructions to read the prototypes. When we need to write the properties of an object (such as obj2), we copy a prototype image and point future operations to this image. As shown in the picture:
The advantage of this method is that we don’t need a lot of memory overhead when creating instances and reading attributes. We only use some code to allocate memory when writing for the first time, which brings some code and memory overhead. . But there is no such overhead anymore, because the efficiency of accessing the image is the same as accessing the prototype. However, for systems that frequently perform write operations, this method is no more economical than the previous method. Method 3: Read traversal This method changes the replication granularity from prototype to member. The characteristic of this method is that only when writing a member of an instance, the member information is copied to the instance image. When writing an object attribute, for example (obj2.value=10), an attribute value named value will be 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 pointing to the prototype, and no object instance of the same size as the prototype is created during the operation. In this way, write operations do not result in large memory allocations, so memory usage becomes economical. The difference is that obj2 (and all object instances) need to maintain a member list. This member list follows two rules: It is guaranteed to be accessed first when reading. If no attribute is specified in the object, an attempt is made to traverse the entire prototype chain of the object until the prototype is empty or the attribute is found. The prototype chain will be discussed later. Obviously, among the three methods, read traversal has the best performance. Therefore, JavaScript's prototypal inheritance is read traversal. constructor People who are familiar with C will definitely be confused after reading the code of the top object. It’s easier to understand without the class keyword. After all, there is the function keyword, which is just a different keyword. But what about constructors? In fact, JavaScript also has a similar constructor, but it is called a constructor. When using the new operator, the constructor has actually been called and this is bound to an object. For example, we use the following code
animal will be undefined. Some people will say that no return value is of course undefined. Then if you change the object definition of Animal:
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 this defaults to window, which is the top-level variable, if it is not specified. Only by calling the new keyword can the constructor be called correctly. So, how to prevent users from missing the new keyword? We can make some small changes:
This way you’ll be foolproof. The constructor is also used to indicate which object the instance belongs to. We can use instanceof to judge, but instanceof returns true for both ancestor objects and real objects during inheritance, so it is not suitable. When constructor is called with new, it points to the current object by default.
We can think differently: prototype has no value at all when the function is initialized. The implementation may be the following logic
//Set __proto__ as a built-in member of the function, and get_prototyoe() is its method
The advantage of this is that it avoids 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 knows almost what inheritance is, so I won’t show off the lower limit of IQ.
There are several types of JS inheritance, here are two types
1. Method 1 This method is the most commonly used and has better security. Let’s first define two objects
To construct inheritance is very simple, point the prototype of the child object to the instance of the parent object (note that it is an instance, not an object)
At this time, dog will have two attributes, name and age. And if you use the instanceof operator
on dogIn this way, inheritance is achieved, but there is a small problem
You can see that the object pointed to by the constructor has changed, which does not meet our purpose. We cannot judge who our new instance belongs to. Therefore, we can add a sentence:
Let’s take a look again:
done. This method is part of the maintenance of the prototype chain and will be explained in detail below. 2. Method 2 This method has its advantages and disadvantages, but the disadvantages outweigh the advantages. Let’s look at the code first
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 prototype copying.
The advantage of this method is that it does not need to instantiate objects (compared to method 1), which saves resources. The disadvantages are also obvious. In addition to the same problem as above, that is, the constructor points to the parent object, it can only copy the properties and methods declared by the prototype of the parent object. In other words, in the above code, the name attribute 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 both objects will be affected. Therefore, this method is not recommended.
Prototype Chain
Everyone who has written inheritance knows that inheritance can be inherited at multiple levels. 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 attribute 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" composed of the constructor's prototype (which is what we usually call the "prototype chain"). Let’s first construct a simple inheritance relationship according to the above code:
As a reminder, as mentioned before, all objects inherit empty objects. Therefore, we constructed 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 also points to the instance of the parent object, forming an internal prototype chain. When we need to find a certain attribute, the code is similar to
In this example, if we look for the name attribute in dog, it will be searched in the member list in dog. Of course, it will not be found because the member list of dog now only has age. Then it will continue to search along the prototype chain, that is, the instance pointed to by .proto, that is, in animal, it will find the name attribute and return it. If it is looking for a property that does not exist, and cannot find it in animal, it will continue to search along .proto and find an empty object. If it cannot find it, it will continue to search along .proto, and the empty object. proto points to null, looking for exit.
Maintenance of the prototype chain We raised a question when we just talked about prototypal inheritance. 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 attribute, but the disadvantages are also obvious. An object, the instance it generates should point to itself, that is,
Then, when we override the prototype property, the constructor of the instance generated by the sub-object does not point to itself! This goes against the original intention of the constructor. We mentioned a solution above:
Looks like there is no problem. But in fact, this brings a new problem, because we will find that we can't backtrack 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 improved solution: adding an attribute 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 makes it easier to maintain the constructor.
However, there are two more problems:
__proto__ is overridable, which means there are still risks when using it
__proto__ is a special processing of spiderMonkey and cannot be used in other engines (such as JScript).
We also have another way, which is to keep the prototype's constructor properties and initialize the instance's constructor properties within the subclass constructor function.
The code is as follows: Rewrite the sub-object
In this way, the constructors of all child object instances correctly point to the object, and the prototype's constructor points to the parent object. Although this method is relatively inefficient because the constructor attribute 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 situation into consideration and completely solves this problem: you can use Object.getPrototypeOf() at any time to get the real prototype of an object without having to access the constructor or maintain an external prototype chain. Therefore, to find object properties as mentioned in the previous section, we can rewrite it as follows:
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. I believe readers are very good at this, so I won’t show off here.