愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Javascript学习总结——六种继承方法

一、原型链继承

原型链继承是最简单的一种继承方式,只要将新建的父类对象赋给子类的prototype即可

1、实现方式:

function Super() {
    this.val = 1;
    this.arr = [1, 2];
}

function Sub(){}
Sub.prototype = new Super(); // 核心实现代码

var s1 = new Sub;
var s2 = new Sub;

s1.val = 2;
console.log(s1.val, s2.val); // 输出:2 1

s1.arr.push(3);
console.log(s1.arr, s2.arr); // 输出:[1, 2, 3] [1, 2, 3]

2、总结如下:
1)优点:实现简单
2)缺点:

  • 修改s1.arr后导致s2.arr也变更了(这是因为s1没有本地属性arr,于是它沿着原型链往上查找,找到了原型中的arr属性,但是因为引用对象是被所有实例共享的,所以对s1.arr的修改也影响到了s2.arr)
  • 创建子类实例的时候,是无法向父类构造器传递参数的


二、借用构造函数

借用构造函数的核心思想,是直接调用父类的构造函数,相当于把父类复制了一份给了子类,完全没有用到原型

1、实现方式

function Super(val) {
    this.val = val;
    this.arr = [1, 2];
}

function Sub(val) {
    Super.call(this, val); // 核心实现
}

var s1 = new Sub(1);
var s2 = new Sub(2);

s1.val = 5;
console.log(s1.val, s2.val); // 输出:5 2
s1.arr.push(3);
console.log(s1.arr, s2.arr); // 输出:[1, 2, 3] [1, 2]

2、总结:
1)优点:解决了原型链继承方式中引用实例是共享的的问题;子类可以向父类的构造函数传递参数
2)缺点:由于这种方式主要是通过复制父类的属性实现,所以,对于父类中的方法也是复制一遍,无法实现共享方法代码,我们可以尝试对原型链继承方法中添加以下语句:

Super.prototype.hello = function(){}
s1.hello == s2.hello; // 返回true

这时候,我们再用借用构造函数继承方法测试如下:

function Super(val) {
    this.val = val;
    this.arr = [1, 2];
    this.hello = function() {}
}

s1.hello == s2.hello; // 返回false 



三、组合继承

组合继承把属性放在父类构造器里,而把方法放在原型里。这种方式是比较常用的

1、实现方式

function Super(val) {
    this.val = val;
    this.arr = [1, 2];
}
Super.prototype.hello = function(){} // 核心实现

function Sub(val) {
    Super.call(this, val); // 核心实现
}
Sub.prototype = new Super();

var s1 = new Sub(1);
var s2 = new Sub(2);

s1.arr.push(3);
console.log(s1.arr, s2.arr); // 输出:[1, 2, 3] [1, 2]
console.log(s1.hello == s2.hello); // 输出:true

2、总结:
1)优点:

  • 组合继承,解决了父类引用属性在实例中是共享的问题(因为调用了父类构造器,会把父类的属性复制一遍,成为本地属性,就避免了在原型链上查找然后修改原型中引用属性的问题)
  • 可以给父类构造函数传递参数
  • 可以实现方法复用
    2)缺点:父类构造函数调用了两次(一次是在原型指向新建的父类对象里,一次是在调用父类构造函数里)


四、原型式继承

原型式主要是通过一个中间函数来完成继承的过程。用extendsFrom函数创建一个空的新对象,再逐步填充实例属性

1、实现方式

// 核心函数:
function extendsFrom(obj) {
    var F = function(){};
    F.prototype = obj;
    return new F();
}

function Super() {
    this.val = 1;
    this.arr = [1];
}

var sup = new Super();
var s1 = extendsFrom(sup); // 核心
var s2 = extendsFrom(sup);

console.log(s1.val, s1.name); // 输出:1 Demo

s1.arr.push(2);
console.log(s1.arr, s2.arr); // 输出:[1, 2] [1, 2]

分析一下,可知:因为var sup = new Super(),所以sup.__proto__指向Super.prototype,因为var sub = new F(),所以sub.__proto__指向F.prototypesup,而sup__proto__可以指向Super.prototype,所以就形成了:
sub.__proto__ == sup
sub.__proto__.__proto__ == Super.prototype
的原型链
2、总结:
1)优点:不需要定义子类型构造函数,直接从已有的父类对象衍生得到新对象
2)缺点:

  • 因为用父类对象直接充当了子类的原型对象(sub.__proto__指向父类对象sup),所以无法避免引用对象共享的问题
  • 无法实现代码复用


五、寄生式继承

其实所谓寄生式继承,就是给原型式穿个马甲而已(囧),把它封装起来,使得更好看罢了

1、实现方式

function extendsFrom(obj) {
    var F = function(){};
    F.prototype = obj;
    return new F();
}

function Super() {
    this.val = 1;
    this.arr = [1];
}

function getSub(parent) {
    var sub = extendsFrom(parent);
    // 加入内容
    sub.name = "Hello";
    sub.greet = function() {
        console.log("Hello, world!");
    }

    return sub;
}

var sup = new Super;
var s1 = getSub(sup);
var s2 = getSub(sup);

s1.val = 2;
console.log(s1.val, s2.val); // 输出:2 1
console.log(s1.greet == s2.greet); // 输出:false

2、总结:
1)优点:和原型式一样
2)缺点:还是不能实现方法代码复用


六、寄生组合继承

这种继承方式号称是最佳的继承方式,是对组合继承和寄生继承的优化

1、实现方式:

function extendsFrom(obj) {
    var F = function(){}
    F.prototype = obj;

    return new F();
}

function Super() {
    this.val = 1;
    this.arr = [1, 2];
}
Super.prototype.hello = function(){};

function Sub() {
    Super.call(this);
}
var temp = extendsFrom(Super.prototype);
temp.constructor = Sub;
Sub.prototype = temp;

var s1 = new Sub;
var s2 = new Sub;

s1.val = 2;
console.log(s1.val, s2.val); // 输出:2 1

s1.arr.push(3);
console.log(s1.arr, s2.arr); // 输出:[1, 2, 3] [1, 2]

console.log(s1.hello == s2.hello); // 输出:true

2、总结:
1)优点:堪称完美!
2)缺点:用起来麻烦(=.=)
3、分析:我们知道原型式继承有个缺点,就是sub.__proto__ == sup,也就是父类型的对象直接作为了子类型对象的原型,这样子的情况下,会导致共享引用对象问题。而寄生组合继承方式下,当var temp = extendsFrom(Super.prototype)的时候,我们实现了temp.__proto__指向了Super.prototype,但是此时的temp的构造器,是指向Super的,所以,我们还需要修复这个构造器,使得它指向Sub,因此使用了:

var temp = extendsFrom(Super.prototype);
temp.constructor = Sub;
Sub.prototype = temp;

这样子的情况下,就实现了继承关系,也不会导致调用父类构造函数两次。这之后,采用借用构造函数继承的思想,调用父类的构造函数,解决共享引用对象问题。


参考资料

本文学习自:重新理解JS的6种继承方式,在文章的基础上结合自己的理解总结。