從一道面試題認識函式柯里化

惆悵客發表於2018-08-27

最近在整理面試資源的時候,發現一道有意思的題目,所以就記錄下來。

題目

如何實現 multi(2)(3)(4)=24?

首先來分析下這道題,實現一個 multi 函式並依次傳入引數執行,得到最終的結果。通過題目很容易得到的結論是,把傳入的引數相乘就能夠得到需要的結果,也就是 2X3X4 = 24。

簡單的實現

那麼如何實現 multi 函式去計算出結果值呢?腦海中首先浮現的解決方案是,閉包。

function multi(a) {
    return function(b) {
        return function(c) {
            return a * b * c;
        }
    }
}
複製程式碼

利用閉包的原則,multi 函式執行的時候,返回 multi 函式中的內部函式,再次執行的時候其實執行的是這個內部函式,這個內部函式中接著又巢狀了一個內部函式,用於計算最終結果並返回。

閉包實現

單純從題面來說,似乎是已經實現了想要的結果,但仔細一想就會發現存在問題。

上面的實現方案存在的缺陷:

  • 程式碼不夠優雅,實現步驟需要一層一層的巢狀函式。
  • 可擴充套件性差,假如是要實現 multi(2)(3)(4)...(n) 這樣的功能,那就得巢狀 n 層函式。

那麼有沒有更好的解決方案,答案是,使用函數語言程式設計中的函式柯里化實現。

函式柯里化

在函數語言程式設計中,函式是一等公民。那麼函式柯里化是怎樣的呢?

函式柯里化指的是將能夠接收多個引數的函式轉化為接收單一引數的函式,並且返回接收餘下引數且返回結果的新函式的技術。

函式柯里化的主要作用和特點就是引數複用、提前返回和延遲執行。

例如:封裝相容現代瀏覽器和 IE 瀏覽器的事件監聽的方法,正常情況下封裝是這樣的。

var addEvent = function(el, type, fn, capture) {
    if(window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    }else {
        el.attachEvent('on' + type, function(e) {
            fn.call(el, e);
        })
    }
}
複製程式碼

該封裝的方法存在的不足是,每次寫監聽事件的時候呼叫 addEvent 函式,都會進行 if else 的相容性判斷。事實上在程式碼中只需要執行一次相容性判斷就可以了,後續的事件監聽就不需要再去判斷相容性了。那麼怎麼用函式柯里化優化這個封裝函式。

var addEvent = (function() {
    if(window.addEventListener) {
        return function(el, type, fn, capture) {
            el.addEventListener(type, function(e) {
                fn.call(el, e);
            }, capture);
        }
    }else {
        return function(ele, type, fn) {
            el.attachEvent('on' + type, function(e) {
                fn.call(el, e);
            })
        }
    }
})()
複製程式碼

js 引擎在執行該段程式碼的時候就會進行相容性判斷,並且返回需要使用的事件監聽封裝函式。這裡使用了函式柯里化的兩個特點:提前返回和延遲執行。

柯里化另一個典型的應用場景就是 bind 函式的實現。使用了函式柯里化的兩個特點:引數複用和提前返回。

Function.prototype.bind = function(){
	var fn = this;
	var args = Array.prototye.slice.call(arguments);
	var context = args.shift();

	return function(){
		return fn.apply(context, args.concat(Array.prototype.slice.call(arguments)));
	};
};
複製程式碼

柯里化的實現

那麼如何通過函式柯里化實現面試題的功能呢?

通用版

function curry(fn) {
    var args = Array.prototype.slice.call(arguments, 1);
	return function() {
		var newArgs = args.concat(Array.prototype.slice.call(arguments));
        return fn.apply(this, newArgs);
    }
}
複製程式碼

curry 函式的第一個引數是要動態建立柯里化的函式,餘下的引數儲存在 args 變數中。

執行 curry 函式返回的函式接收新的引數與 args 變數儲存的引數合併,並把合併的引數傳入給柯里化了的函式。

function multiFn(a, b, c) {
    return a * b * c;
}
var multi = curry(multiFn);
multi(2,3,4);
複製程式碼

結果:

image

雖然得到的結果是一樣的,但是很容易發現存在問題,就是程式碼相對於之前的閉包實現方式較複雜,而且執行方式也不是題目要求的那樣 multi(2)(3)(4)。那麼下面就來改進這版程式碼。

改進版

就題目而言,是需要執行三次函式呼叫,那麼針對柯里化後的函式,如果傳入的引數沒有 3 個的話,就繼續執行 curry 函式接收引數,如果引數達到 3 個,就執行柯里化了的函式。

function curry(fn, args) {
    var length = fn.length;
    var args = args || [];
    return function(){
        newArgs = args.concat(Array.prototype.slice.call(arguments));
        if(newArgs.length < length){
            return curry.call(this,fn,newArgs);
        }else{
            return fn.apply(this,newArgs);
        }
    }
}
function multiFn(a, b, c) {
    return a * b * c;
}
var multi = curry(multiFn);
multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4);
複製程式碼

image

可以看到,通過改進版的柯里化函式,已經將題目定的實現方式擴充套件到好幾種了。這種實現方案的程式碼擴充套件性就比較強了,但是還是有點不足,就是必須事先知道求值的引數個數,那能不能讓程式碼更靈活點,達到隨意傳參的效果,例如: multi(2)(3)(4),multi(5)(6)(7)(8)(9) 這樣的。

優化版

function multi() {
    var args = Array.prototype.slice.call(arguments);
	var fn = function() {
		var newArgs = args.concat(Array.prototype.slice.call(arguments));
        return multi.apply(this, newArgs);
    }
    fn.toString = function() {
        return args.reduce(function(a, b) {
            return a * b;
        })
    }
    return fn;
}
複製程式碼

image

這樣的解決方案就可以靈活的使用了。不足的是返回值是 Function 型別。

image

總結

  • 就題目本身而言,是存在多種實現方式的,只要理解並充分利用閉包的強大。
  • 可能在實際應用場景中,很少使用函式柯里化的解決方案,但是瞭解認識函式柯里化對自身的提升還是有幫助的。
  • 理解閉包和函式柯里化之後,如果在面試中遇到類似的題型,應該就可以迎刃而解了。

後記

本著學習和總結的態度寫的技術輸出,文中有任何錯誤和問題,請大家指出。更多的技術輸出可以檢視我的 github部落格

整理了一些前端的學習資源,希望能夠幫助到有需要的人,地址: 學習資源彙總

參考

相關文章