Overview
The concept most often discussed in relation to OO programming is inheritance. Many OO languages support two types of inheritance: interface inheritance, where only the method signatures are inherited, and implementation inheritance, where actual methods are inherited. Interface inheritance is not possible in ECMAScript, because, as mentioned previously, functions do not have signatures. Implementation inheritance is the only type of inheritance supported by ECMAScript, and this is done primarily through the use of prototype chaining.
Prototype Chaining
ECMA-262 describes prototype chaining as the primary method of inheritance in ECMAScript. The basic idea is to use the concept of prototypes to inherit properties and methods between two reference types. Recall { 回顾 } the relationship between constructors, prototypes, and instances: each constructor has a prototype object that points back to the constructor, and instances have an internal pointer to the prototype.
What if the prototype were actually an instance of another type? That would mean the prototype itself would have a pointer to a different prototype that, in turn, would have a pointer to another constructor. If that prototype were also an instance of another type, then the pattern would continue, forming a chain between instances and prototypes. This is the basic idea behind prototype chaining.
Implementing prototype chaining involves { 需要;包含;牵涉 } the following code pattern:
|
|
This code defines two types: SuperType and SubType. Each type has a single property and a single method. The main difference between the two is that SubType inherits from SuperType by creating a new instance of SuperType and assigning it to SubType.prototype.
This overwrites the original prototype and replaces it with a new object, which means that all properties and methods that typically exist on an instance of SuperType now also exist on SubType.prototype.
After the inheritance takes place, a method is assigned to SubType.prototype, adding a new method on top of what was inherited from SuperType. The relationship between the instance and both constructors and prototypes is displayed in Figure 1.
Instead of using the default prototype of SubType, a new prototype is assigned. That new prototype happens to be an instance of SuperType, so it not only gets the properties and methods of a SuperType instance but also points back to the SuperType’s prototype. So instance points to SubType.prototype, and SubType.prototype points to SuperType.prototype.
Note that the getSuperValue() method remains on the SuperType.prototype object, but property ends up on SubType.prototype. That’s because getSuperValue() is a prototype method, and property is an instance property. SubType.prototype is now an instance of SuperType, so property is stored there. Also note that instance.constructor points to SuperType, because the constructor property on the SubType.prototype was overwritten.
Prototype chaining extends to the prototype search mechanism described earlier. As you may recall, when a property is accessed in read mode on an instance, the property is first searched for on the instance. If the property is not found, then the search continues to the prototype. When inheritance has been implemented via prototype chaining, that search can continue up the prototype chain. In the previous example, for instance, a call to instance.getSuperValue() results in a three-step search: 1) the instance, 2) SubType.prototype, and 3) SuperType.prototype, where the method is found. The search for properties and methods always continues until the end of the prototype chain is reached.
Default Prototypes
In reality { 事实上 }, there is another step in the prototype chain. All reference types inherit from Object by default, which is accomplished through prototype chaining. The default prototype for any function is an instance of Object, meaning that its internal prototype pointer points to Object.prototype
. This is how custom types inherit all of the default methods such as toString()
and valueOf()
. So the previous example has an extra layer of inheritance. Figure 2 shows the complete prototype chain.
SubType inherits from SuperType
, and SuperType
inherits from Object
. When instance.toString()
is called, the method being called actually exists on Object.prototype
.
Prototype and Instance Relationship
The relationship between prototypes and instances is discernible { 可辨别的 } in two ways. The first way is to use the instanceof
operator, which returns true whenever an instance is used with a constructor that appears in its prototype chain, as in this example:
|
|
Here, the instance object is technically an instance of Object, SuperType, and SubType because of the prototype chain relationship. The result is that instanceof returns true for all of these constructors.
The second way to determine this relationship is to use the isPrototypeOf()
method. Each prototype in the chain has access to this method, which returns true for an instance in the chain, as in this example:
|
|
Working with Methods
Often a subtype will need to either override a supertype method or introduce new methods that don’t exist on the supertype. To accomplish this, the methods must be added to the prototype after the prototype has been assigned. Consider this example:
|
|
In this code, the highlighted area shows two methods. The first is getSubValue()
, which is a new method on the SubType. The second is getSuperValue()
, which already exists in the prototype chain but is being shadowed here. When getSuperValue()
is called on an instance of SubType
, it will call this one, but instances of SuperType
will still call the original. The important thing to note is that both of the methods are defined after the prototype has been assigned as an instance
of SuperType
.
Another important thing to understand is that the object literal approach to creating prototype methods cannot be used with prototype chaining, because you end up overwriting the chain. Here’s an example:
|
|
In this code, the prototype is reassigned to be an object literal after it was already assigned to be an instance of SuperType
. The prototype now contains a new instance of Object
instead of an instance of SuperType
, so the prototype chain has been broken — there is no relationship between SubType
and SuperType
.
Problems with Prototype Chaining
Even though prototype chaining is a powerful tool for inheritance, it is not without its issues { 它并非没有问题 }. The major issue revolves around prototypes that contain reference values. Recall from earlier that prototype properties containing reference values are shared with all instances; this is why properties are typically defined within the constructor instead of on the prototype. When implementing inheritance using prototypes, the prototype actually becomes an instance of another type, meaning that what once were instance properties are now prototype properties. The issue is highlighted by the following example:
|
|
In this example, the SuperType
constructor defines a property colors that contains an array (a reference value). Each instance of SuperType
has its own colors property containing its own array. When SubType
inherits from SuperType
via prototype chaining, SubType.prototype
becomes an instance of SuperType
and so it gets its own colors property, which is akin { 相似的;类似的 } to specifically creating SubType.prototype.colors
. The end result: all instances of SubType
share a colors property. This is indicated as the changes made to instance1.colors are reflected on instance2.colors.
A second issue with prototype chaining is that you cannot pass arguments into the supertype constructor when the subtype instance is being created. In fact, there is no way to pass arguments into the supertype constructor without affecting all of the object instances. Because of this and the aforementioned issue with reference values on the prototype, prototype chaining is rarely used alone.