Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mixin、多重继承与装饰者模式 #97

Open
youngwind opened this issue Nov 7, 2016 · 17 comments
Open

Mixin、多重继承与装饰者模式 #97

youngwind opened this issue Nov 7, 2016 · 17 comments
Labels

Comments

@youngwind
Copy link
Owner

youngwind commented Nov 7, 2016

疑问

最早接触mixin这个概念,是在使用React的时候。那时候对mixin的认知是这样的:“React不同的组件类可能需要相同的功能,比如一样的getDefaultPropscomponentDidMount等。

我们知道,到处重复地编写同样的代码是不好的。为了将同样的功能添加到多个组件当中,你需要将这些通用的功能包装成一个mixin,然后导入到你的模块中。

出处: React Mixin 的使用

那时候我以为mixin是React独创的一个概念,直到后来我在另外的很多资料中也发现mixin的踪影,比如Vue中也有mixin。仔细研究了一番,才猛地发现:原来mixin是一种模式,有着广泛的应用。
下面我们就来一一理清思路。

Mixin的由来

关于JS中mixin是怎么来的,有两派观点。一派是模仿类,另一派是多重继承

模仿类

众所周知,JS没有真正意义的类。为什么这门语言在设计之初就没有类?为什么非要通过”别扭“的原型链来实现继承?阮一峰老师的这篇文章给出了答案。真正的类在继承中是复制的。因此,虽然JS中没有类,但是无法阻挡众多JS开发者模仿类的复制行为,由此引入了mixin(混入)。
下面举个例子。Vehicle是一个交通工具”类“,Car是一个小汽车”类“,Car要继承于Vehicle,最常见的方法应该是通过原型链。但是,下面的代码却没有这么做。

function mixin(sourceObj, targetObj){
    for(let key in sourceObj){
        if(!(key in targetObj)){
            targetObj[key] = sourceObj[key];
        }
    }
    return targetObj;
}

let Vehicle = {
    engines: 1,
    ignition () {
        console.log("Turning on my engine.");
    },
    drive () {
        this.ignition();
        console.log("Steering and moving forward!");
    }
};

let Car = mixin(Vehicle, {
    wheels: 4,
    drive () {
        Vehicle.drive.call(this);
        console.log(`Rolling on all ${this.wheels} wheels`);
    }
})

Car.drive();

仔细观察上面的代码,我们能够发现:Vehicle类的方法都被复制到Car的属性中(当然,避开了同名属性)。进一步思考,我们最终想要得到的结果是Car能够访问到原先Vehicle有的属性,比如ignition。

  1. 如果是通过常见的原型链,Car固然能够访问到ignition,不过那是沿着原型链向上查找才找到的。
  2. 如果是通过不常见的mixin,Car也能访问到ignition,不过这次是直接在Car的属性上找到的。

so,这就是为了模仿类而引入的mixin。下面我们来看另一派的观点。

多重继承

在面向对象的语言中(如C++、Java和JavaScript),单一继承都是非常常见的。但是,如果想同时继承多于一个结构,例如”猫“在继承”动物“的同时,又想继承”宠物“,那怎么办?
C++给出的答案是:多重继承
然而,多重继承也是一把双刃剑。它在解决问题的同时,却又增加了程序的复杂性和含糊性,最为典型的当属钻石问题。因此,Java和JavaScript都不支持多重继承。然而,多重继承所要解决的问题依然存在,那么,他们各自又是如何解决的呢?
Java的解决方案是:通过原生的接口继承来间接实现多重继承。
JS的解决方案是:原生JS没有解决方案(别忘了,设计之初,这门语言就是定位为很简单的脚本语言,甚至连”类“都不愿意引入,怎么可能会考虑多重继承呢?)。所以,众多JS开发者引入了mixin来解决这个问题。
举个例子。如下面代码所示,有两个超类,SuperTypeAnotherSuperType,有一个子类SubType。一开始SubType继承于SuperType,但是,现在我又想让SubType实例化的某个对象obj也拥有超类AnotherSuperType的方法,怎么办?

// 超类
function SuperType(name){
    this.name = name;
}

// 子类
function SubType(name) {
    SuperType.call(this, name);
}

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

let obj = new SubType("youngwind");

function mixin(sourceObj, targetObj){
    for(let key in sourceObj){
        if(!(key in targetObj)) {
           targetObj[key] = sourceObj[key];
        }
    }
}

// 另一个超类
function AnotherrSuperType(){};

AnotherrSuperType.prototype.sayHi = function() {
    console.log(`hi,${this.name}.`);
}

let anotherObj = new AnotherrSuperType();

mixin(anotherObj, obj);

obj.sayHi();  // hi...

看,这就是为了替代多重继承所引入的mixin。

异曲同工

