继承
原型链
讲原型的时候提到过继承,设计原型的初衷就是为了继承,原型链是实现继承的主要方法。
那什么是原型链,还记得之前提到过的作用域链吗,它表示标识符在环境中的查找顺序,原型链与作用域链相似,它表示属性或方法在实例和构造函数间的追溯顺序,看一个例子:
function Father() {
}
Father.prototype.familyName = "Zhao";
function Child() {
}
var father = new Father();
Child.prototype = father;
var me = new Child();
me.familyName;
// "Zhao"
当我们在
me
实例上访问familyName
属性时,搜索过程会从原型链的末端开始逐步向上,即:me实例 → Child原型 → Father原型
默认的原型
由于所有引用类型都继承了Object
因此所有引用类型原型链的顶端都是Object.prototype
,因此所有自定义类型都会继承toString()
valueOf()
等默认方法。
确定原型和实例的关系
- instanceof
用这个操作符测试实例和原型链上出现的构造函数,即返回true
- isPropertyOf
用这个方法测试原型链中出现过的原型,即返回true
谨慎地定义方法
在上面的例子中,我们有一步替换原型对象的操作:
Child.prototype = father;
很多时候,我们想要在子类型中添加一些超类型没有的方法,应该放在替换原型对象之后。
不能使用对象字面量
通过原型链实现继承时,不能使用对象字面量来重写原型,因此这样做会切断子类型与超类型之间的联系。
原型链的问题
在上述例子中,Child
构造函数的原型是Father
的实例father
,那么father
实例的属性就变成Child
的原型属性,接下来在Child
的所有实例,均会共享father
实例的属性,我们修改一下上述例子:
function Father() {
this.hobbies = [];
}
function Child() {
}
Child.prototype = new Father();
var me = new Child();
var sister = new Child();
me.hobbies.push("dancing");
sister.hobbies;
// ["dancing"]
me
修改Child
原型上的hobbies
属性会影响到sister
访问该属性时获得的值,注意,当我们直接在实例对象上对某个属性赋值时,我们相当于修改或添加实例中的某个属性(同访问不一样,不遵循原型链),比如:
me.hobbies = [];
sister.hobbies;
//["dancing"]
上述例子中,直接在me
实例中添加属性hobbies
,因此没有影响sister
。
借用构造函数
即在子类型的构造函数调用超类型的构造函数,举一个例子:
function Father(givenName) {
this.givenName = givenName;
}
function Child(givenName) {
Father.call(this, givenName)
}
var child = new Child("Xianshu");
child.givenName
// "Xianshu"
- 私有属性
使用这种方式,可以使每个实例从超类型中继承的属性私有化,一个实例更改属性值不会影响到其他属性,举一个例子:
var child1 = new Child("Sue")
var child2 = new Child("Jane")
child1.givenName
// "Sue"
child2.givenName
// "Jane"
传递参数
通过这种方式,子类型可以在调用超类型构造函数时向其传参。结构构造函数的问题
首先没办法在实例间复用函数,比如:
function Father(givenName) {
this.givenName = givenName;
this.cook = function() {
return "delicious food";
}
}
function Child(givenName) {
Father.call(this, givenName)
}
var child1 = new Child("Sue")
var child2 = new Child("Jane")
child1.cook === child2.cook
// false
上述例子中,可以看出,两个实例的cook
方法引用的不是同一块内存地址,说明cook
被实例化了多次,这显然是冗余的。
其次,超类型的原型中定义的方法对于子类型来说也是不可见的。
组合继承
使用原型链来实现方法的继承,使用构造函数实现属性的继承。比如:
function Father(givenName) {
this.givenName = givenName;
}
Father.prototype.cook = function() {
return "delicious food";
}
function Child(givenName) {
Father.call(this, givenName)
}
Child.prototype = new Father();
Child.prototype.constructor = Child;
var child = new Child("Sue")
child.cook();
// "delicious food"
在创建Child
构造函数时,首先继承超类型Father
中的属性,然后创建一个Father
实例,将其赋值给Child
的原型,使其继承Father
定义在原型中的方法,这里需要注意两点:
-
new Father()
中的givenName
属性不会覆盖child
中的givenName
属性,这是由标识符在实例及其原型链上的搜索顺序决定的 - 需要修正
Child.prototype.constructor
属性
原型式继承
无需构造函数的一种继承方式,在一个对象的基础上创建出一个新对象:
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
var person = { age : 18, hobbies: [] };
var anotherPerson = object(person);
anotherPerson.age;
// 18
上述例子中,person
不是构造函数,只是一个普通的对象,anotherPerson
继承了它的属性,需要注意的是,继承到的属性是在实例间共享的。
anotherPerson.hobbies.push("dancing");
anotherPerson.age = 19;
var anotherPerson2 = object(person);
anotherPerson2.age; // 18
anotherPerson2.hobbies; // ["dancing"]
寄生式继承
寄生式继承封装了如下过程:
- 调用一个能够返回新对象的函数
- 以某种方式来增强返回的对象
- 返回对象
function printBook(original) {
var book = object(original);
book.auther = "Sue";
return book;
}
var book = {
name: "travel notes",
type: "travel"
}
var travelBook = printBook(book);
travelBook.name
// "travel notes"
travelBook.auther
// "Sue"
上述例子中,travelBook
不继承了book
中的属性,而且还具有自己的属性。需要注意的是,使用寄生式继承,在第二步中添加的属性或方法不能在实例间复用。
寄生组合式继承
还记得我们在组合继承中提到的注意事项第一条吗...为什么Child
原型中以及child
实例中都包含givenName
属性,这是因为我们调用了两次Father
构造函数,第一次将属性赋予child
实例,第二次是将Father
的实例赋值给Child
的原型,虽然第二次调用添加的givenName
属性并没有影响到child.givenName
,但两次调用毕竟是冗余的,况且Child
只需要继承Father
的原型,而Father
的实例包含了额外的属性。寄生组合方式就是为了解决上述的问题:
function Father(givenName) {
this.givenName = givenName;
}
Father.prototype.cook = function() {
return "delicious food";
}
function Child(givenName) {
Father.call(this, givenName)
}
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
inheritPrototype(Child, Father);
var child = new Child("Sue")
undefined
child.givenName
// "Sue"
"givenName" in Child.prototype
// false
child.cook()
// "delicious food"
child instanceof Father
// true
Father.prototype.isPrototypeOf(child)
// true
Child
使用寄生组合的方式继承了Father
,可以调用Father
原型中的方法,同时,没有留下Father
“私有”属性的痕迹,instanceof()
与isPrototypeOf()
可以正常使用