再谈JS中的原型继承

原型继承应该算是js里的最难点之一,以前确实知道怎么实现,但隔了这么久回过头想想,发现有些概念变得模糊了,虽然在coffeescript里用extends一句话就能实现,但coffee最终还是编译成js,所以还是有必要重新整理一番,温故而知新

一、关键字new与构造函数

1
2
var array = new Array(10)
var today = new Date()

我们都知道用new关键字实例化一个对象,new后面必须跟一个函数调用,也就是构造函数(constructor),构造函数用来初始化实例的属性和方法

可是具体过程是怎么样的呢?实际上只有两步:

第一步,**new创建了一个新的没有任何属性的空对象,然后将构造函数作为这个空对象的方法调用,此时构造函数中的this**关键字指向的便是这个空对象,这样一来便设置了这个空对象的所有属性,完成属性初始化

第二步,创建完这个空对象后,new又干了一件事儿,设置对象的原型__proto__,这个__proto__指向的是构造函数的prototype属性的值

每个对象都有__proto__属性,每个函数都有prototype属性,而__proto__总是指向其构造函数的prototype属性。 当一个函数被创建的时候,其prototype被自动创建并初始化,此时prototype对象只有一个属性,那便是constructor,它指回到这个被创建的函数(这就是为什么每个对象有个constructor属性的原因),添加给prototype对象的任何属性都会成为被构造函数实例化的对象的属性或方法

1
2
3
4
5
6
7
8
9
10
11
var Dog = function(){
this.name = "狗腿子"
}
Dog.prototype.sayHello = function(){
console.log("你好") //你好
}
Dog.prototype.constructor === Dog //true
var dog = new Dog()
dog.constructor === dog.__proto__.constructor //true
dog.__proto__.constructor === Dog //true
dog.sayHello() //你好

二、原型与继承

我们都知道,javascript是基于原型的继承机制,而不是基于类,但在js中我们仍可以采用类似的类层次。比如说Object类是其他所有类的超类或者父类,所有类都从Object类中继承了一些基本方法,比如toString()方法

1、子类继承父类

我的理解,类的继承实际上是我们手动模拟new的行为,大致分三步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Dog = function(){
this.name = "狗腿子"
}
Dog.prototype.sayHello = function(){
console.log("你好") //你好
}
var Dog2 = function(){
Dog.call(this) //第一步
}
Dog2.prototype = new Dog() //第二步
var dog2 = new Dog2()
dog2.constructor === Dog //true
Dog2.prototype.constructor = Dog2 //第三步
dog2.constructor === Dog2 //true
dog2.__proto__.__proto__.__proto__===Object.prototype //true

第一步,把父类的构造函数作为新创造的对象的方法调用,从而实现了构造函数的继承

第二步,把子类的原型设置成父类的一个实例,从而实现原型的继承

第三步,重新设置子类的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
2
3
4
5
6
7
8
9
function borrowMethods(borrowFrom, addTo) {
var from = borrowFrom.prototype
var to = addTo.prototype

for(m in from) {
if (typeof from[m] != "function") continue; //过滤属性,只借方法
to[m] = from[m]
}
}

4、Object.create继承

最后提下Object.create,也算与时俱进,它是ECMAScript5中新引入的方法,可以调用这个方法来创建一个新对象. 新对象的原型就是调用create方法时传入的第一个参数,而构造函数就是Object

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype