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