摘要:作為函數(shù)式編程語言,帶來了很多語言上的有趣特性,比如柯里化和反柯里化。在一些函數(shù)式編程語言中,會定義一個特殊的占位變量。個人理解不知道對不對延遲執(zhí)行柯里化的另一個應(yīng)用場景是延遲執(zhí)行。不斷的柯里化,累積傳入的參數(shù),最后執(zhí)行。
作為函數(shù)式編程語言,JS帶來了很多語言上的有趣特性,比如柯里化和反柯里化。
這里可以對照另外一篇介紹 JS 反柯里化 的文章一起看~
1. 簡介柯里化(Currying),又稱部分求值(Partial Evaluation),是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。
核心思想是把多參數(shù)傳入的函數(shù)拆成單參數(shù)(或部分)函數(shù),內(nèi)部再返回調(diào)用下一個單參數(shù)(或部分)函數(shù),依次處理剩余的參數(shù)。
按照Stoyan Stefanov --《JavaScript Pattern》作者 的說法,所謂“柯里化”就是使函數(shù)理解并處理部分應(yīng)用
柯里化有3個常見作用:
參數(shù)復(fù)用
提前返回
延遲計算/運行
talk is cheap,看看怎么實現(xiàn)吧~
2. 實現(xiàn) 2.1 通用實現(xiàn)一個通用實現(xiàn):
function currying(fn, ...rest1) {
return function(...rest2) {
return fn.apply(null, rest1.concat(rest2))
}
}
注意這里concat接受非數(shù)組元素參數(shù)將被當(dāng)做調(diào)用者的一個元素傳入
用它將一個sayHello函數(shù)柯里化試試:
function sayHello(name, age, fruit) {
console.log(console.log(`我叫 ${name},我 ${age} 歲了, 我喜歡吃 ${fruit}`))
}
const curryingShowMsg1 = currying(sayHello, "小明")
curryingShowMsg1(22, "蘋果") // 我叫 小明,我 22 歲了, 我喜歡吃 蘋果
const curryingShowMsg2 = currying(sayHello, "小衰", 20)
curryingShowMsg2("西瓜") // 我叫 小衰,我 20 歲了, 我喜歡吃 西瓜
嘻嘻,感覺還行~
2.2 高階柯里化函數(shù)以上柯里化函數(shù)已經(jīng)能解決一般需求了,但是如果要多層的柯里化總不能不斷地進(jìn)行currying函數(shù)的嵌套吧,我們希望經(jīng)過柯里化之后的函數(shù)每次只傳遞一個或者多個參數(shù),那該怎么做呢:
function curryingHelper(fn, len) {
const length = len || fn.length // 第一遍運行l(wèi)ength是函數(shù)fn一共需要的參數(shù)個數(shù),以后是剩余所需要的參數(shù)個數(shù)
return function(...rest) {
return rest.length >= length // 檢查是否傳入了fn所需足夠的參數(shù)
");this, rest)
: curryingHelper(currying.apply(this, [fn].concat(rest)), length - rest.length) // 在通用currying函數(shù)基礎(chǔ)上
}
}
function sayHello(name, age, fruit) { console.log(`我叫 ${name},我 ${age} 歲了, 我喜歡吃 ${fruit}`) }
const betterShowMsg = curryingHelper(sayHello)
betterShowMsg("小衰", 20, "西瓜") // 我叫 小衰,我 20 歲了, 我喜歡吃 西瓜
betterShowMsg("小豬")(25, "南瓜") // 我叫 小豬,我 25 歲了, 我喜歡吃 南瓜
betterShowMsg("小明", 22)("倭瓜") // 我叫 小明,我 22 歲了, 我喜歡吃 倭瓜
betterShowMsg("小拽")(28)("冬瓜") // 我叫 小拽,我 28 歲了, 我喜歡吃 冬瓜
如此實現(xiàn)一個高階的柯里化函數(shù),使得柯里化一個函數(shù)的時候可以不用嵌套的currying,當(dāng)然是因為把嵌套的地方放到了curryingHelper里面進(jìn)行了...-。-
2.3 瘋狂柯里化函數(shù)盡管柯里化函數(shù)已經(jīng)很牛了,但是它也讓你必須花費點小心思在你所定義函數(shù)的參數(shù)順序上。在一些函數(shù)式編程語言中,會定義一個特殊的“占位變量”。通常會指定下劃線來干這事,如果作為一個函數(shù)的參數(shù)被傳入,就表明這個是可以“跳過的”,是尚待指定的參數(shù)。比如:
var sendAjax = function (url, data, options) { /* ... */ }
var sendPost = function (url, data) { // 當(dāng)然可以這樣
return sendAjax(url, data, { type: "POST", contentType: "application/json" })
}
// 也可以使用下劃線來指定未確定的參數(shù)
var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" })
JS不具備這樣的原生支持,可以使用一個全局占位符變量const _ = { }并且通過===來判斷是否是占位符,當(dāng)然你如果使用了lodash的話可以使用別的符號代替。那么可以這樣改造柯里化函數(shù):
const _ = {}
function crazyCurryingHelper(fn, length, args, holes) {
length = length || fn.length // 第一遍是fn所需的參數(shù)個數(shù),以后是
args = args || []
holes = holes || []
return function(...rest) {
let _args = args.slice(),
_holes = holes.slice(),
argLength = _args.length, // 存儲接收到的args和holes的長度
holeLength = _holes.length,
arg, i = 0
for (; i < rest.length; i++) {
arg = rest[i]
if (arg === _ && holeLength) {
holeLength-- // 循環(huán)_holes的位置
_holes.push(_holes.shift()) // _holes最后一個移到第一個
} else if (arg === _) {
_holes.push(argLength + i) // 存儲_hole就是_的位置
} else if (holeLength) { // 是否還有沒有填補的hole
holeLength--
_args.splice(_holes.shift(), 0, arg) // 在參數(shù)列表指定hole的地方插入當(dāng)前參數(shù)
} else {
_args.push(arg) // 不需要填補hole,直接添加到參數(shù)列表里面
}
}
return _args.length >= length // 遞歸的進(jìn)行柯里化
");this, _args)
: crazyCurryingHelper.call(this, fn, length, _args, _holes)
}
}
function sayHello(name, age, fruit) { console.log(`我叫 ${name},我 ${age} 歲了, 我喜歡吃 ${fruit}`) }
const betterShowMsg = crazyCurryingHelper(sayHello)
betterShowMsg(_, 20)("小衰", _, "西瓜") // 我叫 小衰,我 20 歲了, 我喜歡吃 西瓜
betterShowMsg(_, _, "南瓜")("小豬")(25) // 我叫 小豬,我 25 歲了, 我喜歡吃 南瓜
betterShowMsg("小明")(_, 22)(_, _, "倭瓜") // 我叫 小明,我 22 歲了, 我喜歡吃 倭瓜
betterShowMsg("小拽")(28)("冬瓜") // 我叫 小拽,我 28 歲了, 我喜歡吃 冬瓜
牛B閃閃
3. 柯里化的常見用法 3.1 參數(shù)復(fù)用通過柯里化方法,緩存參數(shù)到閉包內(nèi)部參數(shù),然后在函數(shù)內(nèi)部將緩存的參數(shù)與傳入的參數(shù)組合后apply/bind/call給函數(shù)執(zhí)行,來實現(xiàn)參數(shù)的復(fù)用,降低適用范圍,提高適用性。
參看以下栗子,官員無論添加后續(xù)老婆,都能和合法老婆組合,通過柯里化方法,getWife方法就無需添加多余的合法老婆...
var currying = function(fn) {
var args = [].slice.call(arguments, 1) // fn 指官員消化老婆的手段,args 指的是那個合法老婆
return function(...rest) {
var newArgs = args.concat(...rest) // 已經(jīng)有的老婆和新搞定的老婆們合成一體,方便控制
return fn.apply(null, newArgs) // 這些老婆們用 fn 這個手段消化利用,完成韋小寶前輩的壯舉并返回
}
}
var getWife = currying(function() {
console.log([...arguments].join(";")) // allwife 就是所有的老婆的,包括暗渡陳倉進(jìn)來的老婆
}, "合法老婆")
getWife("老婆1", "老婆2", "老婆3") // 合法老婆;老婆1;老婆2;老婆3
getWife("超越韋小寶的老婆") // 合法老婆;超越韋小寶的老婆
getWife("超級老婆") // 合法老婆;超級老婆
3.2 提高適用性
通用函數(shù)解決了兼容性問題,但同時也會再來,使用的不便利性,不同的應(yīng)用場景往,要傳遞很多參數(shù),以達(dá)到解決特定問題的目的。有時候應(yīng)用中,同一種規(guī)則可能會反復(fù)使用,這就可能會造成代碼的重復(fù)性。
// 未柯里化前
function square(i) { return i * i; }
function dubble(i) { return i * 2; }
function map(handler, list) { return list.map(handler); }
map(square, [1, 2, 3, 4, 5]); // 數(shù)組的每一項平方
map(square, [6, 7, 8, 9, 10]);
map(dubble, [1, 2, 3, 4, 5]); // 數(shù)組的每一項加倍
map(dubble, [6, 7, 8, 9, 10]);
同一規(guī)則重復(fù)使用,帶來代碼的重復(fù)性,因此可以使用上面的通用柯里化實現(xiàn)改造一下:
// 柯里化后
function square(i) { return i * i; }
function dubble(i) { return i * 2; }
function map(handler, ...list) { return list.map(handler); }
var mapSQ = currying(map, square);
mapSQ([1, 2, 3, 4, 5]);
mapSQ([6, 7, 8, 9, 10]);
var mapDB = currying(map, dubble);
mapDB([1, 2, 3, 4, 5]);
mapDB([6, 7, 8, 9, 10]);
可以看到這里柯里化方法的使用和偏函數(shù)比較類似,順便回顧一下偏函數(shù)~
偏函數(shù)是創(chuàng)建一個調(diào)用另外一個部分(參數(shù)或變量已預(yù)制的函數(shù))的函數(shù),函數(shù)可以根據(jù)傳入的參數(shù)來生成一個真正執(zhí)行的函數(shù)。比如:
const isType = function(type) {
return function(obj) {
return Object.prototype.toString.call(obj) === `[object ${type}]`
}
}
const isString = isType("String")
const isFunction = isType("Function")
這樣就用偏函數(shù)快速創(chuàng)建了一組判斷對象類型的方法~
偏函數(shù)固定了函數(shù)的某個部分,通過傳入的參數(shù)或者方法返回一個新的函數(shù)來接受剩余的參數(shù),數(shù)量可能是一個也可能是多個 柯里化是把一個有n個參數(shù)的函數(shù)變成n個只有1個參數(shù)的函數(shù),例如:add = (x, y, z) => x + y + z→curryAdd = x => y => z => x + y + z 當(dāng)偏函數(shù)接受一個參數(shù)并且返回了一個只接受一個參數(shù)的函數(shù),與兩個接受一個參數(shù)的函數(shù)curry()()的柯里化函數(shù),這時候兩個概念類似。(個人理解不知道對不對)
3.3 延遲執(zhí)行柯里化的另一個應(yīng)用場景是延遲執(zhí)行。不斷的柯里化,累積傳入的參數(shù),最后執(zhí)行。例如累加:
const curryAdd = function(...rest) {
const _args = rest
return function cb(...rest) {
if (rest.length === 0) {
return _args.reduce((sum, single) => sum += single)
} else {
_args.push(...rest)
return cb
}
}
}() // 為了保存添加的數(shù),這里要返回一個閉包
curryAdd(1)
curryAdd(2)
curryAdd(3)
curryAdd(4)
curryAdd() // 最后計算輸出:10
更通用的寫法,將處理函數(shù)提取出來:
const curry = function(fn) {
const _args = []
return function cb(...rest) {
if (rest.length === 0) {
return fn.apply(this, _args)
}
_args.push(...rest)
return cb
}
}
const curryAdd = curry((...T) =>
T.reduce((sum, single) => sum += single)
)
curryAdd(1)
curryAdd(2)
curryAdd(3)
curryAdd(4)
curryAdd() // 最后計算輸出:10
4. Function.prototype.bind 方法也是柯里化應(yīng)用
與 call/apply 方法直接執(zhí)行不同,bind 方法將第一個參數(shù)設(shè)置為函數(shù)執(zhí)行的上下文,其他參數(shù)依次傳遞給調(diào)用方法(函數(shù)的主體本身不執(zhí)行,可以看成是延遲執(zhí)行),并動態(tài)創(chuàng)建返回一個新的函數(shù), 這符合柯里化特點。
var foo = {x: 888};
var bar = function () {
console.log(this.x);
}.bind(foo); // 綁定
bar(); // 888
下面是一個 bind 函數(shù)的模擬,testBind 創(chuàng)建并返回新的函數(shù),在新的函數(shù)中將真正要執(zhí)行業(yè)務(wù)的函數(shù)綁定到實參傳入的上下文,延遲執(zhí)行了。
Function.prototype.testBind = function(scope) {
return () => this.apply(scope)
}
var foo = { x: 888 }
var bar = function() {
console.log(this.x)
}.testBind(foo) // 綁定
bar() // 888
網(wǎng)上的帖子大多深淺不一,甚至有些前后矛盾,在下的文章都是學(xué)習(xí)過程中的總結(jié),如果發(fā)現(xiàn)錯誤,歡迎留言指出~
參考:
JS高級程序設(shè)計
JS中的柯里化(currying)
前端開發(fā)者進(jìn)階之函數(shù)柯里化Currying
淺析 JavaScript 中的 函數(shù) currying 柯里化
掌握J(rèn)avaScript函數(shù)的柯里化
函數(shù)式JavaScript(4):函數(shù)柯里化
PS:歡迎大家關(guān)注我的公眾號【前端下午茶】,一起加油吧~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/6771.html
摘要:組合的概念是非常直觀的,并不是函數(shù)式編程獨有的,在我們生活中或者前端開發(fā)中處處可見。其實我們函數(shù)式編程里面的組合也是類似,函數(shù)組合就是一種將已被分解的簡單任務(wù)組織成復(fù)雜的整體過程。在函數(shù)式編程的世界中,有這樣一種很流行的編程風(fēng)格。 JavaScript函數(shù)式編程,真香之認(rèn)識函數(shù)式編程(一) 該系列文章不是針對前端新手,需要有一定的編程經(jīng)驗,而且了解 JavaScript 里面作用域,閉...
摘要:作為函數(shù)式編程語言,帶來了很多語言上的有趣特性,比如柯里化和反柯里化。而反柯里化,從字面講,意義和用法跟函數(shù)柯里化相比正好相反,擴(kuò)大適用范圍,創(chuàng)建一個應(yīng)用范圍更廣的函數(shù)。作為函數(shù)式編程語言,JS帶來了很多語言上的有趣特性,比如柯里化和反柯里化。 可以對照另外一篇介紹 JS 柯里化 的文章一起看~ 1. 簡介 柯里化,是固定部分參數(shù),返回一個接受剩余參數(shù)的函數(shù),也稱為部分計算函數(shù),目的是為了縮...
摘要:函數(shù)式編程,一看這個詞,簡直就是學(xué)院派的典范。所以這期周刊,我們就重點引入的函數(shù)式編程,淺入淺出,一窺函數(shù)式編程的思想,可能讓你對編程語言的理解更加融會貫通一些。但從根本上來說,函數(shù)式編程就是關(guān)于如使用通用的可復(fù)用函數(shù)進(jìn)行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數(shù)式編程(Functional Programming),一...
摘要:如果你對函數(shù)式編程有一定了解,函數(shù)柯里化是不可或缺的,利用函數(shù)柯里化,可以在開發(fā)中非常優(yōu)雅的處理復(fù)雜邏輯。同樣先看簡單版本的方法,以方法為例,代碼來自高級程序設(shè)計加強(qiáng)版實現(xiàn)上面函數(shù),可以換成任何其他函數(shù),經(jīng)過函數(shù)處理,都可以轉(zhuǎn)成柯里化函數(shù)。 我們經(jīng)常說在Javascript語言中,函數(shù)是一等公民,它們本質(zhì)上是十分簡單和過程化的。可以利用函數(shù),進(jìn)行一些簡單的數(shù)據(jù)處理,return 結(jié)果,...
摘要:所以下面介紹一些函數(shù)式編程的知識和概念。函數(shù)式編程的一個明顯的好處就是這種聲明式的代碼,對于無副作用的純函數(shù),我們完全可以不考慮函數(shù)內(nèi)部是如何實現(xiàn)的,專注于編寫業(yè)務(wù)代碼。我會在下一篇文章中介紹函數(shù)式編程的更加高階一些的知識,例如等等概念。 一、引言 說到函數(shù)式編程,大家可能第一印象都是學(xué)院派的那些晦澀難懂的代碼,充滿了一大堆抽象的不知所云的符號,似乎只有大學(xué)里的計算機(jī)教授才會使用這些東...
閱讀 2307·2023-04-25 14:17
閱讀 1515·2021-11-23 10:02
閱讀 2170·2021-11-23 09:51
閱讀 873·2021-10-14 09:49
閱讀 3384·2021-10-11 10:57
閱讀 2921·2021-09-24 09:47
閱讀 3046·2021-08-24 10:00
閱讀 2298·2019-08-29 18:46