前一段时间内部分享,我选择了前端常用设计模式这个题目。主要讲到:mixin模式、修饰器模式、观察者模式和发布订阅模式。在准备
ppt的过程中发现了很多以前没有注意到的问题,这里和大家分享一下。有异议的地方也欢迎留言讨论。
从继承说起
说起面向对象编程,总是绕不开继承,他也是类型扩展的基础。如果类A继承类B,则表示A具有B的能力。
这实际上是一种封装抽象,通过类,我们可以把行为整合,而类名更是一个很好的注释。比如说class Cat extends Animal,我们自然可以知道Cat具有Animal的一切特征。与此同时class Dog extends Animal,这里的Animal实际上是对动物的共有行为进行抽象,这样不仅让我们的代码逻辑更加清晰容易阅读,同时也易维护。
js的继承和原型
原型是类的特殊属性,如果真要解释的话:
- 可以将它看成是静态属性
static,它在类被实例化之前就存在,可以通过ClassName.prototype访问。 - 实例化之后,可以通过实例的
__proto__访问。 new ClassName().__proto__ === ClassName.prototype,原型被所有的实例共享。- 类的原型属性本身是原型的一个实例。所以它本身也有
__proto__属性。 - 当我们访问实例的属性的时候,会先查找自身属性,如果没有则查找原型上的属性,然后再找原型的原型,一直到最顶层,如果没有找到则返回
undefined
所以,如果把Parent类的实例赋值给Child的prototype属性,实际上就是实现了class Child extends Parent。
这样一层层的继承也就形成了一种链表的结构,也就是我们常说的原型链。而原型链最终指向的,则是Object.prototype。

谈谈继承的问题
通过上面的继承的原理,我们大致可以知道为什么说javascript是单继承的(一个类只能有一个父类)。
但是,在现实场景中,一个类具有多个特征才是常态。
就用平时写的页面Page来说,我们可能抽象出下面几个模块:
Layout类:这个类用来统一处理页面共有的布局(比如说Header和Footer)Dialog类:用来管理页面弹窗Report类:用来管理页面上报动作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工厂来注入方法和属性,我们还可以用ES6的decorator,来实现相同的功能,不知道的可以看一下阮一峰老师的ECMAScript 6 入门,虽然写法上不同,但是原理基本相同,也都很直观。
但是有一点要注意的是:
上面我们说到prototype是所有实例共有的,所以在操作prototype的时候,我们需要格外的注意。如果随意的修改prototype可能会导致我们的代码执行结果不可预期,也很难维护。因此,修改prototype的时候,个人觉得至少应该遵从在哪定义,在哪修改的规则
mixin模式也好,修饰器模式也好他们都是为了解决特定问题而存在的,也不是说继承这种写法不好,他们是相互补充的共存关系而非竞争。什么时候用什么模式,就需要我们权衡了。