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

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

題目

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

首先來分析下這道題,實現一個 multi 函數並依次傳入參數執行,獲得最終的結果。經過題目很容易獲得的結論是,把傳入的參數相乘就可以獲得須要的結果,也就是 2X3X4 = 24。java

簡單的實現

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

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

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

閉包實現

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

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

  • 代碼不夠優雅,實現步驟須要一層一層的嵌套函數。
  • 可擴展性差,假如是要實現 multi(2)(3)(4)...(n) 這樣的功能,那就得嵌套 n 層函數。

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

函數柯里化

在函數式編程中,函數是一等公民。那麼函數柯里化是怎樣的呢?segmentfault

函數柯里化指的是將可以接收多個參數的函數轉化爲接收單一參數的函數,而且返回接收餘下參數且返回結果的新函數的技術。瀏覽器

函數柯里化的主要做用和特色就是參數複用、提早返回和延遲執行。

例如:封裝兼容現代瀏覽器和 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博客

整理了一些前端的學習資源,但願可以幫助到有須要的人,地址: 學習資源彙總

參考

相關文章
相關標籤/搜索