likes
comments
collection
share

无处不在的原型链(下)

作者站长头像
站长
· 阅读数 9

对象继承方式及应用场景

面向对象三大特性:封装性、继承性、多态性 上一章详细地分析了原型及原型链的完整关系,接下来就说说各类继承方式的演变和继承方式的关系图以及现在主流的继承方式。

主流继承方式的演变

实际JS中有很多种继承方式,我个人更倾向于把继承方式的演变大致划分为了以下三个阶段:

无处不在的原型链(下)

原型链继承

function Super(name) {
    this.name = name;
    this.play = [1, 2, 3]
}

function Sub() {
    this.type = 'SubType';
}
Sub.prototype = new Super(); 

const sub = new Sub();

特点:

  • 父类在原型新增方法和属性,子类都能访问到
  • 子类所有实例共享同一个原型对象的所有属性/方法

缺点:

  • 创建每一个子类实例时,无法向父类构造函数自定义传参
  • 无法继承父类实例属性/方法(因为执行Sub.prototype = new Super()的时候,父类中定义的实例属性和方法已经全部赋值到子类的原型对象上了)

借用构造函数继承

function Super(myName) {
    this.name = myName;
}
Super.prototype.getName = function() {
    return this.name;
}

function Sub(myName){
    Super.call(this, myName);
    this.type = 'child'
}

const sub = new Sub('sub');
console.log(sub);  // 没问题
console.log(sub.getName());  // 会报错

特点:

  • 解决了原型链继承中,子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数

缺点:

  • 只能继承父类的实例属性和方法,不能继承父类原型属性/方法无法实现函数复用
  • 每个子类都有父类实例函数的副本,影响性能

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

function Super (name) {
    this.name = name;
    this.play = [1, 2, 3];
}
Super.prototype.getName = function () {
    return this.name;
}

function Sub(name) {
    Super.call(this, name); // 第二次调用 Super()
    this.type = 'child';
}
Sub.prototype = new Super(); // 第一次调用 Super()
Sub.prototype.constructor = Sub; // 手动挂上构造器,指向自己的构造函数

const s1 = new Sub('child1');
const s2 = new Sub('child2');
s1.play.push(4);
console.log(s1.play, s2.play);  // 不互相影响
console.log(s1.getName()); // 正常输出'child1'
console.log(s2.getName()); // 正常输出'child2'

组合继承是结合以上两种方式实现的。

特点:

  • 解决了原型链继承和借用构造函数继承方式所存在的问题,成功地继承了父类的实例属性/方法以及原型属性/方法
  • 不存在引用属性共享问题
  • 可以自定义传参
  • 函数可复用

缺点:

  • 父类构造函数会被执行两次,造成不必要的性能消耗
  • 实例对象的原型上会有多余的从父类继承下来的属性和方法(也就是Sub.prototype上残留了不必要的属性)
    • 无处不在的原型链(下)

继承关系图

在这里我们可以看一下组合继承方式构成的关系图: 无处不在的原型链(下) 从图中我们也可以看到为什么这种方式就成功完成了继承,因为它构建了一个完整的原型链查找关系,也就是__proto__的查找关系(原型链查找关系不明白的可以翻看上一章内容)

寄生式组合继承

针对上一种继承方式,还有可优化空间,于是就演变为了寄生式组合继承

function Super (name) {
    this.name = name;
    this.play = [1, 2, 3];
}
Super.prototype.getName = function () {
    return this.name;
}

function Sub(name) {
    Super.call(this, name);
    this.type = 'child';
}
Sub.prototype = Object.create(Super.prototype); // (优化)
Sub.prototype.constructor = Sub; // 手动挂上构造器,指向自己的构造函数

const s1 = new Sub('child1');

其实对比上一种,寄生式组合继承只需替换一行代码完成了最终的优化 将Sub.prototype = new Super();替换为了Sub.prototype = Object.create(Super.prototype); 如果不清楚Object.create用法的小伙伴可自行查阅MDN文档

特点:

这两种不同写法的最终目的都是为了实现子类继承父类原型上的数据,但不同点是通过Sub.prototype = Object.create(Super.prototype); 这种方式使得我们无需生成一份父类的实例副本在子类原型上实现继承,而是直接通过更改子类原型链的指向。

Sub.prototype = Object.create(Super.prototype); 可以理解成以下操作: Sub.prototype = { __proto__: Super.prototype }

继承关系图

在这里我们可以看一下寄生式组合继承构成的关系图:

无处不在的原型链(下)

class继承(ES6)

这种是自ES6以后我们一直在用的继承方式,其实也就是寄生式组合继承的语法糖。我们利用babel工具进行转换,会发现extends关键字实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式。只不过在class中还实现了父类静态属性/方法继承。

无处不在的原型链(下)

Vue中的继承应用

思考

我们平时在Vue2开发中,常常会在组件中调用到this.$emit或是自定义的通讯方式this.$eventbus,或是ElementUI中的全局方法this.$message等等。那么这些方法是从哪里调用的?

eventbus事件总线的使用

无处不在的原型链(下) 我们平时可能会这样去定义事件总线方便在各组件中调用到其方法,但是我们为什么可以在组件中拿到Vue.prototype中的方法或属性?

Vue构造函数继承

我们在使用Vue组件化开发时,Vue会帮我们注册我们定义的组件,在组件注册时会执行到Vue.extend方法,该方法部分源码如下:

Vue.extend = function (extendOptions) {
    var Super = this; 
    …
    var Sub = function VueComponent (options) {
        this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    …
    return Sub
}

是不是看着似曾相识的写法,其实这里就用到了寄生式组合继承,我们可以看一看继承关系图:

无处不在的原型链(下) 我们自己定义的组件就是经过VueComponent构造函数实例化的component组件,组件中通过this.xxx调用方法就会顺着对象的原型链一层一层往上查找。

文章部分内容参考自作者:HelloJames 链接:www.jianshu.com/p/555cf3435… 来源:简书

转载自:https://juejin.cn/post/7138698547352272933
评论
请登录