摘要:一個經常會看到的函數的實現為第一版我們可以這樣使用或者或者已經有柯里化的感覺了,但是還沒有達到要求,不過我們可以把這個函數用作輔助函數,幫助我們寫真正的函數。
定義JavaScript 專題系列第十三篇,講解函數柯里化以及如何實現一個 curry 函數
維基百科中對柯里化 (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.
翻譯成中文:
在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。
舉個例子:
function add(a, b) { return a + b; } // 執行 add 函數,一次傳入兩個參數即可 add(1, 2) // 3 // 假設有一個 curry 函數可以做到柯里化 var addCurry = curry(add); addCurry(1)(2) // 3用途
我們會講到如何寫出這個 curry 函數,并且會將這個 curry 函數寫的很強大,但是在編寫之前,我們需要知道柯里化到底有什么用?
舉個例子:
// 示意而已 function ajax(type, url, data) { var xhr = new XMLHttpRequest(); xhr.open(type, url, true); xhr.send(data); } // 雖然 ajax 這個函數非常通用,但在重復調用的時候參數冗余 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 類型請求數據 var post = ajaxCurry("POST"); post("www.test.com", "name=kevin"); // 以 POST 類型請求來自于 www.test.com 的數據 var postFromTest = post("www.test.com"); postFromTest("name=kevin");
想想 jQuery 雖然有 $.ajax 這樣通用的方法,但是也有 $.get 和 $.post 的語法糖。(當然 jQuery 底層是否是這樣做的,我就沒有研究了)。
curry 的這種用途可以理解為:參數復用。本質上是降低通用性,提高適用性。
可是即便如此,是不是依然感覺沒什么用呢?
如果我們僅僅是把參數一個一個傳進去,意義可能不大,但是如果我們是把柯里化后的函數傳給其他函數比如 map 呢?
舉個例子:
比如我們有這樣一段數據:
var person = [{name: "kevin"}, {name: "daisy"}]
如果我們要獲取所有的 name 值,我們可以這樣做:
var name = person.map(function (item) { return item.name; })
不過如果我們有 curry 函數:
var prop = curry(function (key, obj) { return obj[key] }); var name = person.map(prop("name"))
我們為了獲取 name 屬性還要再編寫一個 prop 函數,是不是又麻煩了些?
但是要注意,prop 函數編寫一次后,以后可以多次使用,實際上代碼從原本的三行精簡成了一行,而且你看代碼是不是更加易懂了?
person.map(prop("name")) 就好像直白的告訴你:person 對象遍歷(map)獲取(prop) name 屬性。
是不是感覺有點意思了呢?
第一版未來我們會接觸到更多有關柯里化的應用,不過那是未來的事情了,現在我們該編寫這個 curry 函數了。
一個經常會看到的 curry 函數的實現為:
// 第一版 var curry = function (fn) { var args = [].slice.call(arguments, 1); return function() { var newArgs = args.concat([].slice.call(arguments)); return fn.apply(this, newArgs); }; };
我們可以這樣使用:
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
已經有柯里化的感覺了,但是還沒有達到要求,不過我們可以把這個函數用作輔助函數,幫助我們寫真正的 curry 函數。
第二版// 第二版 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); } }; }
我們驗證下這個函數:
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"]
效果已經達到我們的預期,然而這個 curry 函數的實現好難理解吶……
為了讓大家更好的理解這個 curry 函數,我給大家寫個極簡版的代碼:
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
大家先從理解這個 curry 函數開始。
當執行 fn1() 時,函數返回:
curry(sub_curry(fn0)) // 相當于 curry(function(){ return fn0() })
當執行 fn1()() 時,函數返回:
curry(sub_curry(function(){ return fn0() })) // 相當于 curry(function(){ return (function(){ return fn0() })() }) // 相當于 curry(function(){ return fn0() })
當執行 fn1()()() 時,函數返回:
// 跟 fn1()() 的分析過程一樣 curry(function(){ return fn0() })
當執行 fn1()()()() 時,因為此時 length > 2 為 false,所以執行 fn():
fn() // 相當于 (function(){ return fn0() })() // 相當于 fn0() // 執行 fn0 函數,打印 1
再回到真正的 curry 函數,我們以下面的例子為例:
var fn0 = function(a, b, c, d) { return [a, b, c, d]; } var fn1 = curry(fn0); fn1("a", "b")("c")("d")
當執行 fn1("a", "b") 時:
fn1("a", "b") // 相當于 curry(fn0)("a", "b") // 相當于 curry(sub_curry(fn0, "a", "b")) // 相當于 // 注意 ... 只是一個示意,表示該函數執行時傳入的參數會作為 fn0 后面的參數傳入 curry(function(...){ return fn0("a", "b", ...) })
當執行 fn1("a", "b")("c") 時,函數返回:
curry(sub_curry(function(...){ return fn0("a", "b", ...) }), "c") // 相當于 curry(function(...){ return (function(...) {return fn0("a", "b", ...)})("c") }) // 相當于 curry(function(...){ return fn0("a", "b", "c", ...) })
當執行 fn1("a", "b")("c")("d") 時,此時 arguments.length < length 為 false ,執行 fn(arguments),相當于:
(function(...){ return fn0("a", "b", "c", ...) })("d") // 相當于 fn0("a", "b", "c", "d")
函數執行結束。
所以,其實整段代碼又很好理解:
sub_curry 的作用就是用函數包裹原函數,然后給原函數傳入之前的參數,當執行 fn0(...)(...) 的時候,執行包裹函數,返回原函數,然后再調用 sub_curry 再包裹原函數,然后將新的參數混合舊的參數再傳入原函數,直到函數參數的數目達到要求為止。
如果要明白 curry 函數的運行原理,大家還是要動手寫一遍,嘗試著分析執行步驟。
更易懂的實現當然了,如果你覺得還是無法理解,你可以選擇下面這種實現方式,可以實現同樣的效果:
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"]
或許大家覺得這種方式更好理解,又能實現一樣的效果,為什么不直接就講這種呢?
因為想給大家介紹各種實現的方法嘛,不能因為難以理解就不給大家介紹吶~
第三版curry 函數寫到這里其實已經很完善了,但是注意這個函數的傳參順序必須是從左到右,根據形參的順序依次傳入,如果我不想根據這個順序傳呢?
我們可以創建一個占位符,比如這樣:
var fn = curry(function(a, b, c) { console.log([a, b, c]); }); fn("a", _, "c")("b") // ["a", "b", "c"]
我們直接看第三版的代碼:
// 第三版 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 正確的下標 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) 用參數 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)寫在最后
至此,我們已經實現了一個強大的 curry 函數,可是這個 curry 函數符合柯里化的定義嗎?柯里化可是將一個多參數的函數轉換成多個單參數的函數,但是現在我們不僅可以傳入一個參數,還可以一次傳入兩個參數,甚至更多參數……這看起來更像一個柯里化 (curry) 和偏函數 (partial application) 的綜合應用,可是什么又是偏函數呢?下篇文章會講到。
專題系列JavaScript專題系列目錄地址:https://github.com/mqyqingfeng/Blog。
JavaScript專題系列預計寫二十篇左右,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特點是研(chao)究(xi) underscore 和 jQuery 的實現方式。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87191.html
摘要:專題系列共計篇,主要研究日常開發中一些功能點的實現,比如防抖節流去重類型判斷拷貝最值扁平柯里遞歸亂序排序等,特點是研究專題之函數組合專題系列第十六篇,講解函數組合,并且使用柯里化和函數組合實現模式需求我們需要寫一個函數,輸入,返回。 JavaScript 專題之從零實現 jQuery 的 extend JavaScritp 專題系列第七篇,講解如何從零實現一個 jQuery 的 ext...
摘要:專題系列第十四篇,講解偏函數以及如何實現一個函數定義維基百科中對偏函數的定義為翻譯成中文在計算機科學中,局部應用是指固定一個函數的一些參數,然后產生另一個更小元的函數。 JavaScript 專題系列第十四篇,講解偏函數以及如何實現一個 partial 函數 定義 維基百科中對偏函數 (Partial application) 的定義為: In computer science, pa...
摘要:寫在前面專題系列是我寫的第二個系列,第一個系列是深入系列。專題系列自月日發布第一篇文章,到月日發布最后一篇,感謝各位朋友的收藏點贊,鼓勵指正。 寫在前面 JavaScript 專題系列是我寫的第二個系列,第一個系列是 JavaScript 深入系列。 JavaScript 專題系列共計 20 篇,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里...
摘要:里也有柯里化的實現,只是平時沒有在意。如果函數柯里化后雖然生搬硬套,不過現實業務也會有類似場景。 柯里化 先解釋下什么是 柯里化 在計算機科學中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數而且返回結果的新函數的技術。 js 里也有柯里化的實現,只是平時沒有在意。先把原文簡介貼...
摘要:專題系列第十八篇,講解遞歸和尾遞歸定義程序調用自身的編程技巧稱為遞歸。然而非尾調用函數,就會創建多個執行上下文壓入執行上下文棧。所以我們只用把階乘函數改造成一個尾遞歸形式,就可以避免創建那么多的執行上下文。 JavaScript 專題系列第十八篇,講解遞歸和尾遞歸 定義 程序調用自身的編程技巧稱為遞歸(recursion)。 階乘 以階乘為例: function factorial(n...
閱讀 2722·2021-11-22 13:54
閱讀 1063·2021-10-14 09:48
閱讀 2292·2021-09-08 09:35
閱讀 1550·2019-08-30 15:53
閱讀 1166·2019-08-30 13:14
閱讀 606·2019-08-30 13:09
閱讀 2521·2019-08-30 10:57
閱讀 3334·2019-08-29 13:18