许多面向对象语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于ECMAScript 的函数没有签名,所以无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要依赖原型链来实现。
1、原型链
ECMASCript 中描述了原型链的概念,并将原型链作为实现继承的主要方式。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。其实现是让原型对象等于另一个类型的实例。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function (){
return this.property;
}
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType(); // 继承了 SuperType
var instance = new SubType();
instance.getSuperValue() // true
注意:
(1)所有引用类型通过原型链默认继承了 Object 。所有函数的默认原型都是 Object,
SuperType.prototype.__proto__ === Object.prototype // true
(2)子类实例既是子类的实例,又是父类的实例。(使用 instanceof 或者 isPrototypeOf() 方法确定原型与实例的关系)
console.log(instance instanceof SubType); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof Object); // true
console.log(Object.prototype.isPrototypeOf(instance)); // true
(3)给原型添加方法或重写父类方法的代码移动要放在替换原型的语句之后,即 SubType.prototype = new SuperType();
语句之后。否则,继承后会被重名方法覆盖。
(4)在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。
SubType.prototype = new SuperType(); // 继承了SuperType
SubType.prototype = {
// 使用字面量添加新方法,会导致上一行代码无效
getSubValue: function(){
return this.subproperty;
}
}
var instance = new SubType();
instance.getSuperValue()
// error instance.getSuperValue is not a function
(5)原型链继承时,包含引用类型值的原型属性会被所有实例共享。
function A (){
this.colors = ['red', 'blue'];
}
function B (){}
B.prototype = new A(); // 继承了 A
var ins1 = new B();
console.log(ins1.colors); // ['red', 'blue']
ins1.colors.push('green');
console.log(ins1.colors); // ['red', 'blue', 'green']
var ins2 = new B();
console.log(ins2.colors); // ['red', 'blue', 'green']
(6)原型链继承没有办法在不影响所有对象实例的情况下,给父类的构造函数传递参数。
2、借用构造函数
在解决原型中包含引用类型所带来的问题,开发人员使用一种叫做借用构造函数的技术。这种技术的基本思想是在子类构造函数的内部调用父类构造函数。
function A (){
this.colors = ['red', 'blue'];
}
function B (){
A.call(this); // 继承了 A(或使用 apply 方法)
}
var ins1 = new B();
console.log(ins1.colors); // ['red', 'blue']
ins1.colors.push('green');
console.log(ins1.colors); // ['red', 'blue', 'green']
var ins2 = new B();
console.log(ins2.colors); // ['red', 'blue']
该方法可以在子类的构造函数中向父类的构造函数传递参数。 call 或 apply 方法传递参数。
缺点:
(1)该方法是借用构造函数来实现继承,那么就存在构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用性较低。
(2)该方法实现的继承,父类原型中定义的方法,对子类不可见。
function A (){
this.colors = ['red', 'blue'];
}
A.prototype.getc = function (){
return this.colors;
}
function B (){
A.call(this);
}
var ins1 = new B();
console.log(ins1.getc());
// Uncaught TypeError: ins1.getc is not a function
3、组合继承
组合继承也叫伪经典继承,指的是将原型链和借用继承构造函数的方法技术组合到一起。其是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
function A (){
this.colors = ['red', 'blue'];
}
A.prototype.getc = function (){
return this.colors;
}
function B (age){
A.call(this); // 第二次调用 A 的构造函数
this.age = age;
}
B.prototype = new A(); // 第一次调用 A 的构造函数
B.prototype.constructor = B; // 弥补因重写原型而失去的默认的 constructor 属性
var ins1 = new B(12);
console.log(ins1.getc()); // ['red', 'blue']
ins1.colors.push('green');
console.log(ins1.colors); // ['red', 'blue', 'green']
console.log(ins1.age); // 12
var ins2 = new B(13);
console.log(ins2.colors); // ['red', 'blue']
console.log(ins2.age); // 13
注意: 组合继承在任何情况下都会调用两次父类的构造函数。
4、原型式继承
原型式继承时借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为达到这个目的,给出了如下函数:
function object(o){
function F() {}
F.prototype = o;
return new F();
}
调用该方法得到继承。
ES5 通过新增 Object.create()
方法规范了原型式继承。这个方法接受两个参数:一个用作新对象原型的对象;一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()
与 object()
方法的行为相同。
Object.create()
方法的第二个参数与 Object.defineProperties()
方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。 以这种方式指定的任何属性都会覆盖原型对象的同名属性。
注意:使用该方法继承时,包含引用类型值的属性始终都会共享相应的值,就像使用原型模型式一样。
5、寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,其创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后在返回对象。类似于工厂模式。
function createAnother(original){
var clone = Object.create(original);
clone.sayHi = function(){
console.log("Hi");
}
return clone;
}
调用该方法得到继承。
6、寄生组合式继承
为了解决组合继承调用两次父类构造函数问题,提出了寄生组合式继承。
寄生组合式继承是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本而已。本质上,就是使用寄生式继承来继承父类的原型然后再将结果指定给子类的原型。寄生组合式继承的基本模型如下:
function inheritPrototype( subType, superType){
var prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green'];
}
SuperType.prototype.asyName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
console.log(this.age);
}
7、以上方法无法继承原生构造函数
比如,不能定义一个 Array 的子类。
function MyArray(){
Array.apply(this, arguments);
}
inheritPrototype(MyArray, Array);
var colors = new MyArray();
colors[0] = 'red';
console.log(colors.length); // 0
colors.length = 0;
console.log(colors[0]); // red
上述代码发生的异常是因为子类无法获得原生构造的内部属性,通过 Array.apply()
或者分配给原型对象都不行。原生构造函数会忽略 apply 方法传入的 this ,也就是是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。
ES5 是先新建子类的实例对象 this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。
8、Class 的继承
ES6 允许继承原生构造函数定义的子类,因为 ES6 是先新建父类的实例对象 this ,然后再用子类的构造函数修饰 this ,使得父类的素有行为都可以继承。
class MyArray extends Array{
constructor(...args){
super(...args);
}
}
var colors = new MyArray();
colors[0] = 'red';
console.log(colors); // ['red']
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
上述代码,可以看出,ES6 可以自定义原生数据结构的子类,这是 ES5 无法做到的。
extends 的具体使用参见Class 的继承。