一. 原型继承
原型继承相当于把父类的实例赋值于子类的原型,这样子类的原型上就有父类的属性和方法了,具体代码如下:
function Father(name) {
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function() {
console.log(this.name)
}
function Children(age) {
this.age = age;
}
Children.prototype = new Father("li")
Children.prototype.constructor = Children
var children = new Children(30)
// 但是如果实例属性被赋值,则优先输出实例上的属性
children.name = 'zhou'
console.log('name:',children.name)
console.log('children实例:',children)
console.log("验证原型:",children.__proto__ == Children.prototype,children.__proto__.__proto__ == Father.prototype)
console.log("验证继承:",children instanceof Children,children instanceof Father)
由下图可知:Children实例的原型指向Father的实例,且Father实例上的属性也在Children实例的原型上,Father原型上的方法则在进一步的Children实例的原型的原型上,这就形成了原型链
这种继承有个弊端,由于这种继承是来自包含引用类型值的原型,这样会导致原型属性(上面的Father中的name和colors )会被所有实例共享,即改变一个实例的原型上的属性会导致所有实例原型上的属性都发生变化,如下:
var children = new Children(30)
var children2 = new Children(40)
// 因为colors是数组,数组为引用类型值,修改数组会导致所有实例原型上的colors都发生变化
children.colors.push("black")
// 这里name虽然不是引用类型值,但是只要修改原型上的属性就会导致所有共享该原型的实例的原型上的属性都发生变化
children.__proto__.name = "zhou"
console.log("children:",children)
console.log("children2:",children2)
由下图可知:为实例children的colors添加一个元素“black”,导致children2的colors也发生变化,而且一般我们把属性对应在构造函数中而不是原型中,因为修改原型中的属性也会导致所有实例的原型属性发生变化
二. 使用call或者apply配合原型继承(组合继承)
因为使用call可以在子类生成实例的时候调用父类并改变this指向,相当于把父类的属性复制过来,再加上原型继承,即可实现继承,由于现在属性是在子类的实例上了,而不是实例的原型上,这样修改引用类型值也不会导致所有实例发生变化
function Father(name) {
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function() {
alert(this.name)
}
function Children(name,age) {
Father.call(this,name);
this.age = age;
}
Children.prototype = new Father();
Children.prototype.sayAge = function() {
alert(this.age)
}
var children = new Children('lee',30)
var children2 = new Children('li',40)
children.colors.push('black')
console.log(children,children2)
console.log(children.__proto__ == Children.prototype,children.__proto__.__proto__ == Father.prototype)
console.log(children instanceof Children,children instanceof Father)
由下图可知:修改了children 的colors也不会导致children2的colors发生变化
但是这种方式也有弊端,会调用两次父类构造函数,而且导致原型上多出无用的属性,如下图,children实例的原型上的name和colors与实例上的属性重复了,这是多余的
三. 寄生组合继承
寄生组合继承和组合继承类似,不过优化了组合继承的弊端,具体实现如下:
function Father(name) {
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function() {
alert(this.name)
}
function Children(name,age) {
Father.call(this,name);
this.age = age;
}
// 定义一个空函数
// 相当于把空函数的原型设置为父类的原型,再把constructor设置为子类,这样就实现了继承
function F() {}
F.prototype = Father.prototype
var prototype = new F()
prototype.constructor = Children
Children.prototype = prototype
Children.prototype.sayAge = function() {
alert(this.age)
}
var children = new Children('lee',30)
console.log(children)
console.log(children.__proto__ == Children.prototype,children.__proto__.__proto__ == Father.prototype)
console.log(children instanceof Children,children instanceof Father)
由下图可知:这样就优化了组合继承的二次调用父类构造函数和多余的属性的弊端了,相对高效
四. 使用ES6的class继承
es6class是语法糖,和es5的prototype继承是一样的
如下:A和B是完全一样的
class A {
constructor(a,b) {
this.a = a
this.b = b
}
c() {
console.log(this.a)
}
}
function B(a,b) {
this.a = a
this.b = b
}
B.prototype.c = function() {
console.log(this.a)
}
var a = new A(1,2)
var b = new B(1,2)
console.log(a,b)
a.c()
b.c()
接下来是class的继承,使用extends继承
class Father {
constructor(name) {
this.name = name
this.colors = ["red","blue"]
}
sayName() {
alert(this.name)
}
}
class Children extends Father {
constructor(name,age) {
// 如果不加super会报错,因为子类没有自己的this,
// 这个this是父类的,调用super相当于把父类的this变为子类的this
// 因此这里调用super(name)相当于Father.prototype.constructor.call(this,name)。
super(name)
this.age = age
}
}
var children = new Children("lee",30)
console.log(children)
console.log(children.__proto__ == Children.prototype,children.__proto__.__proto__ == Father.prototype)
console.log(children instanceof Children,children instanceof Father)
class继承是最简单的方式了,因为是es6语法糖,相当于寄生组合继承
总结
继承方式多样,但是总体而言寄生组合继承和ES6的class继承是最好最高效的,而且问题相对较少
参考文献
ECMAScript 6入门
JavaScript高级程序设计(第3版)