javascript
JavaScript 专题之函数柯里化
JavaScript 專題系列第十三篇,講解函數(shù)柯里化以及如何實現(xiàn)一個 curry 函數(shù)
定義
維基百科中對柯里化 (Currying) 的定義為:
In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.
翻譯成中文:
在數(shù)學(xué)和計算機科學(xué)中,柯里化是一種將使用多個參數(shù)的一個函數(shù)轉(zhuǎn)換成一系列使用一個參數(shù)的函數(shù)的技術(shù)。
舉個例子:
function add(a, b) {return a + b; }// 執(zhí)行 add 函數(shù),一次傳入兩個參數(shù)即可 add(1, 2) // 3// 假設(shè)有一個 curry 函數(shù)可以做到柯里化 var addCurry = curry(add); addCurry(1)(2) // 3復(fù)制代碼用途
我們會講到如何寫出這個 curry 函數(shù),并且會將這個 curry 函數(shù)寫的很強大,但是在編寫之前,我們需要知道柯里化到底有什么用?
舉個例子:
// 示意而已 function ajax(type, url, data) {var xhr = new XMLHttpRequest();xhr.open(type, url, true);xhr.send(data); }// 雖然 ajax 這個函數(shù)非常通用,但在重復(fù)調(diào)用的時候參數(shù)冗余 ajax('POST', 'www.test.com', "name=kevin") ajax('POST', 'www.test2.com', "name=kevin") ajax('POST', 'www.test3.com', "name=kevin")// 利用 curry var ajaxCurry = curry(ajax);// 以 POST 類型請求數(shù)據(jù) var post = ajaxCurry('POST'); post('www.test.com', "name=kevin");// 以 POST 類型請求來自于 www.test.com 的數(shù)據(jù) var postFromTest = post('www.test.com'); postFromTest("name=kevin");復(fù)制代碼想想 jQuery 雖然有 $.ajax 這樣通用的方法,但是也有 $.get 和 $.post 的語法糖。(當(dāng)然 jQuery 底層是否是這樣做的,我就沒有研究了)。
curry 的這種用途可以理解為:參數(shù)復(fù)用。本質(zhì)上是降低通用性,提高適用性。
可是即便如此,是不是依然感覺沒什么用呢?
如果我們僅僅是把參數(shù)一個一個傳進(jìn)去,意義可能不大,但是如果我們是把柯里化后的函數(shù)傳給其他函數(shù)比如 map 呢?
舉個例子:
比如我們有這樣一段數(shù)據(jù):
var person = [{name: 'kevin'}, {name: 'daisy'}]復(fù)制代碼如果我們要獲取所有的 name 值,我們可以這樣做:
var name = person.map(function (item) {return item.name; })復(fù)制代碼不過如果我們有 curry 函數(shù):
var prop = curry(function (key, obj) {return obj[key] });var name = person.map(prop('name'))復(fù)制代碼我們?yōu)榱双@取 name 屬性還要再編寫一個 prop 函數(shù),是不是又麻煩了些?
但是要注意,prop 函數(shù)編寫一次后,以后可以多次使用,實際上代碼從原本的三行精簡成了一行,而且你看代碼是不是更加易懂了?
person.map(prop('name')) 就好像直白的告訴你:person 對象遍歷(map)獲取(prop) name 屬性。
是不是感覺有點意思了呢?
第一版
未來我們會接觸到更多有關(guān)柯里化的應(yīng)用,不過那是未來的事情了,現(xiàn)在我們該編寫這個 curry 函數(shù)了。
一個經(jīng)常會看到的 curry 函數(shù)的實現(xiàn)為:
// 第一版 var curry = function (fn) {var args = [].slice.call(arguments, 1);return function() {var newArgs = args.concat([].slice.call(arguments));return fn.apply(this, newArgs);}; };復(fù)制代碼我們可以這樣使用:
function add(a, b) {return a + b; }var addCurry = curry(add, 1, 2); addCurry() // 3 //或者 var addCurry = curry(add, 1); addCurry(2) // 3 //或者 var addCurry = curry(add); addCurry(1, 2) // 3復(fù)制代碼已經(jīng)有柯里化的感覺了,但是還沒有達(dá)到要求,不過我們可以把這個函數(shù)用作輔助函數(shù),幫助我們寫真正的 curry 函數(shù)。
第二版
// 第二版 function sub_curry(fn) {var args = [].slice.call(arguments, 1);return function() {return fn.apply(this, args.concat([].slice.call(arguments)));}; }function curry(fn, length) {length = length || fn.length;var slice = Array.prototype.slice;return function() {if (arguments.length < length) {var combined = [fn].concat(slice.call(arguments));return curry(sub_curry.apply(this, combined), length - arguments.length);} else {return fn.apply(this, arguments);}}; }復(fù)制代碼我們驗證下這個函數(shù):
var fn = curry(function(a, b, c) {return [a, b, c]; });fn("a", "b", "c") // ["a", "b", "c"] fn("a", "b")("c") // ["a", "b", "c"] fn("a")("b")("c") // ["a", "b", "c"] fn("a")("b", "c") // ["a", "b", "c"]復(fù)制代碼效果已經(jīng)達(dá)到我們的預(yù)期,然而這個 curry 函數(shù)的實現(xiàn)好難理解吶……
為了讓大家更好的理解這個 curry 函數(shù),我給大家寫個極簡版的代碼:
function sub_curry(fn){return function(){return fn()} }function curry(fn, length){length = length || 4;return function(){if (length > 1) {return curry(sub_curry(fn), --length)}else {return fn()}} }var fn0 = function(){console.log(1) }var fn1 = curry(fn0)fn1()()()() // 1復(fù)制代碼大家先從理解這個 curry 函數(shù)開始。
當(dāng)執(zhí)行 fn1() 時,函數(shù)返回:
curry(sub_curry(fn0)) // 相當(dāng)于 curry(function(){return fn0() })復(fù)制代碼當(dāng)執(zhí)行 fn1()() 時,函數(shù)返回:
curry(sub_curry(function(){return fn0() })) // 相當(dāng)于 curry(function(){return (function(){return fn0()})() }) // 相當(dāng)于 curry(function(){return fn0() })復(fù)制代碼當(dāng)執(zhí)行 fn1()()() 時,函數(shù)返回:
// 跟 fn1()() 的分析過程一樣 curry(function(){return fn0() })復(fù)制代碼當(dāng)執(zhí)行 fn1()()()() 時,因為此時 length > 2 為 false,所以執(zhí)行 fn():
fn() // 相當(dāng)于 (function(){return fn0() })() // 相當(dāng)于 fn0() // 執(zhí)行 fn0 函數(shù),打印 1復(fù)制代碼再回到真正的 curry 函數(shù),我們以下面的例子為例:
var fn0 = function(a, b, c, d) {return [a, b, c, d]; }var fn1 = curry(fn0);fn1("a", "b")("c")("d")復(fù)制代碼當(dāng)執(zhí)行 fn1("a", "b") 時:
fn1("a", "b") // 相當(dāng)于 curry(fn0)("a", "b") // 相當(dāng)于 curry(sub_curry(fn0, "a", "b")) // 相當(dāng)于 // 注意 ... 只是一個示意,表示該函數(shù)執(zhí)行時傳入的參數(shù)會作為 fn0 后面的參數(shù)傳入 curry(function(...){return fn0("a", "b", ...) })復(fù)制代碼當(dāng)執(zhí)行 fn1("a", "b")("c") 時,函數(shù)返回:
curry(sub_curry(function(...){return fn0("a", "b", ...) }), "c") // 相當(dāng)于 curry(function(...){return (function(...) {return fn0("a", "b", ...)})("c") }) // 相當(dāng)于 curry(function(...){return fn0("a", "b", "c", ...) })復(fù)制代碼當(dāng)執(zhí)行 fn1("a", "b")("c")("d") 時,此時 arguments.length < length 為 false ,執(zhí)行 fn(arguments),相當(dāng)于:
(function(...){return fn0("a", "b", "c", ...) })("d") // 相當(dāng)于 fn0("a", "b", "c", "d")復(fù)制代碼函數(shù)執(zhí)行結(jié)束。
所以,其實整段代碼又很好理解:
sub_curry 的作用就是用函數(shù)包裹原函數(shù),然后給原函數(shù)傳入之前的參數(shù),當(dāng)執(zhí)行 fn0(...)(...) 的時候,執(zhí)行包裹函數(shù),返回原函數(shù),然后再調(diào)用 sub_curry 再包裹原函數(shù),然后將新的參數(shù)混合舊的參數(shù)再傳入原函數(shù),直到函數(shù)參數(shù)的數(shù)目達(dá)到要求為止。
如果要明白 curry 函數(shù)的運行原理,大家還是要動手寫一遍,嘗試著分析執(zhí)行步驟。
更易懂的實現(xiàn)
當(dāng)然了,如果你覺得還是無法理解,你可以選擇下面這種實現(xiàn)方式,可以實現(xiàn)同樣的效果:
function curry(fn, args) {length = fn.length;args = args || [];return function() {var _args = args.slice(0),arg, i;for (i = 0; i < arguments.length; i++) {arg = arguments[i];_args.push(arg);}if (_args.length < length) {return curry.call(this, fn, _args);}else {return fn.apply(this, _args);}} }var fn = curry(function(a, b, c) {console.log([a, b, c]); });fn("a", "b", "c") // ["a", "b", "c"] fn("a", "b")("c") // ["a", "b", "c"] fn("a")("b")("c") // ["a", "b", "c"] fn("a")("b", "c") // ["a", "b", "c"]復(fù)制代碼或許大家覺得這種方式更好理解,又能實現(xiàn)一樣的效果,為什么不直接就講這種呢?
因為想給大家介紹各種實現(xiàn)的方法嘛,不能因為難以理解就不給大家介紹吶~
第三版
curry 函數(shù)寫到這里其實已經(jīng)很完善了,但是注意這個函數(shù)的傳參順序必須是從左到右,根據(jù)形參的順序依次傳入,如果我不想根據(jù)這個順序傳呢?
我們可以創(chuàng)建一個占位符,比如這樣:
var fn = curry(function(a, b, c) {console.log([a, b, c]); });fn("a", _, "c")("b") // ["a", "b", "c"]復(fù)制代碼我們直接看第三版的代碼:
// 第三版 function curry(fn, args, holes) {length = fn.length;args = args || [];holes = holes || [];return function() {var _args = args.slice(0),_holes = holes.slice(0),argsLen = args.length,holesLen = holes.length,arg, i, index = 0;for (i = 0; i < arguments.length; i++) {arg = arguments[i];// 處理類似 fn(1, _, _, 4)(_, 3) 這種情況,index 需要指向 holes 正確的下標(biāo)if (arg === _ && holesLen) {index++if (index > holesLen) {_args.push(arg);_holes.push(argsLen - 1 + index - holesLen)}}// 處理類似 fn(1)(_) 這種情況else if (arg === _) {_args.push(arg);_holes.push(argsLen + i);}// 處理類似 fn(_, 2)(1) 這種情況else if (holesLen) {// fn(_, 2)(_, 3)if (index >= holesLen) {_args.push(arg);}// fn(_, 2)(1) 用參數(shù) 1 替換占位符else {_args.splice(_holes[index], 1, arg);_holes.splice(index, 1)}}else {_args.push(arg);}}if (_holes.length || _args.length < length) {return curry.call(this, fn, _args, _holes);}else {return fn.apply(this, _args);}} }var _ = {};var fn = curry(function(a, b, c, d, e) {console.log([a, b, c, d, e]); });// 驗證 輸出全部都是 [1, 2, 3, 4, 5] fn(1, 2, 3, 4, 5); fn(_, 2, 3, 4, 5)(1); fn(1, _, 3, 4, 5)(2); fn(1, _, 3)(_, 4)(2)(5); fn(1, _, _, 4)(_, 3)(2)(5); fn(_, 2)(_, _, 4)(1)(3)(5)復(fù)制代碼寫在最后
至此,我們已經(jīng)實現(xiàn)了一個強大的 curry 函數(shù),可是這個 curry 函數(shù)符合柯里化的定義嗎?柯里化可是將一個多參數(shù)的函數(shù)轉(zhuǎn)換成多個單參數(shù)的函數(shù),但是現(xiàn)在我們不僅可以傳入一個參數(shù),還可以一次傳入兩個參數(shù),甚至更多參數(shù)……這看起來更像一個柯里化 (curry) 和偏函數(shù) (partial application) 的綜合應(yīng)用,可是什么又是偏函數(shù)呢?下篇文章會講到。
專題系列
JavaScript專題系列目錄地址:github.com/mqyqingfeng…。
JavaScript專題系列預(yù)計寫二十篇左右,主要研究日常開發(fā)中一些功能點的實現(xiàn),比如防抖、節(jié)流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特點是研(chao)究(xi) underscore 和 jQuery 的實現(xiàn)方式。
如果有錯誤或者不嚴(yán)謹(jǐn)?shù)牡胤?#xff0c;請務(wù)必給予指正,十分感謝。如果喜歡或者有所啟發(fā),歡迎 star,對作者也是一種鼓勵。
總結(jié)
以上是生活随笔為你收集整理的JavaScript 专题之函数柯里化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 二进制-高效位运算
- 下一篇: JavaScript中错误正确处理方式,