有以下需求:
創建一個compose函數,返回函數集 functions 組合后的復合函數, 也就是一個函數執行完之后把返回的結果再作為參數賦給下一個函數來執行. 以此類推. 在數學里, 把函數 f(), g(), 和 h() 組合起來可以得到復合函數 f(g(h()))。
如果只是為了完成這道題,可用以下做法
var greet = function(name){
return 'hi:'+name
}
var exclaim = function(statement){
return statement.toUpperCase()+'!'
}
var compose = function(greet,exclaim){
return function(name){
console.log(exclaim(greet(name)).replace(/(\w+:)/,function($1){
return $1.toLowerCase()
}))
}
}
var welcome=compose(greet,exclaim)
welcome('dot')
//'hi: DOT!'
但上面的代碼沒有擴展性,如果嵌套的函數更多,該怎么解決呢?
我們可以定義兩個方法,分別是compose()和pipe()(上面的題目我們用compose就可以實現,pipe是另行擴展的,不要有疑問),這兩個方法接收的參數都是N個函數,返回的值都是一個函數。
- compose內的函數執行順序為從右向左,即最右邊的函數(最后一個參數)最先執行,執行完的結果作為參數傳遞給前一個函數(包裹它的函數),一直到整個函數執行完畢,return一個函數,所以compose內部實現的原理類似多米諾骨牌,層層遞進的。
- pipe函數與compose函數十分相近,也是一個函數執行完畢后將結果作為參數傳遞給另一個函數,但它們的區別僅在于pipe函數的接收的函數參數,是從左向右執行的,即第一個參數(函數)執行完畢,將結果吐出來作為參數傳遞給第二個函數,也就是pipe的第二個參數,直到pipe所有參數作為函數都執行完畢,return出一個函數,才算執行完成。
compose和pipe的優點在于,哪怕再要增加或者刪除一個參數(執行函數),只需增加或刪除相應的參數和定義的函數即可,維護和擴展都十分方便。
代碼實現如下:
function compose() {
var fns = [].slice.call(arguments)
return function (initialArg) {
var res = initialArg
for (var i = fns.length - 1; i > -1; i--) {
res = fns[i](res)
}
return res
}
}
function pipe() {
var fns = [].slice.call(arguments)
return function (initialAgr) {
var res = initialAgr
for (var i = 0; i < fns.length; i++) {
res = fns[i](res)
}
return res
}
}
var greet = function (name) { return 'hi:' + name }
var exclaim = function (statement) { return statement.toUpperCase() + '!' }
var transform = function (str) { return str.replace(/[dD]/, 'DDDDD') }
var welcome1 = compose(greet, exclaim, transform)
var welcome2 = pipe(greet, exclaim, transform)
console.log(welcome1('dot'))//hi:DDDDDOT!
console.log(welcome2('dolb'))//HI:DDDDDOLB!
根據前面說過的原理,分析一下以上代碼:
出題人的意圖是“把函數 f(), g(), 和 h() 組合起來得到復合函數 f(g(h()))”,我們首先就可以想到遞歸,函數從右至左依次執行,每執行完一個函數將其結果作為參數傳遞給它左邊的函數(即包裹它的函數),我們可以很清楚地知道,compose接收的是N個函數參數,而其中每個參數作為函數在執行時接收的都是一個參數(前一個被執行的函數的結果)。
只看代碼就可以發現,調用compose函數,將得到的結果賦值給welcome1變量,但這時我們并沒有直接把welcome1打印出來,而是向welcome1里傳入了參數,這就很像函數調用的格式有木有,那么我們可以做一個設想,其實compose返回的就是一個匿名函數,我們可以通過傳遞參數給這個匿名函數來得到某種結果,現在思路就很清晰了。
首先在頁面上定義一個compose函數,但是不傳遞任何參數,因為參數的數量(即執行函數的個數)是不確定的,在compose函數內部return一個匿名函數,這個匿名函數接收一個形參initialAgr,函數執行過程中這個參數就是傳遞給welcome1的實參,也就是第一個被執行的函數接收的參數。
到這里完成了以下代碼:
function compose() {
return function (initialArg) {
}
}
接下來干什么呢?
看到f(g(h()))這種形式的函數嵌套,我首先想到的就是遞歸,所以我們首先要取得調用compose函數時傳遞的參數,這個參數的形式如下:
arguments = {
0: fn1,
1: fn2,
2: fn3,
length: 3
}
為了實現遞歸,我們需要把這個形式的參數轉換為數組,借用數組的slice方法可以實現這一點:
var fns = [].slice.call(arguments)
以上代碼中,我們將傳遞給compose函數的參數轉化為了一個數組并賦值給了fns,接下來我們用一個變量res將傳遞給welcome1的參數保存起來,定義一個for循環從右向左遍歷fns中的每一項并執行它,在這段代碼中函數的執行順序是transform=>exclaim=>greet
,其中,將res傳遞給第一個被執行的函數,并將值賦值給res,相當于每執行完一個函數,res的值就被重寫一次。
for循環結束意味著復合函數已經執行完畢,我們要的無非就是res的值,所以在函數內部return res就可以達到我們預期的效果。
綜上所述,compose函數定義如下:
function compose() {
var fns = [].slice.call(arguments)
return function (initialArg) {
var res = initialArg
for (var i = fns.length - 1; i > -1; i--) {
res = fns[i](res)
}
return res
}
}
這就是compose大致的使用,總結下來要注意的有以下幾點:
- compose的參數是函數,返回的也是一個函數。
- 因為除了第一個函數的接受參數,其他函數的接受參數都是上一個函數的返回值,所以初始函數的參數是多元的(本題只討論了一元參數的情況),而其他函數的接受值是一元的。
- compsoe函數可以接受任意的參數,所有的參數都是函數,且執行方向是自右向左的,初始函數一定放到參數的最右面。
pipe函數沒有什么好說的,與compose的原理相同,只不過函數的執行順序是從左至右,體現在以上代碼中就是greet=>exclaim=>transform
可以看出,compose和pipe函數對相同執行函數的執行順序不同,得到的結果也不同。