从继承到mixin---谈谈个人对组件抽象的理解

1,264 阅读5分钟

前一段时间内部分享,我选择了前端常用设计模式这个题目。主要讲到:mixin模式、修饰器模式、观察者模式和发布订阅模式。在准备ppt的过程中发现了很多以前没有注意到的问题,这里和大家分享一下。有异议的地方也欢迎留言讨论。

从继承说起

说起面向对象编程,总是绕不开继承,他也是类型扩展的基础。如果类A继承类B,则表示A具有B的能力。

这实际上是一种封装抽象,通过类,我们可以把行为整合,而类名更是一个很好的注释。比如说class Cat extends Animal,我们自然可以知道Cat具有Animal的一切特征。与此同时class Dog extends Animal,这里的Animal实际上是对动物的共有行为进行抽象,这样不仅让我们的代码逻辑更加清晰容易阅读,同时也易维护。

js的继承和原型

原型是类的特殊属性,如果真要解释的话:

  1. 可以将它看成是静态属性static,它在类被实例化之前就存在,可以通过ClassName.prototype访问。
  2. 实例化之后,可以通过实例的__proto__访问。
  3. new ClassName().__proto__ === ClassName.prototype,原型被所有的实例共享。
  4. 类的原型属性本身是原型的一个实例。所以它本身也有__proto__属性。
  5. 当我们访问实例的属性的时候,会先查找自身属性,如果没有则查找原型上的属性,然后再找原型的原型,一直到最顶层,如果没有找到则返回undefined

所以,如果把Parent类的实例赋值给Childprototype属性,实际上就是实现了class Child extends Parent

这样一层层的继承也就形成了一种链表的结构,也就是我们常说的原型链。而原型链最终指向的,则是Object.prototype

原型链

谈谈继承的问题

通过上面的继承的原理,我们大致可以知道为什么说javascript是单继承的(一个类只能有一个父类)。

但是,在现实场景中,一个类具有多个特征才是常态。

就用平时写的页面Page来说,我们可能抽象出下面几个模块:

  1. Layout类:这个类用来统一处理页面共有的布局(比如说HeaderFooter
  2. Dialog类:用来管理页面弹窗
  3. Report类:用来管理页面上报动作
  4. Share类:用来管理页面分享

这时候,很多做法都是把这四个统一塞在一个Base类里面,然后所有的页面继承这个类class Page extends Base

这样写的话会导致Base类的代码越来越多,也让我们阅读起来很不明确一眼看不出来这个页面到底能干嘛,Base这个单词,本身就和A、B、C这种命名没多大差别。

vue的mixins

由于之前写过vue,对它的mixins属性印象比较深刻。对于上面提到的场景,vue的写法一般如下:

export default {
  mixins: [Layout, Dialog, Report, Share]
}

很清晰,对不对?这个就是mixin模式了。其实原理也很简单,就是将N个对象的方法和属性,通过一定的规则合并起来并输出一个新的混合(mixin)对象。

当然,这里混合的规则是需要我们自己定义的。

function mixin( sourceObj, targetObj ) {
  for (var key in sourceObj) {
    // 只会在不存在的情况下复制 
    if (!(key in targetObj)) {
      targetObj[key] = sourceObj[key];
    }
  }
  return targetObj; 
}

扩展一下

上面的mixin显然是针对实例话对象的混合,对于未实例话的类,我们怎么实现这种混合呢?

这里就用到原型了。在未实例化之前,我们可以访问和操作prototype的,这时候只需要混合prototype就可以达到多继承的目的了。

代码大致如下:

// mixinFactory
function mixinFactory(base, ...targets) {
  if(targets.length === 0 || !base || !base.prototype) {
    return base
  }
  for(let i in targets) {
    const target = targets[i]
    for(let key in target) {
      if (!(key in base)) {
        base.prototype[key] = target[key]
      }
    }
  }
  return base
}

// target1
const target1 = { t1: 't1', getT1() { return 'target1' } }

// target2
const target2 = { t2: 't2', getT2() { return 'target2' } }

// Page
class Page {}

export default mixinFactory(Page, target1, target2)

上面我们说到prototype是所有实例共有的,所以在操作prototype的时候,我们需要格外的注意。如果随意的修改prototype可能会导致我们的代码执行结果不可预期,也很难维护。因此,修改prototype的时候,个人觉得至少应该遵从:

在哪定义,在哪修改的规则

小结

mixin模式实际上是一种修饰器模式,本质上就是动态的往一个对象动态的添加方法。除了我上面提到的用mixins工厂来注入方法和属性,我们还可以用ES6decorator,来实现相同的功能,不知道的可以看一下阮一峰老师的ECMAScript 6 入门,虽然写法上不同,但是原理基本相同,也都很直观。

但是有一点要注意的是:

上面我们说到prototype是所有实例共有的,所以在操作prototype的时候,我们需要格外的注意。如果随意的修改prototype可能会导致我们的代码执行结果不可预期,也很难维护。因此,修改prototype的时候,个人觉得至少应该遵从在哪定义,在哪修改的规则

mixin模式也好,修饰器模式也好他们都是为了解决特定问题而存在的,也不是说继承这种写法不好,他们是相互补充的共存关系而非竞争。什么时候用什么模式,就需要我们权衡了。