本篇文章為大家展示了JavaScript 中怎么實(shí)現(xiàn)柯里化函數(shù),內(nèi)容簡(jiǎn)明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
創(chuàng)新互聯(lián)公司主要從事成都做網(wǎng)站、網(wǎng)站設(shè)計(jì)、網(wǎng)頁設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)保山,10年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來電咨詢建站服務(wù):13518219792
現(xiàn)在有一個(gè)加法函數(shù):
function add(x, y, z) { return x + y + z}
調(diào)用方式是 add(1, 2, 3)。
如果執(zhí)行柯里化,變成了 curriedAdd(),從效果來說,大致就是變成 curriedAdd(1)(2)(3) 這樣子。
現(xiàn)在先不看怎么對(duì)原函數(shù)執(zhí)行柯里化,而是根據(jù)這個(gè)調(diào)用方式重新寫一個(gè)函數(shù)。代碼可能是這樣的:
function curriedAdd1(x) { return function (y) { return function (z) { return x + y + z } }}
假如現(xiàn)在想要升級(jí)一下,不止可以接受三個(gè)參數(shù)。可以使用 arguments,或者使用展開運(yùn)算符來處理傳入的參數(shù)。
但是有一個(gè)衍生的問題。因?yàn)橹懊看沃荒軅鬟f一個(gè),總共只能傳遞三個(gè),才保證了調(diào)用三次之后參數(shù)個(gè)數(shù)剛好足夠,函數(shù)才能執(zhí)行。
既然我們打算修改為可以接受任意個(gè)數(shù)的參數(shù),那么就要規(guī)定一個(gè)終點(diǎn)。比如說,可以規(guī)定為當(dāng)不再傳入?yún)?shù)的時(shí)候,就執(zhí)行函數(shù)。
下面是使用 arguments 的實(shí)現(xiàn)。
function getCurriedAdd() { // 在外部維護(hù)一個(gè)數(shù)組保存?zhèn)鬟f的變量 let args_arr = [] // 返回一個(gè)閉包 let closure = function () { // 本次調(diào)用傳入的參數(shù) let args = Array.prototype.slice.call(arguments) // 如果傳進(jìn)了新的參數(shù) if (args.length > 0) { // 保存參數(shù) args_arr = args_arr.concat(args) // 再次返回閉包,等待下次調(diào)用 // 也可以 return arguments.callee return closure } // 沒有傳遞參數(shù),執(zhí)行累加 return args_arr.reduce((total, current) => total + current) } return closure}curriedAdd = getCurriedAdd()curriedAdd(1)(2)(3)(4)()復(fù)制代碼
這時(shí)可以發(fā)現(xiàn),上面的整個(gè)函數(shù)里,與函數(shù)具體功能(在這里就是執(zhí)行加法)有關(guān)的,就只是當(dāng)沒有傳遞參數(shù)時(shí)的部分,其他部分都是在實(shí)現(xiàn)怎樣多次接收參數(shù)。
那么,只要讓 getCurriedAdd 接受一個(gè)函數(shù)作為參數(shù),把沒有傳遞參數(shù)時(shí)的那一行代碼替換一下,就可以實(shí)現(xiàn)一個(gè)通用的柯里化函數(shù)了。
把上面的修改一下,實(shí)現(xiàn)一個(gè)通用柯里化函數(shù),并把一個(gè)階乘函數(shù)柯里化:
function currying(fn) { let args_arr = [] let closure = function (...args) { if (args.length > 0) { args_arr = args_arr.concat(args) return closure } // 沒有新的參數(shù),執(zhí)行函數(shù) 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)()
上面的代碼里,對(duì)于函數(shù)執(zhí)行時(shí)機(jī)的判斷,是根據(jù)是否有參數(shù)傳入。但是更多時(shí)候,更合理的依據(jù)是原函數(shù)可以接受的參數(shù)的總數(shù)。
函數(shù)名的 length 屬性就是該函數(shù)接受的參數(shù)個(gè)數(shù)。比如:
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) { // 先把參數(shù)加進(jìn)去 args_arr = args_arr.concat(args) // 如果參數(shù)沒滿,返回閉包等待下一次調(diào)用 if (args_arr.length < max_length) return closure // 傳遞完成,執(zhí)行 return fn(...args_arr) } return closure}function add(x, y, z) { return x + y + z}curriedAdd = currying(add)console.log(curriedAdd(1, 2)(3))復(fù)制代碼
讓我們先看一下 lodash.js 的文檔,看看一個(gè)真正的 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 能夠讓我們:
在多個(gè)函數(shù)調(diào)用中逐步收集參數(shù),不用在一個(gè)函數(shù)調(diào)用中一次收集。
當(dāng)收集到足夠的參數(shù)時(shí),返回函數(shù)執(zhí)行結(jié)果。
為了更好的理解它,我在網(wǎng)上找了多個(gè)實(shí)現(xiàn)示例。然而,我希望是有一個(gè)非常簡(jiǎn)單的教程從一個(gè)基本的例子開始,就像下面這個(gè)一樣,而不是直接從最終的實(shí)現(xiàn)開始。
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 函數(shù)是所有的起點(diǎn)。基本上,這個(gè)函數(shù)的作用就是一個(gè)“參數(shù)收集器”。每次調(diào)用該函數(shù)時(shí),它都會(huì)返回一個(gè)自身的綁定函數(shù)(fb),并且將該函數(shù)提供的“參數(shù)”綁定到返回函數(shù)上。該“參數(shù)”將位于之后調(diào)用返回的綁定函數(shù)時(shí)提供的任何參數(shù)之前。因此,每個(gè)調(diào)用中傳的參數(shù)將被逐漸收集到一個(gè)數(shù)組當(dāng)中。
當(dāng)然,就像 curry 函數(shù)一樣,我們不必一直收集下去?,F(xiàn)在我們可以先寫死一個(gè)終止點(diǎn)。
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; }}
為了讓它表現(xiàn)得和 curry 方法一樣,需要解決兩個(gè)問題:
我們希望將收集到的參數(shù)傳遞給需要它們的目標(biāo)函數(shù),而不是通過將它們傳遞給 console.log 在最后打印出來。
變量 numOfRequiredArguments 不應(yīng)該是寫死的,它應(yīng)該是目標(biāo)函數(shù)所期望的參數(shù)個(gè)數(shù)。
幸運(yùn)的是,JavaScript函數(shù)確實(shí)帶有一個(gè)名為 “l(fā)ength” 的屬性,它指定了函數(shù)所期望的參數(shù)個(gè)數(shù)。因此,我們就可以使用這個(gè)屬性來確定所需要的參數(shù)個(gè)數(shù),而不用再寫死了。那么第二個(gè)問題就解決了。
那第一個(gè)問題呢:保持對(duì)目標(biāo)函數(shù)的引用?
網(wǎng)上有幾個(gè)例子可以解決這個(gè)問題。它們之間雖然略有不同,但是有著相同的思路:除去存儲(chǔ)參數(shù)以外,我們還需要在某處存儲(chǔ)對(duì)于目標(biāo)函數(shù)的引用。
這里我把它們分為兩種不同的方法,它們之間或多或少都有相似之處,理解它們能夠幫助我們更好地理解背后的邏輯。順便說一句,這里我將這個(gè)函數(shù)叫做 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 函數(shù)的作用是:它接收目標(biāo)函數(shù)作為參數(shù),然后返回‘參數(shù)收集器’函數(shù),與上例中 fn 函數(shù)作用相同。唯一的不同點(diǎn)在于,當(dāng)收集的參數(shù)數(shù)量與目標(biāo)函數(shù)所必需的參數(shù)數(shù)量相等時(shí),它將把收集到的參數(shù)通過 apply 方法給到該目標(biāo)函數(shù),并返回計(jì)算的結(jié)果。這個(gè)方法通過將其存儲(chǔ)在 magician 創(chuàng)建的閉包當(dāng)中來解決第一個(gè)問題(引用目標(biāo)函數(shù))。
這個(gè)方法更進(jìn)一步,由于參數(shù)收集器函數(shù)只是一個(gè)普通函數(shù),那為什么不使用 magician 函數(shù)本身作為參數(shù)收集器呢?
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中的一個(gè)不同。因?yàn)?magician 接收目標(biāo)函數(shù)作為它的第一個(gè)參數(shù),因此收集到的參數(shù)將始終包含該函數(shù)作為 arguments[0]。這就導(dǎo)致,我們?cè)跈z查有效參數(shù)的總數(shù)時(shí),需要減去第一個(gè)參數(shù)。
順便說一句,因?yàn)槟繕?biāo)函數(shù)是遞歸地傳遞給 magician 函數(shù)的,所以我們可以通過傳入第一個(gè)參數(shù)顯式地引用目標(biāo)函數(shù),以代替使用閉包來存儲(chǔ)目標(biāo)函數(shù)的引用。
正如你所見,Eric Elliott 上面使用到的 “curry” 函數(shù)和方法1功能相似,但實(shí)際上它是一個(gè)偏函數(shù)(這又是另外一說了)。
const curry = fn => (…args) => fn.bind(null, …args);
上面是一個(gè) curry 函數(shù),它返回“參數(shù)收集器”,該收集器只收集一次參數(shù),并返回綁定的目標(biāo)函數(shù)。
上面的‘magician’函數(shù)仍然沒有l(wèi)odash.js中的‘curry’函數(shù)那樣神奇。lodash的curry允許使用‘_’作為輸入?yún)?shù)的占位符。
curried(1)(_, 3)(2); // => [1, 2, 3], 注意占位符 '_'
為了實(shí)現(xiàn)占位符功能,有一個(gè)隱含的需求:我們需要知道哪些參數(shù)被預(yù)設(shè)給了綁定函數(shù),以及哪些是在調(diào)用函數(shù)時(shí)顯示提供的附加參數(shù)(這里我們稱之為added參數(shù))。
這個(gè)功能可以通過創(chuàng)建另外一個(gè)閉包來完成:
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]); //簡(jiǎn)單起見,使用es6 }}
上面的 fn2 幾乎和 fn 一樣,功能就像‘參數(shù)收集器’一樣。然而,fn2 不是直接返回綁定函數(shù),而是返回一個(gè)中間輔助函數(shù) helper。helper 函數(shù)是未綁定的,因此它可以用來分離預(yù)設(shè)的參數(shù)和后來提供的參數(shù)。
當(dāng)然,我們需要在組合時(shí)進(jìn)行一些修改,而不是通過 [...preset, ...added] 將預(yù)設(shè)的參數(shù)和后來提供的參數(shù)合并起來。我們需要在preset參數(shù)中找到占位符的位置,并用有效的added參數(shù)替換它。我沒有看lodash是如何實(shí)現(xiàn)它的,但下面是一個(gè)完成類似功能的簡(jiǎn)單實(shí)現(xiàn)。
// 定義占位符var _ = '_';function magician3 (targetfn, ...preset) { var numOfArgs = targetfn.length; var nextPos = 0; // 下一個(gè)有效輸入位置的索引,可以是'_',也可以是preset的結(jié)尾 // 查看是否有足夠的有效參數(shù) if (preset.filter(arg=> arg !== _).length === numOfArgs) { return targetfn.apply(null, preset); } else { // 返回'helper'函數(shù) return function (...added) { // 循環(huán)并將added參數(shù)添加到preset參數(shù) while(added.length > 0) { var a = added.shift(); // 獲取下一個(gè)占位符的位置,可以是'_'也可以是preset的末尾 while (preset[nextPos] !== _ && nextPos < preset.length) { nextPos++ } // 更新preset preset[nextPos] = a; nextPos++; } // 綁定更新后的preset return magician3.call(null, targetfn, ...preset); } }}
第15到24行是用于將added參數(shù)放入preset數(shù)組中正確位置的邏輯:無論是占位符或是preset的結(jié)尾。該位置被標(biāo)記為 nextPos 并初始化為索引0。
上述內(nèi)容就是JavaScript 中怎么實(shí)現(xiàn)柯里化函數(shù),你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。