许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于 ECMAScript 中的函数没有签名,所以在 JS 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。所以,下面所要说的原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承和寄生组合式继承都属于实现继承。
最后的最后,我会解释 ES6 中的 extend 语法利用的是寄生组合式继承。
1. 原型链继承
ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。实现原型链继承有一种基本模式,其代码大致如下:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } SubType.prototype = new SuperType(); // 敲黑板!这是重点:继承了 SuperType SubType.prototype.getSubValue = function (){ return this.subproperty; }; var instance = new SubType(); alert(instance.getSuperValue()); // true
原型链继承的一个本质是重写原型对象,代之以一个新类型的实例;给原型添加方法的代码一定要放在替换原型的语句之后;在通过原型链实现继承时,不能使用对象字面量创建原型方法。
实例属性在实例化后,会挂载在实例对象下面,因此称之为实例属性。上面的代码中 SubType.prototype = new SuperType(); ,执行完这条语句后,原 SuperType 的实例属性 property 就挂载在了 SubType.prototype 对象下面。这其实是个隐患,具体原因后面会讲到。
每次去查找属性或方法的时候,在找不到属性或方法的情况下,搜索过程总是要一环一环的前行到原型链末端才会停下来。
所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。由此可知,所有函数的默认原型都是 object 的实例,因此函数的默认原型都会包含一个内部指针,指向 Object.prototype 。
缺点:
最主要的问题来自包含引用类型值的原型。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
在创建子类型的实例时,不能向超类型的构造函数传递参数。
* 题外话:确定原型与实例的关系的两种方式
第一种方式是使用 instanceOf 操作符,只要用这个操作符来测试实例的原型链中是否出现过某构造函数。如果有,则就会返回 true ;如果无,则就会返回 false 。以下为示例代码:
alert(instance instanceof Object); //true alert(instance instanceof SuperType); //true alert(instance instanceof SubType); //true
第二种方式是使用 isPrototypeOf() 方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生出来的实例的原型。以下为示例代码:
alert(Object.prototype.isPrototypeOf(instance)); //true alert(SuperType.prototype.isPrototypeOf(instance)); //true alert(SubType.prototype.isPrototypeOf(instance)); //true
2. 借用构造函数继承
借用构造函数继承,也叫伪造对象或经典继承。其基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。其继承代码大致如下:
function SuperType(){ this.colors = [ "red", "blue", "green"]; } function SubType(){ SuperType.call(this); // 敲黑板!注意了这里继承了 SuperType } var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.colors); // "red,blue,green,black" var instance2 = new SubType(); alert(instance2.colors); // "red,blue,green"
通过使用 call() 方法(或 apply() 方法也可以),我们实际上是在(未来将要)新创建的子类的实例环境下调用父类构造函数。
为了确保超类构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
优点:可以在子类型构造函数中向超类型构造函数传递参数。
缺点:
方法都在构造函数中定义,每次实例化,都是新创建一个方法对象,因此函数根本做不到复用;
使用这种模式定义自定义类型,超类型的原型中定义的方法,对子类型而言是不可见。
3. 组合继承