纵观上面两派的观点,虽然各自要解决的问题和应用场景不尽相同,但是,有一处是相同的,那就是:mixin(混合)本质上是将一个对象的属性拷贝到另一个对象上面去,其实就是对象的融合。
这时候我们回过头来考察React和Vue中的mixin,就容易理解多了。而且,不仅是React和mixin,很多js库都有类似的功能,一般定义在util工具类中。有时候会换个名字,不叫mixin,叫extend,不过它们本质上是一样的。另外,说到对象融合,Object.assign也是常用的方法,它跟mixin有一个重大的区别在于:mixin会把原型链上的属性一并复制过去(因为for...in),而Object.assign则不会。

寄生式继承

什么?mixin居然也跟寄生式继承有关?
是的。正是结合mixin与工厂模式,才诞生了寄生式继承。
《高程》章节6.3中,介绍了很多种继承方式,其中我一直都记不住寄生式继承,因为我无法理解为什么会有这么一种继承方式(连名字都是怪怪的)。
下面来看这个例子。

// 传统类
function Vehicle(){
    this.engines = 1;
}

Vehicle.prototype.ignition = function(){
    console.log("Turning on my engine");
}

Vehicle.prototype.drive = function(){
    this.ignition();
    console.log("Steering and moving forward!");
}

// 寄生类
function Car(){
    // 首先,Car是一个Vehicle
    let car = new Vehicle();
    
    // 接着,我们对Car进行定制
    car.wheels = 4;
    
    // 保存Vehicle::drive()的函数引用
    let vehDrive = car.drive;
    
    // 重写drive方法
    car.drive = function(){
        vehDrive.call(this);
        console.log(`Rolling on all ${this.wheels} wheels!`);
    }
    
    return car;
}

let myCar = new Car();

myCar.drive();

仔细观察代码,我们能够发现寄生式继承的原理:

创建一个仅用于封装继承过程的函数(Car),该函数在内部以某种方式(添加属性、重定义方法)来增强对象,最后再像真地是它做了所有工作一样返回对象。(对于一般的构造函数,一般是默认返回this的,无须指定。)

出处:《高程》章节6.3.5,第171页

PS,此处对寄生式继承进行阐述,并非代表我推荐使用这种继承模式,我只是希望通过结合mixin来帮助理解为什么会产生这种继承方式。恰恰相反,我从未在生产环境中用过这种(怪怪的)模式,我个人常用的还是经典的组合模式。

ES7装饰器

为什么我会由mixin联想到ES7装饰器呢?
因为我记得以前刚开始用React创建组件的时候,还是老的语法,const demo = React.createClass,这种情况下是可以用mixin的。后来React用ES6重写之后,就有了class demo extends React.Component这样新的写法,这种情况就用不了mixin了。
why?大概说来,就是ES6的class语法不支持,具体的可以参考这篇文章。也正是由此我发现了ES7有装饰器(decorator)这一功能。仔细看了一些,并未能完全掌握,加之decorator这东西太高级了,距离我生产环境太远,就先作罢,暂不深究。
然而,正是“装饰器”这一名字,让我想起了设计模式中有一种就叫装饰者模式,这东西就非常有实用价值了。

装饰者模式

接手别人的项目总是不可避免的(很可能是常见的),对于一些别人早就写好的函数,当我们想往里面添加一些功能的时候,该怎么办呢?
举个例子,我们的目标是在原有的函数doSomething里面的最后再输出一些其他的东西,下面是不好的(可能是常用的)的做法。

// 修改前
let doSomething = function() {
  console.log(1);
}

// 修改后
let doSomething = function() {
  console.log(1);
  console.log(2);
}

可以看到,这种做法非常简单粗暴,我们直接修改了别人的函数,这违反了开放-封闭原则,并非正途。

下面是好一些的做法。

let doSomething = function() {
  console.log(1);
}

let _doSomething = doSomething;

doSomething = function() {
  _doSomething();
  console.log(2);
}

doSomething();

仔细分析代码,我们能发现其原理:用一个临时变量暂存原函数,然后定义一个新函数拓展原有的函数。这就是装饰者模式:为函数(对象)动态增加职责,又不直接修改这个函数(对象)本身。
然而,我们同时也能发现这个方案的缺点:需要增加一个临时变量来存储原函数。当需要装饰的函数越来越多的时候,临时变量的数量呈线性增长,代码将变得越来越难以维护。
怎么办?请看下面的例子。

Function.prototype.before = function(beforefn){
    let _self = this;
    return function(){
        beforefn.apply(this, arguments);
        return _self.apply(this, arguments);
    }
}

