JavaScript的继承
许多面向对象语言都支持两种继承的方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。在JavaScript中由于函数没有签名也就无法进行接口继承,而只支持实现继承,并且是通过原型链来实现的。
关于原型链可以参考Prototype原型。
JavaScript实现继承的方式
- 原型链继承
- 构造函数继承
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
1、原型链继承
// 创建父级类型
function FatherType() {
this.property = true;
}
FatherType.prototype.getFatherValue = function () {
return this.property;
};
// 创建子级类型
function SonType() {
this.subproperty = false;
}
// 继承,用FatherType类型的一个实例来重写SonType类型的原型对象
SonType.prototype = new FatherType();
SonType.prototype.getSonValue = function () {
return this.subproperty;
};
// 创建一个SonType新实例
var instance = new SonType();
// 尝试访问继承自父级类型的属性
console.log(instance.getSonValue()); // false
console.log(instance.getFatherValue()); // true
其中,SonType继承了FatherType,而继承是通过创建FatherType实例,并将其赋值给SonType的原型实现的。
子类型的新原型对象中有一个内部属性Prototype指向了FatherType的原型,还有一个从FatherType原型中继承过来的属性constructor指向了FatherType构造函数。
最终的原型链:instance 指向 SonType的原型, SonType的原型又指向FatherType的原型, FatherType的原型又指向Object的原型(所有函数的默认原型都是Object的原型,因此默认原型都会包含一个内部指针指向Object.prototype)
原型链的缺点:
- 在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性,并且会被所有的实例共享。可以这样理解:在父类型构造函数中定义的引用类型值的实例属性,会在子类型原型上变成原型属性被所有子类型实例所共享。
- 在创建子类型的实例时,不能向父类型的构造函数中传递参数。
2、借用构造函数继承(也成为伪造函数或经典继承)
// 在子类型构造函数的内部调用父类型构造函数:使用apply()或call()方法将父对象的构造函数绑定在子对象上
function FatherType() {
// 定义引用类型值属性
this.colors = ['red', 'green', 'blue'];
}
function SonType() {
// 继承FatherType,在这里还可以给父类型构造函数传参
FatherType.call(this);
}
var instance1 = new SonType();
instance1.colors.push('purple');
console.log(instance1.colors); // "red,green,blue,purple"
var instance2 = new SonType();
console.log(instance2.colors); // "red,green,blue"
通过使用apply()或call()方法,我们实际上是在将要创建的SonType实例的环境下调用了FatherType的构造函数。这样一来,就会在新SonType对象上执行FatherType()函数中定义的所有对象初始化代码。结果SonType的每个实例就都会具有自己的colors属性的副本了。
借用构造函数的优点是解决了原型链实现继承存在的两个问题,但缺点在于方法都是在构造函数中定义,因此函数复用就无法实现了。而且在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
3、组合继承(也称伪经典继承)
将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。
function FatherType(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
FatherType.prototype.sayName = function () {
console.log(this.name);
};
function SonType(name, age) {
// 借用构造函数方式继承属性
FatherType.call(this, name);
this.age = age;
}
// 原型链方式集成方法
SonType.prototype = new FatherType();
SonType.prototype.constructor = SonType;
SonType.prototype.sayAge = function () {
console.log(this.age);
};
var instance1 = new SonType('NovisBerg', 22);
instance1.colors.push('purple');
console.log(instance1.colors); // "red,green,blue,purple"
instance1.sayName(); // "NovisBerg"
instance1.sayAge(); // 22
var instance2 = new SonType('TomHardy', 34);
console.log(instance2.colors); // "red,green,blue"
instance2.sayName(); // "TomHardy"
instance2.sayAge(); // 34
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JS中最常用的继承模式。而且使用instanceof操作符和isPrototype()方法也能够用于识别基于组合继承创建的对象。
但它也有自己的不足:无论在什么情况下,都会调用两次父类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
4、原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
在object()函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。实质上,object()对传入其中的对象执行了一次浅复制。
使用Object.create()方法实现原型式继承。这个方法接收两个参数:一是用作新对象原型的对象和一个为新对象定义额外属性的对象。在传入一个参数的情况下,此方法与object()方法作用一致。在传入第二个参数的情况下,指定的任何属性都会覆盖原型对象上的同名属性。
var person = {
name: 'NovisBerg',
colors: ['red', 'green', 'blue']
};
var anotherPerson1 = Object.create(person, {
name: {
value: 'TomHardy'
}
});
var anotherPerson2 = Object.create(person, {
name: {
value: 'ChrisNew'
}
});
anotherPerson1.colors.push('purple');
console.log(anotherPerson1.name); // "TomHardy"
console.log(anotherPerson2.name); // "ChrisNew"
console.log(anotherPerson1.colors); // "red,green,blue,purple"
console.log(anotherPerson2.colors); // "red.green.blue,purple"
只是想让一个对象与另一个对象类似的情况下,原型式继承是完全可以胜任的,但是缺点也很明显:包含引用类型值的属性始终会共享相应的值。(如例中anotherPerson1的colors属性与anotherPerson2的colors属性值是一样的。)
5、寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象.。
// 创建一个增强函数enhancePerson
function enhancePerson(original, age, height) {
var clone = Object.create(original,{
// 第一种设置函数属性的方式
age: {
value: age
}
}); // 通过Object.create()函数创建一个新对象
clone.sayGood = function () {
console.log('Hello world!!!');
};
clone.height = height; // 第二种设置函数属性的方式
return clone; // 返回这个对象
}
var person = {
name: 'NovisBerg'
};
var person1 = enhancePerson(person, 22, 178);
console.log(person1);
此时的person1为:
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。此模式的缺点是做不到函数复用。
6、寄生组合式继承
通过借用构造函数来继承属性,通过原型链的混成形式来集成方法。本质上,就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
function FatherType(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
FatherType.prototype.sayName = function () {
console.log(this.name);
};
function SonType(name, age) {
FatherType.call(this,name);
this.age = age;
}
// 创建父类型原型的一个副本
var anotherPrototype = Object.create(FatherType.prototype);
// 重设因重写原型而失去的默认的constructor属性
anotherPrototype.constructor = SonType;
// 将新创建的对象赋值给子类型的原型
SonType.prototype = anotherPrototype;
SonType.prototype.sayAge = function () {
console.log(this.age);
};
var instance1 = new SonType('NovisBerg', 22);
instance1.colors.push('purple');
console.log(instance1.colors); // "red,green,blue,purple"
instance1.sayName(); // "NovisBerg"
instance1.sayAge(); // 22
var instance2 = new SonType('TomHardy', 34);
console.log(instance2.colors); // "red,green,blue"
instance2.sayName(); // "TomHardy"
instance2.sayAge(); // 34
这个例子的高效率体现在它只调用了一次FatherType构造函数,并且因此避免了在SonType.prototype上面创建不必要,多余的属性。与此同时,原型链还能保持不变;因此还能正常使用instance操作符和isPrototype()方法。