Mixin是JavaScript中用的最普遍的模式,幾乎所有流行類庫都會有Mixin的實現。
Mixin是摻合,混合,糅合的意思,即可以就任意一個對象的全部或部分屬性拷貝到另一個對象上。
從提供的接口來看,有的是對對象的操作,有的是對類的操作。對類的操作又稱為摻元類(Mixin classes)
一、摻合對象 (Mixin object)
先看最簡單的mixin實現
function mixin(dest, src) { for (var key in src) { dest[key] = src[key] } }
使用下
var person = {name: 'John', age: 29} var obj = {} mixin(obj, person) console.log(obj) // {name: 'John', age: 29}
可看到,已經將person的所有屬性拷貝到obj上了。 有個缺點,如果obj上已經有了name: 'Jack',那么會被person覆蓋。因此需要加個判斷,如果dest上已經存在了,就不拷貝。
function mixin(dest, src) { for (var key in src) { if (!dest[key]) { dest[key] = src[key] } } } var person = {name: 'John', age: 29} var obj = {name: 'Jack'} mixin(obj, person) console.log(obj) // Object { name="Jack", age=29}
當然,你可以提供更強大,靈活的Mixin,比如可以將任意多個對象摻合到目標對象
function mixin(dest /*, Any number of objects */) { var sources = Array.prototype.slice.call(arguments, 1) for (var i=0; i<sources.length; i++) { var src = sources[i] for (var key in src) { if (!dest[key]) { dest[key] = src[key] } } } } var person = {name: 'John', age: 29, toString: function(){return 'aa'}} var permission = {open: 1} var obj = {name: 'Jack'} mixin(obj, person, permission) console.log(obj) // Object { name="Jack", age=29, open=1}
以下類庫都是對對象的摻合
- jQuery的$.extend 操作對象,將其它對象的屬性方法拷貝到目標對象。
- RequireJS的私有的mixin 操作對象,將其它對象的屬性方法拷貝到目標對象。
- ExtJS的Ext.apply 也是操作對象,它還提供了一個defaults參數。
- Underscore.js 的 _.extend,把第二個參數起的所有對象都拷貝到第一個參數
二、摻和類(Mixin Classes)
有的翻譯過來叫做摻元類,它是一種不需要用到嚴格的繼承就可以復用代碼的一種技術。如果多個類想用到某個類的某個方法,可以通過擴充這些類的原型已達到共享該方法。比如先創建一個包含各種通用方法的類,然后讓其它類擴充於它。這個包含通用方法的類就叫摻元類。多數時候它不會直接實例化或調用,而是作為其它類的模板用於擴充。
先看最簡單的實現
// 工具方法,實現mixin function augment(destClass, srcClass) { var destProto = destClass.prototype var srcProto = srcClass.prototype for (var method in srcProto) { if (!destProto[method]) { destProto[method] = srcProto[method] } } } function Person() {} // 具有兩個方法的類,用於mixin Person.prototype.getName = function() {} Person.prototype.getAge = function() {} function Student() {} // 沒有任何方法的類 augment(Student, Person) // 調用,拷貝 var s1 = new Student() console.log(s1) // Student { getName=function(), getAge=function()}
工具函數augment接受兩個參數,都是函數類型(類),第一個類會從第二個類的原型上繼承其方法。即使用Person類擴充了Student類。
我們知道,某些語言如C++/Python允許子類繼承多個父類,但在JavaScript中是不允許的,因為一個構造器只有一個原型對象,不過這可以通過多個摻元類的方式實現擴充,這實際是一種變相多繼承的實現。和mixin方法類似,修改下augment方法。
function augment(destClass, /*, Any number of classes */) { var classes = Array.prototype.slice.call(arguments, 1) for (var i=0; i<classes.length; i++) { var srcClass = classes[i] var srcProto = srcClass.prototype var destProto = destClass.prototype for (var method in srcProto) { if (!destProto[method]) { destProto[method] = srcProto[method] } } } }
這樣就實現了多繼承。
有時不想繼承所有的方法,指向拷貝指定的方法,增加一個參數methods
function augment(destClass, srcClass, methods) { var srcProto = srcClass.prototype var destProto = destClass.prototype for (var i=0; i<methods.length; i++) { var method = methods[i] if (!destProto[method]) { destProto[method] = srcProto[method] } } } function Person() {} Person.prototype.getName = function() {} Person.prototype.setName = function() {} Person.prototype.getAge = function() {} Person.prototype.setAge = function() {} function Student() {} augment(Student, Person, ['getName', 'setName']) var s1 = new Student() console.log(s1) // Student { getName=function(), setName=function()}
Backbone是廣泛使用摻元類的庫
首先,Backbone庫自身就采用Mixin classes方式組織,如Backbone.Events是最底層的摻元類,它的方法(on/off/trigger...)都被Backbone.Model/Backbone.Collection/Backbone.View等繼承。代碼片段如下
_.extend(Model.prototype, Events, { ... }) _.extend(Collection.prototype, Events, { ... }) _.extend(View.prototype, Events, { ... })
它這里使用_.extend來擴充Model,Collection,View的原型,把Events的方法都拷貝到原型。即Event就是一個摻元類(雖然被實現為一個對象)
其次,我們使用Backbone開發時,你寫的模型會用Backbone.Model去擴充,視圖會用Backbone.View去擴充。如
var MyModel = Backbone.Model.extend({ instanceProp: xx },{ classProp: yy }) var MyView = Backbone.Model.extend({ instanceProp: xx },{ classProp: yy })
這時,Backbone.Model/Backbone.View等就是摻元類了。當然,你還可以把underscore當做摻元對象,因為Backbone的很多類都繼承了_.extend方法,如Backbone.Events/Backbone.Model等。