Function.prototype.after = function(afterfn){
    let _self = this;
    return function(){
        let ret = _self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
}

let doSomething = function() {
  console.log(1);
}

doSomething = doSomething.before(() => {
    console.log(3);
}).after(() => {
    console.log(2);
});

doSomething();  // 输出 312

这样,有了beforeafter方法,我们就能在函数的前面和后面动态添加功能,而且非常的优雅。

这一部分摘抄自曾探所著《JavaScript设计模式与开发实践》第二部分第15章,书中还列举了很多在业务上常见的情况,比如在已经写好的按钮点击事件上添加数据打点功能,比如把字段校验功能移到submit函数外边等等,这些例子都非常的生动实用,建议读者直接阅读原著。

总结

长长的思路到此终于结束,让我们回想一下:无论是复制类的mixin、多重继承的mixin、寄生式继承、ES7的装饰器、设计模式中的装饰者模式,它们都有一个共同点,那就是:在不修改原有的类/对象/函数的前提下,为新的类/对象/函数添加新的职责,以增强其功能。其实这些都是以下两个程序设计原则的外化:开发-封闭原则单一职责原则

@ian4hu
Copy link

ian4hu commented Nov 7, 2016

然后这个东西在stateless function component里面废了,React后来又推荐使用stateless function component了

@ian4hu
Copy link

ian4hu commented Nov 7, 2016

同名方法返回值方面,我调用了合并后的方法,那么方法的返回值到底是mixin里的还是后面声明的?

@youngwind
Copy link
Owner Author

同名方法返回值方面,我调用了合并后的方法,那么方法的返回值到底是mixin里的还是后面声明的?

@ian4hu 这个问题我没明白,请描述清楚一些。

@youngwind youngwind added the JS label Nov 7, 2016
@ian4hu
Copy link

ian4hu commented Nov 7, 2016

比如

class mixin1 {
  getName() {
    return 'mixin'
  }
}


class A {
  mixins: [mixin1]
  getName() {
    return 'a'
  }
}

这时候 let a = new A(), a.getName() 返回值是什么

@youngwind
Copy link
Owner Author

@ian4hu ES6的class语法并不支持mixin,你为什么要这样写?

@ian4hu
Copy link

ian4hu commented Nov 7, 2016

只是一个示例,不在于ES6语法

@youngwind
Copy link
Owner Author

@ian4hu 我明白你的意思了,你想问“同名属性”是否覆盖对吧? 这个问题没有固定的答案,因为mixin总是人为地实现的,无论是否覆盖,都不改变混入的本质。是否覆盖,关键就是看你在实现mixin函数的时候有没有进行同名属性的判断。比如下面这种就不会覆盖。

function mixin(sourceObj, targetObj){
    for(let key in sourceObj){
        if(!(key in targetObj)){   // 当然,你要是喜欢把这行去掉,那就是覆盖同名属性了。
            targetObj[key] = sourceObj[key];
        }
    }
    return targetObj;
}

@ian4hu
Copy link

ian4hu commented Nov 7, 2016

@youngwind 我明白了,这个其实不是语言规范里的 所以要视具体实现而定

@Thinking80s
Copy link

mark

@ihaichao
Copy link

ihaichao commented Nov 10, 2016

在最后一个例子当中,doSomething 这个方法不是也被修改了吗,打印 doSomething 结果是
function () {
let ret = _self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
}

@youngwind
Copy link
Owner Author

@ihaichao 是的,doSomething确实是改变了。
但是doSomething只是一个指针,所谓“不改变原函数”并非指不改变doSomething,而是指不改变原先doSomething指针所指向的那个函数,也就是下面这个。

function() {
  console.log(1);
}

前后的主要区别在于:用一个新的函数包裹原函数,然后把doSomething指向这个新函数。

@obetame
Copy link

obetame commented Nov 11, 2016

讲的很清晰,同时也发现自己对这些js中的设计模式这方面的不足,要重新翻翻书了

@alex2wong
Copy link

看到这儿,忍不住评论下。写得很棒,把许多问题联系起来看。装饰器在Angular中也有广泛应用,给组件装上不同的@component@directive 等装饰器,赋予不同的临时功能。 还能想起曾探讲的 给飞机装上原子弹的例子。学习了:)

@mygaochunming
Copy link

@youngwind 你好,关于你例子中的“寄生式继承”,代码是自己写的吗?myCar instanceof Car 返回false,因为在这个过程中myCar的__proto__已经是Vehicle的prototype了。不了解寄生式继承,难道寄生式继承就是这个样子吗?有什么存在的价值??

@magicds
Copy link

magicds commented Jul 16, 2018

beforeafter方法的扩展真的妙,学到了!

1 similar comment
@magicds
Copy link

magicds commented Jul 16, 2018

beforeafter方法的扩展真的妙,学到了!

@sanfengliao
Copy link

sanfengliao commented Jun 19, 2019 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants