JS面向对象知识中,继承是比较难比较抽象的一块内容,而且实现继承有很多种方法,每种方法又各有优缺点,更加的让人崩溃,这需要对面向对象知识中的对象原型原型链构造函数等基础知识掌握透彻,否则《JavaScript高级程序设计》里第六章继承也是看不明白的,网上也有茫茫多的文章,看了这么多依然不是很明白...下面我结合自己的理解,和参考了《JavaScript高级程序设计》和网上其他文章,总结一下实现继承的几种方法及优缺点。

先创建一个父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Father(name){
// 属性
this.name = name || 'Father',
this.colors = ["red","blue","yellow"];

// 实例方法
this.sleep = function(){
console.log(this.name+"正在睡觉")
}
}
// 原型方法
Father.prototype.look = function(book){
console.log(this.name + "正在看:" + book);
}

借用构造函数继承(call,apply)

核心:使用父类的构造函数来增强子类实例
特点:把父类私有的属性和方法,克隆一份一样的给子类私有的属性,Father执行的时候,把Father的中的this换成Son的实例,由于并不是new Father,所以Father.prototype上的属性无关

1
2
3
4
function Son(){
Father.call( this ); // 或apply
this.type = "Son";
}

第2行,在子类(Son)中执行父类(Father)的构造函数,通过这种调用,把父类构造函数的this指向为子类实例化对象引用,从而导致父类执行的时候父类里面的属性都会被挂载到子类的实例上去。

1
2
new Son().name; // Father
new Son().colors; // (3) ["red", "blue", "yellow"]

但是通过这种方式,父类原型上的东西是没法继承的,因此函数复用也就无从谈起

1
2
3
4
5
6
7
Father.prototype.age = 55;
Father.prototype.say = function() {
console.log(" Oh,My God! ");
}
new Son().age; // undefined
// Uncaught TypeError: (intermediate value).say is not a function
new Son().say();

缺点:Son 无法继承 Father 的原型对象,并没有真正的实现继承(部分继承)

原型链式继承

核心:将父类的实例作为子类的原型(并不是把父类中的属性和方法克隆一份一模一样的给子类,而是让子类父类之间增加了原型链接)
特点:父类中私有的和公有的都继承到了子类原型上(子类公有的)

1
2
3
4
5
function Son(){
this.name = "Son";
}
Son.prototype = new Father();
Son.prototype.constructor = Son;

这种方式能否解决借用构造函数继承的缺点呢?我们依然为父类的原型添加sex属性和say方法:

1
2
3
4
5
6
7
Father.prototype.sex = "男";
Father.prototype.say = function() {
console.log(" Oh,My God! ");
}

new Son().sex; // 男
new Son().say(); // Oh,My God!

这种方式确实解决了上面借用构造函数继承方式的缺点。

但是,这种方式仍有缺陷:

1
2
3
4
5
6
let instance1 = new Son();
instance1.colors.push("black");
let instance2 = new Son();

instance1.colors; // (4) ["red", "blue", "yellow", "balck"]
instance2.colors; // (4) ["red", "blue", "yellow", "balck"]

我们实例化了两个Son,在实例instance1中为父类的colors属性push了一个颜色,但是instance2也被跟着改变了。造成这种现象的原因就是原型链上中的原型对象它俩是共用的。

这不是我们想要的,instance1和instance2这个两个对象应该是隔离的,这是这种继承方式的缺点。

组合式继承(构造函数 + 原型链继承)

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
特点:把父类私有的和公有的都变成了子类共有的,但是调用了两次父类构造函数,生成了两份实例

1
2
3
4
5
6
function Son(){
Father.call(this);
this.type = "Son";
}
Son.prototype = new Father()
Son.prototype.constructor = Son;

注意第2,5行,这种方式结合了借用构造函数继承原型链继承的优点,能否解决上述两个实例对象没有被隔离的问题呢?

1
2
3
4
5
6
let instance1 = new Son();
instance1.colors.push("black");
let instance2 = new Son();

instance1.colors; // (4) ["red", "blue", "yellow", "balck"]
instance2.colors; // (3) ["red", "blue", "yellow"]

可以看到,instance2和instance1两个实例对象已经被隔离了。

但这种方式仍有缺点。父类的构造函数被执行了两次,第一次是Son.prototype = new Father(),第二次是在实例化的时候,这是没有必要的。

寄生组合式继承(组合继承优化)

核心:通过寄生方式,去掉父类的实例属性,这样在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

1
2
3
4
5
6
function Son(){
Father.call(this);
this.type = "Son";
}
Son.prototype = Object.create(Father.prototype)
Son.prototype.constructor = Son

Object.create是一种创建对象的方式,它会创建一个中间对象

1
2
3
let person = {name: "person"}
let obj = Object.create(person)
// Object.create({ name: "person" })

通过这种方式创建对象,新创建的对象obj的原型就是person,同时obj也拥有了属性name,这个新创建的中间对象的原型对象就是它的参数。

这种方式解决了上面的所有问题,是继承的最完美实现方式。

ES6中继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

1
2
3
4
5
6
7
8
9
class Son extends Father {
constructor(x, y, colors) {
super(x, y); // 调用父类的constructor(x, y)
this.colors = colors;
}
toString() {
return this.colors + ':' + super.toString(); // 调用父类的toString()
}
}
  • constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

  • 子类必须在constructor方法中调用super方法,否则新建实例时会报错。如果子类没有定义constructor方法,这个方法会被默认添加,不管有没有显式定义,任何一个子类都有constructor方法。

  • ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Father.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this