类式继承1——默认模式
最常用的一种模式是使用Parent()
构造函数来创建一个对象,然后把这个对象设为Child()
的原型。这是可复用的inherit()
函数的第一种实现方法:
function inherit(C, P) {
C.prototype = new P();
}
需要强调的是原型(prototype
属性)应该指向一个对象,而不是函数,所以它需要指向由被继承的构造函数创建的实例(对象),而不是构造函数自己。换句话说,请注意new
运算符,有了它这种模式才可以正常工作。
之后在应用中使用new Child()
创建对象的时候,它将通过原型拥有Parent()
实例的功能,像下面的例子一样:
var kid = new Child();
kid.say(); // "Adam"
跟踪原型链
在这种模式中,子对象既继承了(父对象的)“自有属性”(添加给this
的实例属性,比如name
),也继承了原型中的属性和方法(比如say()
)。
我们来看一下在这种继承模式中原型链是怎么工作的。为了讨论方便,我们假设对象是内存中的一块空间,它包含数据和指向其它空间的引用。当使用new Parent()
创建一个对象时,这样的一块空间就被分配了(图6-1中的2号),它保存着name
属性的数据。如果你尝试访问say()
方法(比如通过(new Parent).say()
),2号空间中并没有这个方法。但是在通过隐藏的链接__proto__
指向Parent()
构建函数的原型prototype
属性时,就可以访问到包含say()
方法的1号空间(Parent.prototype
)了。所有的这一块都是在幕后发生的,不需要任何额外的操作,但是知道它是怎样工作的有助于让你明白你正在访问或者修改的数据在哪,这是很重要的。注意,__proto__
在这里只是为了解释原型链而存在,这个属性在语言本身中是不可用的,尽管有一些环境提供了(比如Firefox)。
图6-1 Parent()构造函数的原型链
现在我们来看一下在使用inherit()
函数之后再使用var kid = new Child()
创建一个新对象时会发生什么。见图6-2。
图6-2 继承后的原型链
Child()
构造函数是空的,也没有属性添加到Child.prototype
上,这样,使用new Child()
创建出来的对象都是空的,除了有隐藏的链接__proto__
。在这个例子中,__proto__
指向在inherit()
函数中创建的new Parent()
对象。
现在使用kid.say()
时会发生什么?3号对象没有这个方法,所以通过原型链找到2号。2号对象也没有这个方法,所以也通过原型链找到1号,刚好有这个方法。接下来say()
方法引用了this.name
,这个变量也需要解析,于是沿原型链查找的过程又走了一遍。在这个例子中,this
指向3号对象,它没有name
属性,然后2号对象被访问,并且有name
属性,值为“Adam”。
最后,我们看一点额外的东西,假如我们有如下的代码:
var kid = new Child();
kid.name = "Patrick";
kid.say(); // "Patrick"
图6-3展现了这个例子的原型链:
图6-3 继承并且给子对象添加属性后的原型链
设定kid.name
并没有改变2号对象的name
属性,但是却直接在3号对象上添加了自有的name
属性。当kid.say()
执行时,say()
方法会依次在3号对象中找,然后是2号,最后到1号,像前面说的一样。但是这一次在找this.name
(和kid.name
一样)时很快,因为这个属性在3号对象中就被找到了。
如果通过delete kid.name
的方式移除新添加的属性,那么2号对象的name
属性就将被暴露出来并且在查找的时候被找到。
这种模式的缺点
这种模式的一个缺点是既继承了(父对象的)“自有属性”,也继承了原型中的属性。大部分情况下你可能并不需要“自有属性”,因为它们更可能是为实例对象添加的,并不用于复用。
一个在构造函数上常用的规则是,用于复用的成员(译注:属性和方法)应该被添加到原型上。
在使用这个inherit()
函数时另外一个不便是它不能够让你传参数给子构造函数,这些参数有可能是想再传给父构造函数的。考虑下面的例子:
var s = new Child('Seth');
s.say(); // "Adam"
这并不是我们期望的结果。事实上传递参数给父构造函数是可能的,但这样需要在每次需要一个子对象时再做一次继承,很不方便,因为需要不断地创建父对象。