继承的本质
继承的初衷是复用。即已经有一个原始的类,希望在保留原始类的所有属性和方法的前提下,进行扩展或修改,以最大限度地降低开发成本。JavaScript实现继承,也是源自这个思想,即想办法让新的引用类型能够访问别的引用类型的所有属性和函数。
原型链实现继承
回顾一下原型对象。原型对象与普通对象本质上没有任何区别,只是可以被对应实例对象的[[prototype]]指针指向,从而让所有实例对象都可以访问原型对象的属性和函数。而正是这一个特点,与继承的本质完全一致。换一个角度看,我们可以认为,实例对象继承了原型对象,所以可以访问它的所有属性和函数。
所以,我们甚至可以说,对象与对象的原型关系,就是继承关系。
那么,怎么让某一个新定义的引用类型继承另一个已经定义好的引用类型呢?很简单,只需要让他们建立原型关系。下面看一个例子:
|
|
当调用SubType构造函数以后,会创建一个SubType实例对象,它存有SubType原型对象的指针,该原型对象默认是一个不含属性和函数的空对象。现在我们手动修改SubType构造函数的prototype指针,让它指向一个创建好的SuperType实例对象,从而通过SubType构造函数创建的实例对象的原型对象都是一个SuperType的实例对象,因此所有SubType实例对象都可以访问同一个SuperType实例对象的实例属性和方法,以及SuperType实例对象的原型对象的属性和方法。SubType原型对象的原型,就是SuperType的原型。继承就是这样实现的。
之前我们说过,所有引用类型都继承Object类型,JavaScript原生又是怎么实现这个继承关系的呢?跟上面的继承方式一样。在创建函数的时候,都会创建一个原型对象,这个原型对象初始是空白的,没有任何属性和函数,但是它却有[[prototype]]这个内部属性,指向同一个Object对象。所以,所有原型对象默认的原型都是Object。
这种原型的原型,就构成了原型链。通过不断延伸原型链,我们便可以实现多重继承。只要能够沿着原型链找到的引用类型,都可以通过instanceof操作符返回true,从而判断对象的类型。
分离实例属性与原型属性
继承实际上是让原型对象的[[prototype]]指向下一个原型对象。回到前面几讲,我们希望让所有公共的属性与函数,都放在原型上,让所有各不相同的属性都定义在实例上。在进行原型链的构造时,为了满足这个需求,我们需要对实例属性和原型属性进行分离。下面给出继承的一般方式:
|
|
我们构造一个对象让它继承SuperType.prototype,然后让这个对象作为SubType的原型,从而构造了一条原型链。而让原来在SuperType实例上定义的所有属性都定义在SubType的实例上,而方法则全部定义在原型上,从而实现了实例属性与原型属性的分离。
再看垃圾回收机制的标志清除策略
我们介绍了两条链,一条作用域链,一条原型链。回顾标志清除策略,其实搜索对象就是分别沿着这两条链寻找。通过作用域链,找到执行环境中的所有变量与函数。而通过原型链,则找到执行环境中的变量引用的所有变量与函数。
总结
实现继承,关键在于让被继承的引用类型的原型对象插入到原型链中。继承关系,实质上就是一个原型对象到另一个原型对象通过[[prototype]]指针的连接关系。