第一次看到柯里化這個詞的時候,還是在看一篇算法相關的博客提到把函數柯里化,那時一看這個詞就感覺很高端,實際上當你瞭解了後才發現其實就是高階函數的一個特殊用法。
果然是不管作用怎麼樣都要有個高端的名字纔有用。
首先看看柯里化到底是什麼?
維基百科上說道:柯里化,英語:Currying(果然是滿滿的英譯中的既視感),是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。
看這個解釋有一點抽象,我們就拿被做了無數次示例的add函數,來做一個簡單的實現。
// 普通的add函數
function add(x, y) {
return x + y
}
// Currying後
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
實際上就是把add函數的x,y兩個參數變成了先用一個函數接收x然後返回一個函數去處理y參數。現在思路應該就比較清晰了,就是隻傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
但是問題來了費這麼大勁封裝一層,到底有什麼用處呢?沒有好處想讓我們程序員多幹事情是不可能滴,這輩子都不可能.
來列一列Currying有哪些好處呢?
1. 參數複用
// 正常正則驗證字符串 reg.test(txt)
// 函數封裝後
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true
// Currying後
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false
上面的示例是一個正則的校驗,正常來說直接調用check函數就可以了,但是如果我有很多地方都要校驗是否有數字,其實就是需要將第一個參數reg進行復用,這樣別的地方就能夠直接調用hasNumber,hasLetter等函數,讓參數能夠複用,調用起來也更方便。
- 提前確認
var on = function(element, event, handler) {
if (document.addEventListener) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
} else {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
}
}
var on = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
};
}
})();
//換一種寫法可能比較好理解一點,上面就是把isSupport這個參數給先確定下來了
var on = function(isSupport, element, event, handler) {
isSupport = isSupport || document.addEventListener;
if (isSupport) {
return element.addEventListener(event, handler, false);
} else {
return element.attachEvent('on' + event, handler);
}
}
我們在做項目的過程中,封裝一些dom操作可以說再常見不過,上面第一種寫法也是比較常見,但是我們看看第二種寫法,它相對一第一種寫法就是自執行然後返回一個新的函數,這樣其實就是提前確定了會走哪一個方法,避免每次都進行判斷。
- 延遲運行
Function.prototype.bind = function (context) {
var _this = this
var args = Array.prototype.slice.call(arguments, 1)
return function() {
return _this.apply(context, args)
}
}
像我們js中經常使用的bind,實現的機制就是Currying.
說了這幾點好處之後,發現還有個問題,難道每次使用Currying都要對底層函數去做修改,
有沒有什麼通用的封裝方法?
// 初步封裝
var currying = function(fn) {
// args 獲取第一個方法內的全部參數
var args = Array.prototype.slice.call(arguments, 1)
return function() {
// 將後面方法裏的全部參數和args進行合併
var newArgs = args.concat(Array.prototype.slice.call(arguments))
// 把合併後的參數通過apply作爲fn的參數並執行
return fn.apply(this, newArgs)
}
}
這邊首先是初步封裝,通過閉包把初步參數給保存下來,然後通過獲取剩下的arguments進行拼接,最後執行需要currying的函數。
但是好像還有些什麼缺陷,這樣返回的話其實只能多擴展一個參數,currying(a)(b)(c)這樣的話,貌似就不支持了(不支持多參數調用),一般這種情況都會想到使用遞歸再進行封裝一層。
// 支持多參數傳遞
function progressCurrying(fn, args) {
var _this = this
var len = fn.length;
var args = args || [];
return function() {
var _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(args, _args);
// 如果參數個數小於最初的fn.length,則遞歸調用,繼續收集參數
if (_args.length < len) {
return progressCurrying.call(_this, fn, _args);
}
// 參數收集完畢,則執行fn
return fn.apply(this, _args);
}
}
這邊其實是在初步的基礎上,加上了遞歸的調用,只要參數個數小於最初的fn.length,就會繼續執行遞歸。
好處說完了,通用方法也有了,讓我們來關注下curry的性能
curry的一些性能問題你只要知道下面四點就差不多了:
存取arguments對象通常要比存取命名參數要慢一點
一些老版本的瀏覽器在arguments.length的實現上是相當慢的
使用fn.apply( … ) 和 fn.call( … )通常比直接調用fn( … ) 稍微慢點
創建大量嵌套作用域和閉包函數會帶來花銷,無論是在內存還是速度上
其實在大部分應用中,主要的性能瓶頸是在操作DOM節點上,這js的性能損耗基本是可以忽略不計的,所以curry是可以直接放心的使用。
最後再擴展一道經典面試題
// 實現一個add方法,使計算結果能夠滿足如下預期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
// 第一次執行時,定義一個數組專門用來存儲所有的參數
var _args = Array.prototype.slice.call(arguments);
// 在內部聲明一個函數,利用閉包的特性保存_args並收集所有的參數值
var _adder = function() {
_args.push(...arguments);
return _adder;
};
// 利用toString隱式轉換的特性,當最後執行時隱式轉換,並計算最終的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9