{"action":"created","issue":{"url":"https://api.github.com/repos/youngwind/blog/issues/97","repository_url":"https://api.github.com/repos/youngwind/blog","labels_url":"https://api.github.com/repos/youngwind/blog/issues/97/labels{/name}","comments_url":"https://api.github.com/repos/youngwind/blog/issues/97/comments","events_url":"https://api.github.com/repos/youngwind/blog/issues/97/events","html_url":"https://github.com/youngwind/blog/issues/97","id":187604444,"number":97,"title":"Mixin、多重继承与装饰者模式","user":{"login":"youngwind","id":8401872,"avatar_url":"https://avatars.githubusercontent.com/u/8401872?v=3","gravatar_id":"","url":"https://api.github.com/users/youngwind","html_url":"https://github.com/youngwind","followers_url":"https://api.github.com/users/youngwind/followers","following_url":"https://api.github.com/users/youngwind/following{/other_user}","gists_url":"https://api.github.com/users/youngwind/gists{/gist_id}","starred_url":"https://api.github.com/users/youngwind/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/youngwind/subscriptions","organizations_url":"https://api.github.com/users/youngwind/orgs","repos_url":"https://api.github.com/users/youngwind/repos","events_url":"https://api.github.com/users/youngwind/events{/privacy}","received_events_url":"https://api.github.com/users/youngwind/received_events","type":"User","site_admin":false},"labels":[{"id":451917185,"url":"https://api.github.com/repos/youngwind/blog/labels/ES6","name":"ES6","color":"fbde34","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":5,"created_at":"2016-11-07T00:54:32Z","updated_at":"2016-11-07T05:01:48Z","closed_at":null,"body":"# 疑问\r\n最早接触mixin这个概念，是在使用React的时候。那时候对mixin的认知是这样的：“React不同的组件类可能需要相同的功能，比如一样的`getDefaultProps`、`componentDidMount`等。\r\n\r\n> 我们知道，到处重复地编写同样的代码是不好的。为了将同样的功能添加到多个组件当中，你需要将这些通用的功能包装成一个mixin，然后导入到你的模块中。\r\n\r\n出处: [React Mixin 的使用](https://segmentfault.com/a/1190000003016446)\r\n\r\n那时候我以为mixin是React独创的一个概念，直到后来我在另外的很多资料中也发现mixin的踪影，比如[Vue中也有mixin](https://vuejs.org.cn/guide/mixins.html)。仔细研究了一番，才猛地发现：**原来mixin是一种模式，有着广泛的应用。**\r\n下面我们就来一一理清思路。\r\n\r\n# Mixin的由来\r\n关于JS中mixin是怎么来的，有两派观点。一派是**模仿类**，另一派是**多重继承**。\r\n\r\n## 模仿类\r\n众所周知，JS没有真正意义的类。为什么这门语言在设计之初就没有类？为什么非要通过”别扭“的原型链来实现继承？[阮一峰老师的这篇文章](http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html)给出了答案。真正的类在继承中是复制的。因此，虽然JS中没有类，但是无法阻挡众多JS开发者模仿类的复制行为，由此引入了mixin（混入）。\r\n下面举个例子。Vehicle是一个交通工具”类“，Car是一个小汽车”类“，Car要继承于Vehicle，最常见的方法应该是通过原型链。但是，下面的代码却没有这么做。\r\n```js\r\nfunction mixin(sourceObj, targetObj){\r\n    for(let key in sourceObj){\r\n        if(!(key in targetObj)){\r\n            targetObj[key] = sourceObj[key];\r\n        }\r\n    }\r\n    return targetObj;\r\n}\r\n\r\nlet Vehicle = {\r\n    engines: 1,\r\n    ignition () {\r\n        console.log(\"Turning on my engine.\");\r\n    },\r\n    drive () {\r\n        this.ignition();\r\n        console.log(\"Steering and moving forward!\");\r\n    }\r\n};\r\n\r\nlet Car = mixin(Vehicle, {\r\n    wheels: 4,\r\n    drive () {\r\n        Vehicle.drive.call(this);\r\n        console.log(`Rolling on all ${this.wheels} wheels`);\r\n    }\r\n})\r\n\r\nCar.drive();\r\n```\r\n仔细观察上面的代码，我们能够发现：Vehicle类的方法都被复制到Car的属性中（当然,避开了同名属性）。进一步思考，我们最终想要得到的结果是Car能够访问到原先Vehicle有的属性，比如ignition。\r\n\r\n1. 如果是通过常见的原型链，Car固然能够访问到ignition，不过那是沿着原型链向上查找才找到的。\r\n2. 如果是通过不常见的mixin，Car也能访问到ignition，不过这次是直接在Car的属性上找到的。\r\n\r\nso，这就是为了模仿类而引入的mixin。下面我们来看另一派的观点。\r\n\r\n## 多重继承\r\n在面向对象的语言中（如C++、Java和JavaScript），单一继承都是非常常见的。但是，如果想同时继承多于一个结构，例如”猫“在继承”动物“的同时，又想继承”宠物“，那怎么办？\r\nC++给出的答案是：[多重继承](https://zh.wikipedia.org/wiki/%E5%A4%9A%E9%87%8D%E7%BB%A7%E6%89%BF)。\r\n然而，多重继承也是一把双刃剑。它在解决问题的同时，却又增加了程序的复杂性和含糊性，最为典型的当属[钻石问题](http://blog.csdn.net/tounaobun/article/details/8443228)。因此，Java和JavaScript都不支持多重继承。然而，多重继承所要解决的问题依然存在，那么，他们各自又是如何解决的呢？\r\nJava的解决方案是：通过原生的接口继承来间接实现多重继承。\r\nJS的解决方案是：原生JS没有解决方案（别忘了，设计之初，这门语言就是定位为很简单的脚本语言，甚至连”类“都不愿意引入，怎么可能会考虑多重继承呢？）。所以，众多JS开发者引入了mixin来解决这个问题。\r\n举个例子。如下面代码所示，有两个超类，`SuperType`和`AnotherSuperType`，有一个子类`SubType`。一开始`SubType`继承于`SuperType`，但是，现在我又想让`SubType`实例化的某个对象obj也拥有超类`AnotherSuperType`的方法，怎么办？\r\n```js\r\n// 超类\r\nfunction SuperType(name){\r\n    this.name = name;\r\n}\r\n\r\n// 子类\r\nfunction SubType(name) {\r\n    SuperType.call(this, name);\r\n}\r\n\r\nSubType.prototype = new SuperType();\r\nSubType.prototype.constructor = SubType;\r\n\r\nlet obj = new SubType(\"youngwind\");\r\n\r\nfunction mixin(sourceObj, targetObj){\r\n    for(let key in sourceObj){\r\n        if(!(key in targetObj)) {\r\n           targetObj[key] = sourceObj[key];\r\n        }\r\n    }\r\n}\r\n\r\n// 另一个超类\r\nfunction AnotherrSuperType(){};\r\n\r\nAnotherrSuperType.prototype.sayHi = function() {\r\n    console.log(`hi,${this.name}.`);\r\n}\r\n\r\nlet anotherObj = new AnotherrSuperType();\r\n\r\nmixin(anotherObj, obj);\r\n\r\nobj.sayHi();  // hi...\r\n```\r\n看，这就是为了替代多重继承所引入的mixin。\r\n\r\n## 异曲同工\r\n纵观上面两派的观点，虽然各自要解决的问题和应用场景不尽相同，但是，有一处是相同的，那就是：**mixin（混合）本质上是将一个对象的属性拷贝到另一个对象上面去，其实就是对象的融合。**\r\n这时候我们回过头来考察React和Vue中的mixin，就容易理解多了。而且，不仅是React和mixin，很多js库都有类似的功能，一般定义在util工具类中。有时候会换个名字，不叫mixin，叫`extend`，不过它们本质上是一样的。另外，说到对象融合，Object.assign也是常用的方法，它跟mixin有一个重大的区别在于：**mixin会把原型链上的属性一并复制过去（因为`for...in`），而Object.assign则不会。**\r\n\r\n# 寄生式继承\r\n什么？mixin居然也跟寄生式继承有关？\r\n是的。正是结合mixin与工厂模式，才诞生了寄生式继承。\r\n《高程》章节6.3中，介绍了很多种继承方式，其中我一直都记不住寄生式继承，因为我无法理解为什么会有这么一种继承方式（连名字都是怪怪的）。\r\n下面来看这个例子。\r\n```js\r\n// 传统类\r\nfunction Vehicle(){\r\n    this.engines = 1;\r\n}\r\n\r\nVehicle.prototype.ignition = function(){\r\n    console.log(\"Turning on my engine\");\r\n}\r\n\r\nVehicle.prototype.drive = function(){\r\n    this.ignition();\r\n    console.log(\"Steering and moving forward!\");\r\n}\r\n\r\n// 寄生类\r\nfunction Car(){\r\n    // 首先，Car是一个Vehicle\r\n    let car = new Vehicle();\r\n    \r\n    // 接着，我们对Car进行定制\r\n    car.wheels = 4;\r\n    \r\n    // 保存Vehicle::drive()的函数引用\r\n    let vehDrive = car.drive;\r\n    \r\n    // 重写drive方法\r\n    car.drive = function(){\r\n        vehDrive.call(this);\r\n        console.log(`Rolling on all ${this.wheels} wheels!`);\r\n    }\r\n    \r\n    return car;\r\n}\r\n\r\nlet myCar = new Car();\r\n\r\nmyCar.drive();\r\n```\r\n仔细观察代码，我们能够发现寄生式继承的原理：\r\n\r\n> 创建一个仅用于封装继承过程的函数（Car），该函数在内部以某种方式（添加属性、重定义方法）来增强对象，最后再像真地是它做了所有工作一样返回对象。（对于一般的构造函数，一般是默认返回this的，无须指定。）\r\n\r\n出处：《高程》章节6.3.5，第171页\r\n\r\nPS，此处对寄生式继承进行阐述，并非代表我推荐使用这种继承模式，我只是希望通过结合mixin来帮助理解为什么会产生这种继承方式。恰恰相反，我从未在生产环境中用过这种（怪怪的）模式，我个人常用的还是经典的组合模式。\r\n\r\n# ES7装饰器\r\n为什么我会由mixin联想到[ES7装饰器](http://es6.ruanyifeng.com/#docs/decorator)呢？\r\n因为我记得以前刚开始用React创建组件的时候，还是老的语法，`const demo = React.createClass`，这种情况下是可以用mixin的。后来React用ES6重写之后，就有了`class demo extends React.Component`这样新的写法，这种情况就用不了mixin了。\r\nwhy？大概说来，就是ES6的class语法不支持，具体的可以参考[这篇文章](https://zhuanlan.zhihu.com/p/20361937)。也正是由此我发现了ES7有装饰器（decorator）这一功能。仔细看了一些，并未能完全掌握，加之decorator这东西太高级了，距离我生产环境太远，就先作罢，暂不深究。\r\n然而，正是“装饰器”这一名字，让我想起了设计模式中有一种就叫**装饰者模式**，这东西就非常有实用价值了。\r\n\r\n# 装饰者模式\r\n接手别人的项目总是不可避免的（很可能是常见的），对于一些别人早就写好的函数，当我们想往里面添加一些功能的时候，该怎么办呢？\r\n举个例子，我们的目标是在原有的函数`doSomething`里面的最后再输出一些其他的东西，下面是不好的（可能是常用的）的做法。\r\n```js\r\n// 修改前\r\nlet doSomething = function() {\r\n  console.log(1);\r\n}\r\n\r\n// 修改后\r\nlet doSomething = function() {\r\n  console.log(1);\r\n  console.log(2);\r\n}\r\n```\r\n可以看到，这种做法非常简单粗暴，我们直接修改了别人的函数，这违反了[开放-封闭原则](https://zh.wikipedia.org/wiki/%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99)，并非正途。\r\n\r\n下面是好一些的做法。\r\n```js\r\nlet doSomething = function() {\r\n  console.log(1);\r\n}\r\n\r\nlet _doSomething = doSomething;\r\n\r\ndoSomething = function() {\r\n  _doSomething();\r\n  console.log(2);\r\n}\r\n\r\ndoSomething();\r\n```\r\n仔细分析代码，我们能发现其原理：用一个临时变量暂存原函数，然后定义一个新函数拓展原有的函数。这就是装饰者模式：**为函数（对象）动态增加职责，又不直接修改这个函数（对象）本身。**\r\n然而，我们同时也能发现这个方案的缺点：**需要增加一个临时变量来存储原函数。当需要装饰的函数越来越多的时候，临时变量的数量呈线性增长，代码将变得越来越难以维护。**\r\n怎么办？请看下面的例子。\r\n```js\r\nFunction.prototype.before = function(beforefn){\r\n    let _self = this;\r\n    return function(){\r\n        beforefn.apply(this, arguments);\r\n        return _self.apply(this, arguments);\r\n    }\r\n}\r\n\r\nFunction.prototype.after = function(afterfn){\r\n    let _self = this;\r\n    return function(){\r\n        let ret = _self.apply(this, arguments);\r\n        afterfn.apply(this, arguments);\r\n        return ret;\r\n    }\r\n}\r\n\r\nlet doSomething = function() {\r\n  console.log(1);\r\n}\r\n\r\ndoSomething = doSomething.before(() => {\r\n    console.log(3);\r\n}).after(() => {\r\n    console.log(2);\r\n});\r\n\r\ndoSomething();  // 输出 312\r\n```\r\n这样，有了`before`和`after`方法，我们就能在函数的前面和后面动态添加功能，而且非常的优雅。\r\n\r\n这一部分摘抄自曾探所著《JavaScript设计模式与开发实践》第二部分第15章，书中还列举了很多在业务上常见的情况，比如在已经写好的按钮点击事件上添加数据打点功能，比如把字段校验功能移到submit函数外边等等，这些例子都非常的生动实用，建议读者直接阅读原著。\r\n\r\n# 总结\r\n长长的思路到此终于结束，让我们回想一下：**无论是复制类的mixin、多重继承的mixin、寄生式继承、ES7的装饰器、设计模式中的装饰者模式，它们都有一个共同点，那就是：在不修改原有的类/对象/函数的前提下，为新的类/对象/函数添加新的职责，以增强其功能。其实这些都是以下两个程序设计原则的外化：[开发-封闭原则](https://zh.wikipedia.org/wiki/%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99)和[单一职责原则](http://baike.baidu.com/view/4779987.htm)。**"},"comment":{"url":"https://api.github.com/repos/youngwind/blog/issues/comments/258747858","html_url":"https://github.com/youngwind/blog/issues/97#issuecomment-258747858","issue_url":"https://api.github.com/repos/youngwind/blog/issues/97","id":258747858,"user":{"login":"ian4hu","id":7564574,"avatar_url":"https://avatars.githubusercontent.com/u/7564574?v=3","gravatar_id":"","url":"https://api.github.com/users/ian4hu","html_url":"https://github.com/ian4hu","followers_url":"https://api.github.com/users/ian4hu/followers","following_url":"https://api.github.com/users/ian4hu/following{/other_user}","gists_url":"https://api.github.com/users/ian4hu/gists{/gist_id}","starred_url":"https://api.github.com/users/ian4hu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ian4hu/subscriptions","organizations_url":"https://api.github.com/users/ian4hu/orgs","repos_url":"https://api.github.com/users/ian4hu/repos","events_url":"https://api.github.com/users/ian4hu/events{/privacy}","received_events_url":"https://api.github.com/users/ian4hu/received_events","type":"User","site_admin":false},"created_at":"2016-11-07T05:01:48Z","updated_at":"2016-11-07T05:01:48Z","body":"只是一个示例，不在于ES6语法"}}
{"actor":{"display_login":"ian4hu"}}