Featured image of post Prototype Chaining

Prototype Chaining

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue= function() {
    return this.property;
}

function SubType() {
    this.subproperty = false;
}

// inherit from SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperValue());// true
console.log(instance.getSubValue());// false
console.log(instance.constructor === SuperType)// true
console.log(SuperType.prototype.__proto__.constructor)// [Function: Object]
console.log(typeof SuperType.prototype.__proto__.valueOf)// function
console.log(SuperType.prototype.__proto__.__proto__)// null

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.

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.

Figure 2

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:

1
2
3
console.log(instance instanceof Object)// true
console.log(instance instanceof SuperType)// true
console.log(instance instanceof SubType)// true

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:

1
2
3
console.log(Object.prototype.isPrototypeOf(instance))// true
console.log(SuperType.prototype.isPrototypeOf(instance))// true
console.log(SubType.prototype.isPrototypeOf(instance))// true

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue= function() {
    return this.property;
}

function SubType() {
    this.subproperty = false;
}

SubType.prototype = new SuperType();

// new method
SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

// override existing method
SubType.prototype.getSuperValue = function() {
    return false;
}

var instance = new SubType();
console.log(instance.getSuperValue());// false
console.log(instance.getSubValue());// false
console.log(instance.__proto__.__proto__.constructor)// [Function: SuperType]
console.log(typeof instance.__proto__.__proto__.getSuperValue)// function
console.log(typeof instance.__proto__.__proto__.getSuperValue())// undefined
console.log(instance.__proto__.__proto__.getSuperValue.call(instance))// true

var instance2 = new SuperType();
console.log(instance2.getSuperValue());// true

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:

译文
还有一点需要提醒读者,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链,如下面的例子所示
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue= function() {
    return this.property;
}

function SubType() {
    this.subproperty = false;
}

SubType.prototype = new SuperType();

// new method
SubType.prototype = {
    getSubValue: function() {
        return this.subproperty;
    }
}

var instance = new SubType();
instance.getSuperValue();// error: TypeError: instance.getSuperValue is not a function

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.

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy