再谈JS中的原型继承
原型继承应该算是js里的最难点之一,以前确实知道怎么实现,但隔了这么久回过头想想,发现有些概念变得模糊了,虽然在coffeescript里用extends一句话就能实现,但coffee最终还是编译成js,所以还是有必要重新整理一番,温故而知新
一、关键字new与构造函数
1 | var array = new Array(10) |
我们都知道用new关键字实例化一个对象,new后面必须跟一个函数调用,也就是构造函数(constructor),构造函数用来初始化实例的属性和方法
可是具体过程是怎么样的呢?实际上只有两步:
第一步,**new创建了一个新的没有任何属性的空对象,然后将构造函数作为这个空对象的方法调用,此时构造函数中的this**关键字指向的便是这个空对象,这样一来便设置了这个空对象的所有属性,完成属性初始化
第二步,创建完这个空对象后,new又干了一件事儿,设置对象的原型__proto__,这个__proto__指向的是构造函数的prototype属性的值
每个对象都有__proto__属性,每个函数都有prototype属性,而__proto__总是指向其构造函数的prototype属性。 当一个函数被创建的时候,其prototype被自动创建并初始化,此时prototype对象只有一个属性,那便是constructor,它指回到这个被创建的函数(这就是为什么每个对象有个constructor属性的原因),添加给prototype对象的任何属性都会成为被构造函数实例化的对象的属性或方法
1 | var Dog = function(){ |
二、原型与继承
我们都知道,javascript是基于原型的继承机制,而不是基于类,但在js中我们仍可以采用类似的类层次。比如说Object类是其他所有类的超类或者父类,所有类都从Object类中继承了一些基本方法,比如toString()方法
1、子类继承父类
我的理解,类的继承实际上是我们手动模拟new的行为,大致分三步:
1 | var Dog = function(){ |
第一步,把父类的构造函数作为新创造的对象的方法调用,从而实现了构造函数的继承
第二步,把子类的原型设置成父类的一个实例,从而实现原型的继承
第三步,重新设置子类的constructor属性
实际上这里和new的步骤差不多,第三步只是为了纠正第二步造成的不良影响。第一步没啥好说的,这里重点说说第二步和第三步
Dog的实例继承了Dog所有的属性和方法,其__proto__属性指向Dog的prototype,将子类Dog2的原型设置成父类Dog的实例后便相当于继承了Dog的原型。可是Dog的实例并没有constructor属性,这样一来子类Dog2的原型就没有constructor属性了,当实例化Dog2后,会发现Dog实例化的对象dog2是有constructor属性的,坑爹的是constructor指向的却是父类Dog
这是为什么呢?因为在查询dog2.constructor的时候发现dog2本身并没有constructor属性,那么看看dog2的原型里有没有,这时候dog2的原型其实是Dog的实例,当然没有constructor属性,那么这时候只能沿着原型链往上找,最后找到了Dog的原型,可是Dog的prototype.constructor指向的是Dog,这并非我们所期望的,所以最后我们得手动把子类Dog2的原型的constructor指回Dog2
2、原型链
若是弄明白上面的内容,那么原型链还是比较好理解的,基于原型的继承并不限于一个单个的原型对象,相反,它包含了一个原型对象的链。
比如,Dog2类的实例对象dog2继承了Dog2.prototype、Dog.prototype和Object.prototype的属性,dog2–>Dog2.prototype–>Dog.prototype–>Object.prototype,当在dog2中查询某个属性时,首先查询这个对象本身,若在dog2中没有发现要查询的属性,就查询Dog2.prototype(dog2.__proto__),还没发现就再查询Dog.prototype(dog2.__proto__.__proto__),还是没发现的话最后就查询Object.prototype(dog2.__proto__.__proto___.__proto__)
3、借用方法(非继承)
有没有这样一种情况,A类继承B类,这时半路杀出个C类,A类也想把C类弄过来,实现多重继承,可是A的原型已经是B类的实例了,怎么办?
还有什么办法可以扩展A类呢,其实也很简单,还是从原型下手,不能继承,那借来用用总行吧,看下面栗子:
1 | function borrowMethods(borrowFrom, addTo) { |
4、Object.create继承
最后提下Object.create,也算与时俱进,它是ECMAScript5中新引入的方法,可以调用这个方法来创建一个新对象. 新对象的原型就是调用create方法时传入的第一个参数,而构造函数就是Object
1 | var a = {a: 1}; |