OO对象一般支持两种继承:接口继承和实现继承。接口继承只继承方法的签名,而实现继承则继承实际的方法。ES只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
一、原型链
ES中有描述原型链的概念,并将其作为实现继承的主要方法。其基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法。在知道原型与实例关系的基础上,如果让原型对象等于另一个类型的实例,结果会怎样?那么实例a包含的指针指向的原型ap实际上是函数B的实例b,而b中自然有指向原型bp的指针,bp中本身有指向B的指针。如果让bp等于另一个实例c,上述关系依然成立。如此层层递进,就形成了实例与原型的链条。这就是 原型链 的基本概念。例如
function Person(){
this.personValue = "im person"
}
Person.prototype.getPersonValue = function(){
alert (this.personValue);
};
function Student(){
this.studentValue = "im student";
}
// 继承Person
Student.prototype = new Person();
Student.prototype.getStudentValue = function(){
alert (this.studentValue);
};
var s = new Student();
s.getPersonValue();
s.getStudentValue();
上例可以看出,实现继承的本质是重写原型对象。附关系图
原型链本质上是原型搜索机制的扩展。拿上例来说,首先搜索的s,然后搜索 Student.prototype,接着搜索Person.prototype,搜到为止。
1、默认原型
所有函数的默认原型都是Object的实例,因此都继承自Object。如图
2、确定原型与实例的关系
两种方式可以确定:1、instanceof操作符 2、isPrototypeOf()
alert(instance instanceof Object); // true
alert(Object.prototype.isPrototypeOf(instance)); // true
3、谨慎地定义方法
1、子类定义原型方法必须是在父类实例替换掉子类原型的操作之后。
2、实现继承后,不能使用对象字面量为子类创建原型方法。
4、原型链的问题
1、包含引用类型值的原型被所有实例所共享。例如
// 问题1:引用类型被共享
function Person(){
this.personValue = "im person";
// 有引用类型的属性
this.friends = ["zhangsan","lisi"];
}
Person.prototype.getPersonValue = function(){
alert (this.personValue);
};
function Student(){
this.studentValue = "im student";
}
// 继承Person
Student.prototype = new Person();
Student.prototype.getStudentValue = function(){
alert (this.studentValue);
};
var s1 = new Student();
var s2 = new Student();
s1.friends.push("wangwu");
alert(s1.friends); // zhangsan,lisi,wangwu
// s1的设置影响到了s2的属性
alert(s2.friends); // zhangsan,lisi,wangwu
2、在创建子类的实例时,不能向父类的构造函数中传递参数。
由于这两个问题的存在,单独的原型链模式很少使用
二、借用构造函数
为了解决原型中包含引用属性的问题,开发人员摸索出一种“借用构造函数”的技术。其基本思想就是:在子类构造函数内部调用父类的构造函数。别忘了,函数只不过是在特定环境中执行代码的对象。因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数,例如:
function Person(){
this.friends = ["zs","ls"]
}
function Student(){
// 通过“借调”,继承了Person
Person.call(this);
}
var s1 = new Student();
var s2 = new Student();
s1.friends.push("wangwu");
alert(s1.friends); // zs,ls,wangwu
alert(s2.friends); // zs,li
通过call()方法,我们实际上是在(将来)新创建的实例的环境下调用了Person构造函数。这样,新对象上就执行了父类中的初始化代码。结果就是,每个子类实例有了自己的friends属性副本。
1、传递参数
相对于原型链,借用构造函数的另一大优势就是可以在子类构造函数中向超类构造函数传递参数。
// 解决向父构造函数传递参数的问题
function Dog(name){
this.name = name
}
function PiPi(){
Dog.call(this,"pipi");
}
var pipi = new PiPi();
alert(pipi.name); // pipi
2、借用构造函数的问题
父类原型中的方法,无法在子类中得到复用。所以单独使用借用构造函数模式也并不常见
三、组合继承
组合继承的思路:使用原型链实现对原型属性和方法的继承,使用 借用构造函数 实现对实例属性的继承。
function Person(name){
this.name = name;
this.friends = ["zs","ls"]
}
Person.prototype.sayName = function(){
alert(this.name);
};
function Student(name,age){
// 继承实例属性
Person.call(this,name); // 第二次调用Person()
this.age = age;
}
// 继承公有属性和方法
Student.prototype = new Person(); // 第一次调用Person()
var s1 = new Student("xiaofang",18);
var s2 = new Student("xiaowang",18);
s1.sayName();
s2.sayName();
组合继承融合了双方的优点,成为JS中最常用的继承模式。
组合继承也有自己的缺点:无论什么情况下,都会调用两次父类的构造函数。并且有创建多余、不必要属性的可能性。
四、寄生式组合继承
// 原型式继承:让一个对象与另一个对象保持相似
function inheritObject(o){
function F(){}
// 什么是相似,即源对象或源对象的原型是目标对象的原型,而目标对象是可操作的空对象
F.prototype = o;
return new F();
}
// 寄生式继承:创建对象,增强对象并返回,即为寄生式
function inheritPrototype(subClass,superClass){
// 创建对象
var p = inheritObject(superClass.prototype);
// 增强这个对象:什么是寄生式,即新对象不仅有源对象(superClass.prototype)的属性和方法,还有自己独有的属性和方法
p.constructor = subClass;
// 指定对象
subClass.prototype = p;
}
function Person(name){
this.name = name;
this.friends = ["zs","ls"]
}
Person.prototype.sayName = function(){
alert(this.name);
};
function Student(name,age){
// 继承Person
Person.call(this,name);
this.age = age;
}
// 本质上,就是用一个"干净的"实例,作为子类的原型,这个实例的原型是父类的原型。与之前new一个父类的实例相比
// 1、少了不必要的父类的构造函数中定义的属性
// 2、多了指向子类构造函数本身的constructor指针
inheritPrototype(Student,Person);
var s1 = new Student("xiaofang",18);
var s2 = new Student("xiaowang",18);
alert(s1 instanceof Person);