实现继承的几种方式以及他们的优缺点

继承的继承方式:原型链继承构造继承(call/apply)组合继承寄生组合继承 ES6继承

原型链继承

首先回顾下构造函数、原型、和实例之间的关系:

每一个构造函数都有一个原型对象 F.prototype,原型对象都包含一个指向构造构造的指针 constructor,而实例都包含一个指向原型对象的内部指针 __proto__

1
function F() {}
2
var f = new F()
3
F.prototype.constructor === F;
4
f.__proto__ === F.prototype;

原型链继承的思想:将父类的实例作为子类的原型对象。

1
function Tiger(leg) {
2
    this.leg = leg;
3
}
4
Tiger.prototype.getLeg = function() {
5
    return this.leg;
6
}
7
8
function Cat(eat) {
9
    this.eat =  eat;
10
}
11
12
// 将 Tiger 的实例对象 Tiger {leg: "四条腿"} 作为 Cat 的原型对象
13
// 这是一个重写 Cat 原型对象的过程
14
Cat.prototype = new Tiger("四条腿");  
15
16
Cat.prototype.getEat = function() {
17
    return this.eat;
18
}
19
20
let cat = new Cat("鱼");
21
console.log(cat.getLeg()); // 四条腿
22
console.log(cat.getEat()); // 鱼

原型链继承实际上是重写子类原型对象的过程,子类原型对象的 constructor 属性会被默认指向父类。

1
Cat.prototype.constructor === Tiger; // true

原型链继承的缺点: 当父类的原型对象修改后,子类也会访问到修改后的结果,父类一变其他都跟着变了。

遵循了原型模式的动态性(请看第 1 小节)

构造继承(call/apply)

构造继承的思想: 利用 callapply 将父类的 this 硬绑定到子类上。

1
function Tiger (leg) {
2
    this.leg = leg;
3
    this.getLeg = function () {
4
        return this.leg;
5
    }
6
};
7
//对原型对象进行的扩展的方法就无法被继承了
8
Tiger.prototype.getEat = function () {    
9
    console.log("吃肉");
10
};
11
12
function Cat(eat) {
13
    // 使用 call 将 this 硬绑定到 Cat 类上
14
    // 创建 Cat 类实例后,实例调用方法时,this 指向 Cat
15
    // console.log(this); Cat 
16
    Tiger.call(this, '四条腿');  
17
    this.eat = eat;
18
};
19
20
let cat = new Cat("鱼");
21
console.log(cat.leg);      // 四条腿 (继承自 Tiger 类)
22
console.log(cat.getEat());  // 报错 调用不到 Tiger 类原型对象上面的方法

构造继承的缺点:

  • 只能继承父类的实例属性或方法,不能继承父类原型上的属性或方法,无法实现函数复用。

  • 每个子类都有父类实例函数的副本,性能会有耗损。

组合继承

组合继承的思想:是将原型链继承构造继承组合起来使用,是常用的一种继承方式。

1
function Tiger(leg) {
2
    this.leg = leg;
3
}
4
Tiger.prototype.getLeg = function() {
5
    return this.leg;
6
}
7
8
function Cat(eat) {
9
    // 构造继承 使用 call 调用父类 将 this 硬绑定到子类上
10
    // 继承父类的实例属性或方法
11
    Tiger.call(this, "四条腿"); // 第二次调用父类
12
    this.eat =  eat;
13
}
14
15
// 使用原型链继承 将 Tiger 的实例对象 Tiger {leg: "四条腿"} 作为 Cat 的原型对象
16
// 继承父类的原型属性或原型方法
17
Cat.prototype = new Tiger("四条腿");   // 第一次调用父类
18
19
Cat.prototype.getEat = function() {
20
    return this.eat;
21
}
22
23
let cat = new Cat("鱼"); // 触发第二次调用
24
console.log(cat.leg);  // 四条腿 (继承自父的实例属性 )
25
console.log(cat.getLeg()); // 四条腿 (继承自父类的原型方法)
26
console.log(cat.getEat()); // 鱼

第一次调用 Tiger 构造函数时,Cat.prototype 会得到属性 leg,它是父类的实例属性,只不过现在位于子类的原型对象中。
当 使用 new 调用 Cat 构造函数时,又会调用一次 Tiger 构造函数,这一次会在 new 创建的新对象上创建实例属性 leg, 于是这个属性就屏蔽了原型对象上的同名属性。

组合继承的缺点: 调用了两次父类,第二次调用父类时,会在新对象上创建实例属性,这会屏蔽存在子类原型对象上的同名属性。

寄生式继承

寄生继承的思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

1
function cloneObj(parent) {
2
    let clone = object(parent);
3
    clone.getLeg = function() {
4
       return this.leg;
5
    }
6
    return clone;
7
}
8
9
// 寄生
10
function object(obj) {
11
    function F() {};
12
    F.prototype = obj;
13
    return new F();
14
}
15
16
let tiger = {
17
    eat: "肉",
18
    leg: "四条"
19
}
20
let cat = cloneObj(tiger);
21
cat.getLeg();  // 四条

寄生继承的缺点:使用寄生式继承为对象添加方法,会由于不能做到方法的复用而降低效率,这一点和构造函数模式类似。

寄生组合继承

寄生组合继承思想:是在组合继承的基础上,使用寄生方式对组合继承进行的一次优化。

1
function Tiger(name) {
2
    this.name = name;
3
}
4
Tiger.prototype.leg = "四条腿";
5
6
Tiger.prototype.getEat = function() {
7
    return this.eat;
8
}
9
function Cat(eat) {
10
    // 构造继承 使用 call 调用父类 将 this 硬绑定到子类上
11
    // 继承父类的实例属性或方法
12
    Tiger.call(this, "老虎"); 
13
    this.eat = eat;
14
}
15
// 寄生
16
!function() {
17
    let F = function() {};         // 创建一个空对象作为寄生
18
    F.prototype = Tiger.prototype; // 将父类的原型对象寄生到空对象的原型对象上
19
    new F().constructor  = Cat;    // 构造指针手动指向子类
20
    Cat.prototype = new F();       // 将空对象的实例作为子类的原型对象 (子类只继承了父类的原型上的方法和属性)
21
}();
22
23
let cat = new Cat("鱼");
24
console.log(cat.eat); // 鱼 // (子类实例自身的属性)
25
console.log(cat.leg); // 四条腿 (继承了父类原型上的属性)
26
console.log(cat.getEat());

寄生组合继承是实现基于类型的继承的最有效的方式,但是实现起来相对复杂,代码量会很大。

ES6 继承(这里在本节作为了解)

ES6 中提供了 class 构造函数的写法,对于继承 class 可以使用 extends 关键字。

1
class Tiger {
2
    constructor(leg){
3
        this.leg = leg;
4
    }
5
    getLeg() {
6
        console.log(this.leg);
7
    }
8
}
9
class Cat extends Tiger {  
10
    constructor(leg, eat) {
11
        super(leg);  //相当于构造继承中的 Tiger.call(this, leg);
12
        this.eat = eat;
13
    }
14
    //子类独有的方法
15
    getEat() {
16
        return this.eat;
17
    }
18
}
19
20
    const cat = new Cat("四条腿", "鱼");
21
    cat.getLeg();
22
    cat.getEat();
23
    console.log(cat instanceof Cat);    //true
24
    console.log(cat instanceof Tiger);  //true

拓展:

1
ES6 提供了 Object.create() 方法也可以实现继承。
2
语法:Object.create(proto[, propertiesObject])