這篇繼續說js的現代復用模式:混入、借用方法和綁定。
混入
可以針對前面提到的通過屬性復制實現代碼復用的想法進行一個擴展,就是混入(mix-in)。混入並不是復制一個完整的對象,而是從多個對象中復制出任意的成員並將這些成員組合成一個新的對象。
混入的實現並不難,只需要遍歷每個參數,並且復制出傳遞給這個函數的每個對象中的每個屬性。
function mix(){ var arg,prop,child={}; for(arg=0;arg<arguments.length;arg++){ for(prop in arguments[arg]){ if(arguments[arg].hasOwnProperty(prop)){ child[prop] = arguments[arg][prop]; } } } return child; }
現在,有了一個通用的mix-in函數,可以向它傳遞任意數量的對象,其結果將獲得一個具有所有源對象屬性的新對象,一個調用的例子:
var cake = mix( {eggs : 2,large : true}, {butter : 1,sorted : true}, {flour : "3 cups"}, {suger : "sure!"} ); console.dir(cake);
下面是控制台的輸出:
butter 1 eggs 2 flour "3 cups" large true sorted true sugar "sure!"
借用方法
有時可能恰好僅需要現有對象其中的一個或兩個方法,在想要重用方法的同時,又不希望和源對象是父子的繼承關系,也就是只想使用所需要的方法,而不需要那些永遠用不到的其他方法。這種情況下,可以使用借用方法(borrowing method)來實現,即使用call()和apply(),區別就是傳參的區別。
下面是一個例子,借用了數組的方法:
function f(){ var args = [].slice.call(arguments,1,3); return args; } f(1,2,3,4,5,6);//[2,3]
其中創建空數組是為了使用數組的slice方法,也可以從Array的原型中借用方法,即Array.prototype.slice.call,這個需要輸更長的字符,但是可以節省創建一個空數組的工作。
借用方法,不是通過call()和apply()就是通過簡單的賦值,在借用方法的內部,this所指向的對象是基於調用表達式而確定的,但更多時候,最好可以鎖定this的值,或者把它綁定到特定對象並預先確定該對象。
參考下面的例子,one對象有一個say()的方法:
var one = { name : "object", say : function(greet){ return greet+","+this.name; } }; one.say("hi");//"hi,object"
另一個對象two中沒有say方法,但是可以從one那里借用:
var two = { name : "another object" }; one.say.apply(two,["hello"]);//"hello,another object"
上面借用的say()方法中的this指向了two,所以this.name是"another object".但是在什么場景中,應該給函數指針賦值一個全局變量,或者將函數作為回調函數傳遞?在程序中有這樣的應用,並且出現了問題。
var say = one.say; say("hoho");//"hoho,undefined" var yetanother = { name : "Yet another object", method : function(callback){ return callback("Hola"); } }; yetanother.method(one.say);//"Hola,undefined"
在上面兩種情況下this都指向了全局對象,並且代碼都沒有按預期運行。為了綁定對象與方法之間的關系,可以用下面的一個簡單的函數:
function bind(o,m){ return function(){ return m.apply(o,arguments); }; }
bind()接受了一個對象o和一個方法m,並將它們綁定起來,然后返回另一個函數。返回的函數可以通過閉包來訪問o和m。所以在bind()返回后仍然可以訪問o和m.可以使用bind()創建一個新函數:
var twosay = bind(two,one.say); twosay("yo");//"yo,another object"
無論怎么調用twosay(),這個方法總是綁定到對象two上。
ES5中的bind()
ES5將bind()添加到Function.prototype,使得bind()像call()apply()一樣易用。可以執行下面的表達式:
var newFunc = obj.someFunc.bind(myobj,1,2,3);
就是將someFunc()與myobj綁定到一起,並填充someFunc()的前3個參數。
在不支持ES5的環境下面運行的時候,看看怎么實現Function.prototype.bind():
if (typeof Function.prototype.bind === "undefined"){ Function.prototype.bind = function(thisArg){ var fn = this, slice = Array.prototype.slice, args = slice.call(arguments,1); return function(){ return fn.apply(thisArg,args.concat(slice.call(arguments))); }; }; }
它拼接了參數列表,即傳給bind()的參數(第一個除外),以及那些傳給由bind()返回新函數的參數,新函數將在后面調用。一個調用例子:
var twosay2 = one.say.bind(two); twosay2("Bonjour");//"Bonjour,another object"
也可以傳遞一個參數:
var twosay3 = one.say.bind(two,"Nihao"); twosay3();//"Nihao,another object"
小結
在javascript中可能並不會像C#或Java一樣經常面臨繼承的問題,一些原因是js庫用一些方法解決了這個問題,另一些原因是在js中很少需要建立長而且復雜的繼承鏈。在靜態強類型語言中,繼承可能是唯一復用代碼的方法,但在js中經常有更簡潔並且優雅的方法,包括借用方法,綁定,復制屬性,及從多個對象中混入屬性等方法。畢竟,代碼重用才是最終目的,繼承只是實現這個目標的方法之一。
--end--