JavaScript函數(shù)柯里化詳解
一、簡(jiǎn)單了解apply和call
- call 和 apply 都是為了改變某個(gè)函數(shù)運(yùn)行時(shí)的 context 即上下文而存在的,換句話說(shuō),就是為了改變函數(shù)體內(nèi)部 this 的指向。
- call 和 apply二者的作用完全一樣,只是接受參數(shù)的方式不太一樣。call其實(shí)是apply的一種語(yǔ)法糖。
- 格式:
apply(context,[arguments])
,call(context,param1,param2,...)
。
二、什么是函數(shù)柯里化?
柯里化(Currying)是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù)。
在這里舉個(gè)例子,有一個(gè)add()
函數(shù),它是用來(lái)處理我們傳給它的參數(shù)(param1,params2,…)相加求和的一個(gè)函數(shù)。
// 在這里第一個(gè)具有兩個(gè)參數(shù)`x`、`y`的`add(x , y)`函數(shù) function add(x , y){ return x + y; } // 調(diào)用`add()`函數(shù),并給定兩個(gè)參數(shù)`4`和`6` add(4,6); // 模擬計(jì)算機(jī)操作,第一步 傳入第一個(gè)參數(shù) 4 function add(4 , y){ return 4 + y; } // 模擬計(jì)算機(jī)操作,第二步 傳入第一個(gè)參數(shù) 6 function add(4 , 6){ return 4 + 6; }
如果我們將add()
函數(shù)柯里化,是什么樣子呢?在這里簡(jiǎn)單的實(shí)現(xiàn)一下:
// 柯里化過(guò)的add()函數(shù),可以接受部分參數(shù) function add(x ,y){ if (typeof y === 'undefined') { return function (newy){ return x + newy; } } // 完整應(yīng)用 return x + y; } // 測(cè)試調(diào)用 console.log(typeof add(4)); // [Function] console.log(add(4)(6)); // 10 // 可以創(chuàng)建保存函數(shù) let saveAdd = add(4); console.log(saveAdd(6)); // 10
從以上簡(jiǎn)單柯里化的add()
函數(shù)可以看出,函數(shù)可以接受部分函數(shù),然后返回一個(gè)新的函數(shù),使其繼續(xù)處理剩下的函數(shù)。
三、寫(xiě)一個(gè)公共的柯里化函數(shù)
在這里我們創(chuàng)建一個(gè)公共的柯里化函數(shù),那樣我們就不必每次寫(xiě)一個(gè)函數(shù)都要在其內(nèi)部實(shí)現(xiàn)復(fù)雜的柯里化過(guò)程。
// 定義一個(gè)createCurry的函數(shù) function createCurry(fn){ var slice = Array.prototype.slice, stored_args = slice.call(arguments,1); return function () { let new_args = slice.call(arguments), args = stored_args.concat(new_args); return fn.apply(null,args); } }
在以上公共的柯里化函數(shù)中:
arguments
,并不是一個(gè)真的數(shù)組,只是一個(gè)具有length
屬性的對(duì)象,所以我們從Array.prototype
中借用slice
方法幫我們把arguments
轉(zhuǎn)為一個(gè)真正的數(shù)組,方便我們更好的操作。- 當(dāng)我們第一次調(diào)用函數(shù)
createCurry
的時(shí)候,其中變量stored_args
是保持了除去第一個(gè)參數(shù)以外的參數(shù),因?yàn)榈谝粋€(gè)參數(shù)是我們需要柯里化的函數(shù)。 - 當(dāng)我們執(zhí)行
createCurry
函數(shù)中返回的函數(shù)時(shí),變量new_args
獲取參數(shù)并轉(zhuǎn)為數(shù)組。 - 內(nèi)部返回的函數(shù)通過(guò)閉包訪問(wèn)變量
stored_args
中存儲(chǔ)的值和變量new_args
的值合并為一個(gè)新的數(shù)組,并賦值給變量args
。 - 最后調(diào)用
fn.apply(null,args)
方法,執(zhí)行被柯里化的函數(shù)。
現(xiàn)在我們來(lái)測(cè)試公共的柯里化函數(shù)
// 普通函數(shù)add() function add(x , y){ return x + y; } // 柯里化得到一個(gè)新的函數(shù) var newAdd = createCurry(add,4); console.log(newAdd(6)); // 10 //另一種簡(jiǎn)便方式 console.log(createCurry(add,4)(6));// 10
當(dāng)然這里并不局限于兩個(gè)參數(shù)的柯里化,也可以多個(gè)參數(shù):
// 多個(gè)參數(shù)的普通函數(shù) function add(a,b,c,d){ return a + b + c + d; } // 柯里化函數(shù)得到新函數(shù),多個(gè)參數(shù)可以隨意分割 console.log(createCurry(add,4,5)(5,6)); // 20 // 兩步柯里化 let add_one = createCurry(add,5); console.log(add_one(5,5,5));// 20 let add_two = createCurry(add_one,4,6); console.log(add_two(6)); // 21
通過(guò)以上的例子,我們可以發(fā)現(xiàn)一個(gè)局限,那就是不管是兩個(gè)參數(shù)還是多個(gè)參數(shù),它只能分兩步執(zhí)行,如以下公式:
- fn(x,y) ==> fn(x)(y);
- fn(x,y,z,w) ==> fn(x)(y,z,w) || fn(x,y)(z,w)||…
如果我們想更靈活一點(diǎn):
- fn(x,y) ==> fn(x)(y);
- fn(x,y,z) ==> fn(x,y)(z) || fn(x)(y)(z);
- fn(x,y,z,w) ==> fn(x,y)(z)(w) || fn(x)(y)(z)(w) || …;
我們?cè)撛趺磳?shí)現(xiàn)呢?
四、創(chuàng)建一個(gè)靈活的柯里化函數(shù)
經(jīng)過(guò)以上練習(xí),我們發(fā)現(xiàn)我們創(chuàng)建的柯里化函數(shù)存在一定局限性,我們希望函數(shù)可以分為多步執(zhí)行:
// 創(chuàng)建一個(gè)可以多步執(zhí)行的柯里化函數(shù),當(dāng)參數(shù)滿(mǎn)足數(shù)量時(shí)就去執(zhí)行它: // 函數(shù)公式:fn(x,y,z,w) ==> fn(x)(y)(z)(w); let createCurry = (fn,...params)=> { let args = parsms || []; let fnLen = fn.length; // 指定柯里化函數(shù)的參數(shù)長(zhǎng)度 return (...res)=> { // 通過(guò)作用域鏈獲取上一次的所有參數(shù) let allArgs = args.slice(0); // 深度拷貝閉包共用的args參數(shù),避免后續(xù)操作影響(引用類(lèi)型) allArgs.push(...res); if(allArgs.length < fnLen){ // 當(dāng)參數(shù)數(shù)量小于原函數(shù)的參數(shù)長(zhǎng)度時(shí),遞歸調(diào)用createCurry函數(shù) return createCurry.call(this,fn,...allArgs); }else{ // 當(dāng)參數(shù)數(shù)量滿(mǎn)足時(shí),觸發(fā)函數(shù)執(zhí)行 return fn.apply(this,allArgs); } } } // 多個(gè)參數(shù)的普通函數(shù) function add(a,b,c,d){ return a + b + c + d; } // 測(cè)試柯里化函數(shù) let curryAdd = createCurry(add,1); console.log(curryAdd(2)(3)(4)); // 10
以上我們已經(jīng)實(shí)現(xiàn)了靈活的柯里化函數(shù),但是這里我們又發(fā)現(xiàn)了一個(gè)問(wèn)題:
- 如果我第一次就把參數(shù)全部傳入,但是它并沒(méi)有返回結(jié)果,而是一個(gè)函數(shù)(function)。
- 只有我們?cè)俅螌⒎祷氐暮瘮?shù)調(diào)用一次才能返回結(jié)果:
curryAdd(add,1,2,3,4)()
; - 可能有人說(shuō)如果是全部傳參,就調(diào)用原來(lái)的
add()
函數(shù)就行了,這也是一種辦法;但是我們?cè)谶@里既然是滿(mǎn)足參數(shù)數(shù)量,對(duì)于這種情況我們還是處理一下。
在這里我們只需要在返回函數(shù)前做一下判斷就行了:
let createCurry = (fn,...params)=> { let args = parsms || []; let fnLen = fn.length; // 指定柯里化函數(shù)的參數(shù)長(zhǎng)度 if(length === _args.length){ // 加入判斷,如果第一次參數(shù)數(shù)量以經(jīng)足夠時(shí)就直接調(diào)用函數(shù)獲取結(jié)果 return fn.apply(this,args); } return (...res)=> { let allArgs = args.slice(0); allArgs.push(...res); if(allArgs.length < fnLen){ return createCurry.call(this,fn,...allArgs); }else{ return fn.apply(this,allArgs); } } }
以上可以算是完成了一個(gè)靈活的柯里化的函數(shù)了,但是這里還不算很靈活,因?yàn)槲覀儾荒芸刂扑裁磿r(shí)候執(zhí)行,只要參數(shù)數(shù)量足夠它就自動(dòng)執(zhí)行。我們希望實(shí)現(xiàn)一個(gè)可以控制它執(zhí)行的時(shí)機(jī)該怎么辦呢?
五、寫(xiě)一個(gè)可控制的執(zhí)行時(shí)間的柯里化函數(shù)
我們這里直接說(shuō)明一下函數(shù)公式:
- fn(a,b,c) ==> fn(a)(b)(c )();
- fn(a,b,c) ==> fn(a);fn(b);fn(c );fn();
- 當(dāng)我們參數(shù)足夠時(shí)它并不會(huì)執(zhí)行,只有我們?cè)俅握{(diào)用一次函數(shù)它才會(huì)執(zhí)行并返回結(jié)果。在這里我們?cè)谝陨侠又屑右粋€(gè)小小的條件就可以實(shí)現(xiàn)。
// 當(dāng)參數(shù)滿(mǎn)足,再次執(zhí)行時(shí)調(diào)用函數(shù) let createCurry = (fn,...params)=> { let args = parsms || []; let fnLen = fn.length; // 指定柯里化函數(shù)的參數(shù)長(zhǎng)度 //當(dāng)然這里的判斷需要注釋掉,不然當(dāng)它第一次參數(shù)數(shù)量足夠時(shí)就直接執(zhí)行結(jié)果了 //if(length === _args.length){ // 加入判斷,如果第一次參數(shù)數(shù)量以經(jīng)足夠時(shí)就直接調(diào)用函數(shù)獲取結(jié)果 //return fn.apply(this,args); //} return (...res)=> { let allArgs = args.slice(0); allArgs.push(...res); // 在這里判斷輸入的參數(shù)是否大于0,如果大于0在判斷參數(shù)數(shù)量是否足夠, // 這里不能用 && ,如果用&& 也是參數(shù)數(shù)量足夠時(shí)就執(zhí)行結(jié)果了。 if(res.length > 0 || allArgs.length < fnLen){ return createCurry.call(this,fn,...allArgs); }else{ return fn.apply(this,allArgs); } } } // 多個(gè)參數(shù)的普通函數(shù) function add(a,b,c,d){ return a + b + c + d; } // 測(cè)試可控制的柯里化函數(shù) let curryAdd = createCurry(add,1); console.log(curryAdd(2)(3)(4)); // function console.log(curryAdd(2)(3)(4)()); // 10 console.log(curryAdd(2)(3)()); // 當(dāng)參數(shù)不足夠時(shí)返回 NaN
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
實(shí)現(xiàn)png圖片和png背景透明(支持多瀏覽器)的方法
Firefox和Opera對(duì)PNG的支持非常的好,都是IE卻無(wú)視PNG圖片這一特性的“存在”,雖然IE7已經(jīng)支持都是IE6還是不行。2009-09-09javascript 彈出的窗口返回值給父窗口具體實(shí)現(xiàn)
這篇文章主要介紹了javascript 彈出的窗口返回值給父窗口具體實(shí)現(xiàn),有需要的朋友可以參考一下2013-11-11ZK中使用JS讀取客戶(hù)端txt文件內(nèi)容問(wèn)題
這篇文章主要介紹了ZK中使用JS讀取客戶(hù)端txt文件內(nèi)容問(wèn)題,本文通過(guò)實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11javascript數(shù)組中的reduce方法和pop方法
這篇文章主要介紹了javascript數(shù)組中的reduce方法和pop方法,文章內(nèi)容介紹詳細(xì),具有一定的參考價(jià)值需要的小伙伴可以參考一下,希望對(duì)你的學(xué)習(xí)有所幫助2022-03-03javascript GUID生成器實(shí)現(xiàn)代碼
javascript GUID生成器實(shí)現(xiàn)代碼, 需要的朋友可以參考下。2009-10-10JavaScript 進(jìn)度條實(shí)現(xiàn)代碼(Firefox等相似瀏覽器下不支持)
JavaScript實(shí)現(xiàn)的進(jìn)度條,可惜在Firefox等相似瀏覽器下不支持(遠(yuǎn)程)2009-07-07js中find、findIndex、indexOf的用法和區(qū)別
本文主要介紹了js中find、findIndex、indexOf的用法和區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07