您好,登錄后才能下訂單哦!
本篇文章為大家展示了JavaScript 中怎么實現柯里化函數,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
現在有一個加法函數:
function add(x, y, z) { return x + y + z}
調用方式是 add(1, 2, 3)。
如果執行柯里化,變成了 curriedAdd(),從效果來說,大致就是變成 curriedAdd(1)(2)(3) 這樣子。
現在先不看怎么對原函數執行柯里化,而是根據這個調用方式重新寫一個函數。代碼可能是這樣的:
function curriedAdd1(x) { return function (y) { return function (z) { return x + y + z } }}
假如現在想要升級一下,不止可以接受三個參數。可以使用 arguments,或者使用展開運算符來處理傳入的參數。
但是有一個衍生的問題。因為之前每次只能傳遞一個,總共只能傳遞三個,才保證了調用三次之后參數個數剛好足夠,函數才能執行。
既然我們打算修改為可以接受任意個數的參數,那么就要規定一個終點。比如說,可以規定為當不再傳入參數的時候,就執行函數。
下面是使用 arguments 的實現。
function getCurriedAdd() { // 在外部維護一個數組保存傳遞的變量 let args_arr = [] // 返回一個閉包 let closure = function () { // 本次調用傳入的參數 let args = Array.prototype.slice.call(arguments) // 如果傳進了新的參數 if (args.length > 0) { // 保存參數 args_arr = args_arr.concat(args) // 再次返回閉包,等待下次調用 // 也可以 return arguments.callee return closure } // 沒有傳遞參數,執行累加 return args_arr.reduce((total, current) => total + current) } return closure}curriedAdd = getCurriedAdd()curriedAdd(1)(2)(3)(4)()復制代碼
這時可以發現,上面的整個函數里,與函數具體功能(在這里就是執行加法)有關的,就只是當沒有傳遞參數時的部分,其他部分都是在實現怎樣多次接收參數。
那么,只要讓 getCurriedAdd 接受一個函數作為參數,把沒有傳遞參數時的那一行代碼替換一下,就可以實現一個通用的柯里化函數了。
把上面的修改一下,實現一個通用柯里化函數,并把一個階乘函數柯里化:
function currying(fn) { let args_arr = [] let closure = function (...args) { if (args.length > 0) { args_arr = args_arr.concat(args) return closure } // 沒有新的參數,執行函數 return fn(...args_arr) } return closure}function multiply(...args) { return args.reduce((total, current) => total * current)}curriedMultiply = currying(multiply)console.log(curriedMultiply(2)(3, 4)()
上面的代碼里,對于函數執行時機的判斷,是根據是否有參數傳入。但是更多時候,更合理的依據是原函數可以接受的參數的總數。
函數名的 length 屬性就是該函數接受的參數個數。比如:
function test1(a, b) {}function test2(...args){}console.log(test1.length) // 2console.log(test2.length) // 0
改寫一下:
function currying(fn) { let args_arr = [], max_length = fn.length let closure = function (...args) { // 先把參數加進去 args_arr = args_arr.concat(args) // 如果參數沒滿,返回閉包等待下一次調用 if (args_arr.length < max_length) return closure // 傳遞完成,執行 return fn(...args_arr) } return closure}function add(x, y, z) { return x + y + z}curriedAdd = currying(add)console.log(curriedAdd(1, 2)(3))復制代碼
讓我們先看一下 lodash.js 的文檔,看看一個真正的 curry 方法到底是做什么的。
var abc = function(a, b, c) { return [a, b, c];};var curried = _.curry(abc);curried(1)(2)(3); // => [1, 2, 3]curried(1, 2)(3); // => [1, 2, 3]curried(1, 2, 3); // => [1, 2, 3]// Curried with placeholders.curried(1)(_, 3)(2); // => [1, 2, 3]
在我理解看來,curry 能夠讓我們:
在多個函數調用中逐步收集參數,不用在一個函數調用中一次收集。
當收集到足夠的參數時,返回函數執行結果。
為了更好的理解它,我在網上找了多個實現示例。然而,我希望是有一個非常簡單的教程從一個基本的例子開始,就像下面這個一樣,而不是直接從最終的實現開始。
var fn = function() { console.log(arguments); return fn.bind(null, ...arguments); // 如果沒有es6的話我們可以這樣寫: // return Function.prototype.bind.apply(fn, [null].concat( // Array.prototype.slice.call(arguments) // ));}fb = fn(1); //[1]fb = fb(2); //[1, 2]fb = fb(3); //[1, 2, 3]fb = fb(4); //[1, 2, 3, 4]
理解 fn 函數是所有的起點。基本上,這個函數的作用就是一個“參數收集器”。每次調用該函數時,它都會返回一個自身的綁定函數(fb),并且將該函數提供的“參數”綁定到返回函數上。該“參數”將位于之后調用返回的綁定函數時提供的任何參數之前。因此,每個調用中傳的參數將被逐漸收集到一個數組當中。
當然,就像 curry 函數一樣,我們不必一直收集下去。現在我們可以先寫死一個終止點。
var numOfRequiredArguments = 5;var fn = function() { if (arguments.length < numOfRequiredArguments) { return fn.bind(null, ...arguments); } else { console.log('we already collect 5 arguments: ', [...arguments]); return null; }}
為了讓它表現得和 curry 方法一樣,需要解決兩個問題:
我們希望將收集到的參數傳遞給需要它們的目標函數,而不是通過將它們傳遞給 console.log 在最后打印出來。
變量 numOfRequiredArguments 不應該是寫死的,它應該是目標函數所期望的參數個數。
幸運的是,JavaScript函數確實帶有一個名為 “length” 的屬性,它指定了函數所期望的參數個數。因此,我們就可以使用這個屬性來確定所需要的參數個數,而不用再寫死了。那么第二個問題就解決了。
那第一個問題呢:保持對目標函數的引用?
網上有幾個例子可以解決這個問題。它們之間雖然略有不同,但是有著相同的思路:除去存儲參數以外,我們還需要在某處存儲對于目標函數的引用。
這里我把它們分為兩種不同的方法,它們之間或多或少都有相似之處,理解它們能夠幫助我們更好地理解背后的邏輯。順便說一句,這里我將這個函數叫做 magician,以代替 curry。
function magician(targetfn) { var numOfArgs = targetfn.length; return function fn() { if (arguments.length < numOfArgs) { return fn.bind(null, ...arguments); } else { return targetfn.apply(null, arguments); } }}
magician 函數的作用是:它接收目標函數作為參數,然后返回‘參數收集器’函數,與上例中 fn 函數作用相同。唯一的不同點在于,當收集的參數數量與目標函數所必需的參數數量相等時,它將把收集到的參數通過 apply 方法給到該目標函數,并返回計算的結果。這個方法通過將其存儲在 magician 創建的閉包當中來解決第一個問題(引用目標函數)。
這個方法更進一步,由于參數收集器函數只是一個普通函數,那為什么不使用 magician 函數本身作為參數收集器呢?
function magician (targetfn) { var numOfArgs = targetfn.length; if (arguments.length - 1 < numOfArgs) { return magician.bind(null, ...arguments); } else { return targetfn.apply(null, Array.prototype.slice.call(arguments, 1)); }}
注意方法2中的一個不同。因為 magician 接收目標函數作為它的第一個參數,因此收集到的參數將始終包含該函數作為 arguments[0]。這就導致,我們在檢查有效參數的總數時,需要減去第一個參數。
順便說一句,因為目標函數是遞歸地傳遞給 magician 函數的,所以我們可以通過傳入第一個參數顯式地引用目標函數,以代替使用閉包來存儲目標函數的引用。
正如你所見,Eric Elliott 上面使用到的 “curry” 函數和方法1功能相似,但實際上它是一個偏函數(這又是另外一說了)。
const curry = fn => (…args) => fn.bind(null, …args);
上面是一個 curry 函數,它返回“參數收集器”,該收集器只收集一次參數,并返回綁定的目標函數。
上面的‘magician’函數仍然沒有lodash.js中的‘curry’函數那樣神奇。lodash的curry允許使用‘_’作為輸入參數的占位符。
curried(1)(_, 3)(2); // => [1, 2, 3], 注意占位符 '_'
為了實現占位符功能,有一個隱含的需求:我們需要知道哪些參數被預設給了綁定函數,以及哪些是在調用函數時顯示提供的附加參數(這里我們稱之為added參數)。
這個功能可以通過創建另外一個閉包來完成:
function fn2() { var preset = Array.prototype.slice.call(arguments); /* 原先是這樣: return fn.bind(null, ...arguments); */ return function helper() { var added = Array.prototype.slice.call(arguments); return fn2.apply(null, [...preset, ...added]); //簡單起見,使用es6 }}
上面的 fn2 幾乎和 fn 一樣,功能就像‘參數收集器’一樣。然而,fn2 不是直接返回綁定函數,而是返回一個中間輔助函數 helper。helper 函數是未綁定的,因此它可以用來分離預設的參數和后來提供的參數。
當然,我們需要在組合時進行一些修改,而不是通過 [...preset, ...added] 將預設的參數和后來提供的參數合并起來。我們需要在preset參數中找到占位符的位置,并用有效的added參數替換它。我沒有看lodash是如何實現它的,但下面是一個完成類似功能的簡單實現。
// 定義占位符var _ = '_';function magician3 (targetfn, ...preset) { var numOfArgs = targetfn.length; var nextPos = 0; // 下一個有效輸入位置的索引,可以是'_',也可以是preset的結尾 // 查看是否有足夠的有效參數 if (preset.filter(arg=> arg !== _).length === numOfArgs) { return targetfn.apply(null, preset); } else { // 返回'helper'函數 return function (...added) { // 循環并將added參數添加到preset參數 while(added.length > 0) { var a = added.shift(); // 獲取下一個占位符的位置,可以是'_'也可以是preset的末尾 while (preset[nextPos] !== _ && nextPos < preset.length) { nextPos++ } // 更新preset preset[nextPos] = a; nextPos++; } // 綁定更新后的preset return magician3.call(null, targetfn, ...preset); } }}
第15到24行是用于將added參數放入preset數組中正確位置的邏輯:無論是占位符或是preset的結尾。該位置被標記為 nextPos 并初始化為索引0。
上述內容就是JavaScript 中怎么實現柯里化函數,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。