摘要:引言上一節(jié)介紹了高階函數(shù)的定義,并結(jié)合實例說明了使用高階函數(shù)和不使用高階函數(shù)的情況。我們期望函數(shù)輸出,但是實際上調(diào)用柯里化函數(shù)時,所以調(diào)用時就已經(jīng)執(zhí)行并輸出了,而不是理想中的返回閉包函數(shù),所以后續(xù)調(diào)用將會報錯。
引言上一節(jié)介紹了高階函數(shù)的定義,并結(jié)合實例說明了使用高階函數(shù)和不使用高階函數(shù)的情況。后面幾部分將結(jié)合實際應(yīng)用場景介紹高階函數(shù)的應(yīng)用,本節(jié)先來聊聊函數(shù)柯里化,通過介紹其定義、比較常見的三種柯里化應(yīng)用、并在最后實現(xiàn)一個通用的 currying 函數(shù),帶你認(rèn)識完整的函數(shù)柯里化。
有什么想法或者意見都可以在評論區(qū)留言,下圖是本文的思維導(dǎo)圖,高清思維導(dǎo)圖和更多文章請看我的 Github。
柯里化 定義
函數(shù)柯里化又叫部分求值,維基百科中對柯里化 (Currying) 的定義為:
在數(shù)學(xué)和計算機(jī)科學(xué)中,柯里化是一種將使用多個參數(shù)的函數(shù)轉(zhuǎn)換成一系列使用一個參數(shù)的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。
用大白話來說就是只傳遞給函數(shù)一部分參數(shù)來調(diào)用它,讓它返回一個新函數(shù)去處理剩下的參數(shù)。使用一個簡單的例子來介紹下,最常用的就是 add 函數(shù)了。
// 木易楊
const add = (...args) => args.reduce((a, b) => a + b);
// 傳入多個參數(shù),執(zhí)行 add 函數(shù)
add(1, 2) // 3
// 假設(shè)我們實現(xiàn)了一個 currying 函數(shù),支持一次傳入一個參數(shù)
let sum = currying(add);
// 封裝第一個參數(shù),方便重用
let addCurryOne = sum(1);
addCurryOne(2) // 3
addCurryOne(3) // 4
實際應(yīng)用
我們看下面的部分求和例子,很好的說明了延遲計算這個情況。
// 木易楊
const add = (...args) => args.reduce((a, b) => a + b);
// 簡化寫法
function currying(func) {
const args = [];
return function result(...rest) {
if (rest.length === 0) {
return func(...args);
} else {
args.push(...rest);
return result;
}
}
}
const sum = currying(add);
sum(1,2)(3); // 未真正求值
sum(4); // 未真正求值
sum(); // 輸出 10
上面的代碼理解起來很容易,就是「用閉包把傳入?yún)?shù)保存起來,當(dāng)傳入?yún)?shù)的數(shù)量足夠執(zhí)行函數(shù)時,就開始執(zhí)行函數(shù)」。上面的 currying 函數(shù)是一種簡化寫法,判斷傳入的參數(shù)長度是否為 0,若為 0 執(zhí)行函數(shù),否則收集參數(shù)。
另一種常見的應(yīng)用是 bind 函數(shù),我們看下 bind 的使用。
// 木易楊
let obj = {
name: "muyiy"
}
const fun = function () {
console.log(this.name);
}.bind(obj);
fun(); // muyiy
這里 bind 用來改變函數(shù)執(zhí)行時候的上下文,但是函數(shù)本身并不執(zhí)行,所以本質(zhì)上是延遲計算,這一點和 call / apply 直接執(zhí)行有所不同。
我們看下 bind 模擬實現(xiàn),其本身就是一種柯里化,我們在最后的實現(xiàn)部分會發(fā)現(xiàn),bind 的模擬實現(xiàn)和柯理化函數(shù)的實現(xiàn),其核心代碼都是一致的。
以下實現(xiàn)方案是簡化版實現(xiàn),完整版實現(xiàn)過程和代碼解讀請看我之前寫的一篇文章,【進(jìn)階3-4期】深度解析bind原理、使用場景及模擬實現(xiàn)。
// 木易楊
// 簡化實現(xiàn),完整版實現(xiàn)中的第 2 步
Function.prototype.bind = function (context) {
var self = this;
// 第 1 個參數(shù)是指定的 this,截取保存第 1 個之后的參數(shù)
// arr.slice(begin); 即 [begin, end]
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 此時的 arguments 是指 bind 返回的函數(shù)調(diào)用時接收的參數(shù)
// 即 return function 的參數(shù),和上面那個不同
// 類數(shù)組轉(zhuǎn)成數(shù)組
var bindArgs = Array.prototype.slice.call(arguments);
// 執(zhí)行函數(shù)
return self.apply( context, args.concat(bindArgs) );
}
}
有一種典型的應(yīng)用情景是這樣的,每次調(diào)用函數(shù)都需要進(jìn)行一次判斷,但其實第一次判斷計算之后,后續(xù)調(diào)用并不需要再次判斷,這種情況下就非常適合使用柯里化方案來處理。即第一次判斷之后,動態(tài)創(chuàng)建一個新函數(shù)用于處理后續(xù)傳入的參數(shù),并返回這個新函數(shù)。當(dāng)然也可以使用惰性函數(shù)來處理,本例最后一個方案會有所介紹。
我們看下面的這個例子,在 DOM 中添加事件時需要兼容現(xiàn)代瀏覽器和 IE 瀏覽器(IE < 9),方法就是對瀏覽器環(huán)境進(jìn)行判斷,看瀏覽器是否支持,簡化寫法如下。
// 簡化寫法
function addEvent (type, el, fn, capture = false) {
if (window.addEventListener) {
el.addEventListener(type, fn, capture);
}
else if(window.attachEvent){
el.attachEvent("on" + type, fn);
}
}
但是這種寫法有一個問題,就是每次添加事件都會調(diào)用做一次判斷,那么有沒有什么辦法只判斷一次呢,可以利用閉包和立即調(diào)用函數(shù)表達(dá)式(IIFE)來處理。
const addEvent = (function(){
if (window.addEventListener) {
return function (type, el, fn, capture) {
el.addEventListener(type, fn, capture);
}
}
else if(window.attachEvent){
return function (type, el, fn) {
el.attachEvent("on" + type, fn);
}
}
})();
上面這種實現(xiàn)方案就是一種典型的柯里化應(yīng)用,在第一次的 if...else if... 判斷之后完成部分計算,動態(tài)創(chuàng)建新的函數(shù)用于處理后續(xù)傳入的參數(shù),這樣做的好處就是之后調(diào)用就不需要再次計算了。
當(dāng)然可以使用惰性函數(shù)來實現(xiàn)這一功能,原理很簡單,就是重寫函數(shù)。
function addEvent (type, el, fn, capture = false) {
// 重寫函數(shù)
if (window.addEventListener) {
addEvent = function (type, el, fn, capture) {
el.addEventListener(type, fn, capture);
}
}
else if(window.attachEvent){
addEvent = function (type, el, fn) {
el.attachEvent("on" + type, fn);
}
}
// 執(zhí)行函數(shù),有循環(huán)爆棧風(fēng)險
addEvent(type, el, fn, capture);
}
第一次調(diào)用 addEvent 函數(shù)后,會進(jìn)行一次環(huán)境判斷,在這之后 addEvent 函數(shù)被重寫,所以下次調(diào)用時就不會再次判斷環(huán)境,可以說很完美了。
我們知道調(diào)用 toString() 可以獲取每個對象的類型,但是不同對象的 toString() 有不同的實現(xiàn),所以需要通過 Object.prototype.toString() 來獲取 Object 上的實現(xiàn),同時以 call() / apply() 的形式來調(diào)用,并傳遞要檢查的對象作為第一個參數(shù),例如下面這個例子。
function isArray(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
function isNumber(obj) {
return Object.prototype.toString.call(obj) === "[object Number]";
}
function isString(obj) {
return Object.prototype.toString.call(obj) === "[object String]";
}
// Test
isArray([1, 2, 3]); // true
isNumber(123); // true
isString("123"); // true
但是上面方案有一個問題,那就是每種類型都需要定義一個方法,這里我們可以使用 bind 來擴(kuò)展,優(yōu)點是可以直接使用改造后的 toStr。
const toStr = Function.prototype.call.bind(Object.prototype.toString);
// 改造前
[1, 2, 3].toString(); // "1,2,3"
"123".toString(); // "123"
123.toString(); // SyntaxError: Invalid or unexpected token
Object(123).toString(); // "123"
// 改造后
toStr([1, 2, 3]); // "[object Array]"
toStr("123"); // "[object String]"
toStr(123); // "[object Number]"
toStr(Object(123)); // "[object Number]"
上面例子首先使用 Function.prototype.call 函數(shù)指定一個 this 值,然后 .bind 返回一個新的函數(shù),始終將 Object.prototype.toString 設(shè)置為傳入?yún)?shù),其實等價于 Object.prototype.toString.call() 。
實現(xiàn) currying 函數(shù)我們可以理解所謂的柯里化函數(shù),就是封裝「一系列的處理步驟」,通過閉包將參數(shù)集中起來計算,最后再把需要處理的參數(shù)傳進(jìn)去。那如何實現(xiàn) currying 函數(shù)呢?
實現(xiàn)原理就是「用閉包把傳入?yún)?shù)保存起來,當(dāng)傳入?yún)?shù)的數(shù)量足夠執(zhí)行函數(shù)時,就開始執(zhí)行函數(shù)」。上面延遲計算部分已經(jīng)實現(xiàn)了一個簡化版的 currying 函數(shù)。
下面我們來實現(xiàn)一個更加健壯的的 currying 函數(shù)。
// 木易楊
function currying(fn, length) {
length = length || fn.length; // 注釋 1
return function (...args) { // 注釋 2
return args.length >= length // 注釋 3
");this, args) // 注釋 4
: currying(fn.bind(this, ...args), length - args.length) // 注釋 5
}
}
// Test
const fn = currying(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"]
注釋 1:第一次調(diào)用獲取函數(shù) fn 參數(shù)的長度,后續(xù)調(diào)用獲取 fn 剩余參數(shù)的長度
注釋 2:currying 包裹之后返回一個新函數(shù),接收參數(shù)為 ...args
注釋 3:新函數(shù)接收的參數(shù)長度是否大于等于 fn 剩余參數(shù)需要接收的長度
注釋 4:滿足要求,執(zhí)行 fn 函數(shù),傳入新函數(shù)的參數(shù)
注釋 5:不滿足要求,遞歸 currying 函數(shù),新的 fn 為 bind 返回的新函數(shù)(bind 綁定了 ...args 參數(shù),未執(zhí)行),新的 length 為 fn 剩余參數(shù)的長度
上面使用的是 ES5 和 ES6 的混合語法,那我不想使用 call/apply/bind 這些方法呢,自然是可以的,看下面的 ES6 極簡寫法,更加簡潔也更加易懂。
// 參考自 segmentfault 的@大笑平
const currying = fn =>
judge = (...args) =>
args.length >= fn.length
");(...arg) => judge(...args, ...arg)
// Test
const fn = currying(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"]
如果你還無法理解,看完下面例子你就更加容易理解了,要求實現(xiàn)一個 add 方法,需要滿足如下預(yù)期。
add(1, 2, 3) // 6
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2, 3) // 6
我們可以看到,計算結(jié)果就是所有參數(shù)的和,如果我們分兩次調(diào)用時 add(1)(2),可以寫出如下代碼。
function add(a) {
return function(b) {
return a + b;
}
}
add(1)(2) // 3
add 方法第一次調(diào)用后返回一個新函數(shù),通過閉包保存之前的參數(shù),第二次調(diào)用時滿足參數(shù)長度要求然后執(zhí)行函數(shù)。
如果分三次調(diào)用時 add(1)(2)(3),可以寫出如下代碼。
function add(a) {
return function(b) {
return function (c) {
return a + b + c;
}
}
}
console.log(add(1)(2)(3)); // 6
前面兩次調(diào)用每次返回一個新函數(shù),第三次調(diào)用后滿足參數(shù)長度要求然后執(zhí)行函數(shù)。
這時候我們再來看 currying 實現(xiàn)函數(shù),其實就是判斷當(dāng)前參數(shù)長度夠不夠,參數(shù)夠了就立馬執(zhí)行,不夠就返回一個新函數(shù),這個新函數(shù)并不執(zhí)行,并且通過 bind 或者閉包保存之前傳入的參數(shù)。
// 注釋同上
function currying(fn, length) {
length = length || fn.length;
return function (...args) {
return args.length >= length
");this, args)
: currying(fn.bind(this, ...args), length - args.length)
}
}
擴(kuò)展:函數(shù)參數(shù) length
函數(shù) currying 的實現(xiàn)中,使用了 fn.length 來表示函數(shù)參數(shù)的個數(shù),那 fn.length 表示函數(shù)的所有參數(shù)個數(shù)嗎?并不是。
函數(shù)的 length 屬性獲取的是形參的個數(shù),但是形參的數(shù)量不包括剩余參數(shù)個數(shù),而且僅包括第一個具有默認(rèn)值之前的參數(shù)個數(shù),看下面的例子。
((a, b, c) => {}).length;
// 3
((a, b, c = 3) => {}).length;
// 2
((a, b = 2, c) => {}).length;
// 1
((a = 1, b, c) => {}).length;
// 0
((...args) => {}).length;
// 0
const fn = (...args) => {
console.log(args.length);
}
fn(1, 2, 3)
// 3
所以在柯里化的場景中,不建議使用 ES6 的函數(shù)參數(shù)默認(rèn)值。
const fn = currying((a = 1, b, c) => {
console.log([a, b, c]);
});
fn();
// [1, undefined, undefined]
fn()(2)(3);
// Uncaught TypeError: fn(...) is not a function
我們期望函數(shù) fn 輸出 [1, 2, 3],但是實際上調(diào)用柯里化函數(shù)時 ((a = 1, b, c) => {}).length === 0,所以調(diào)用 fn() 時就已經(jīng)執(zhí)行并輸出了 [1, undefined, undefined],而不是理想中的返回閉包函數(shù),所以后續(xù)調(diào)用 fn()(2)(3) 將會報錯。
小結(jié)我們通過定義認(rèn)識了什么是柯里化函數(shù),并且介紹了三種實際的應(yīng)用場景:延遲計算、動態(tài)創(chuàng)建函數(shù)、參數(shù)復(fù)用,然后實現(xiàn)了強(qiáng)大的通用化 currying 函數(shù),不過更像是柯里化 (currying) 和偏函數(shù) (partial application) 的綜合應(yīng)用,并且在最后介紹了函數(shù)的 length,算是意外之喜。
定義:柯里化是一種將使用多個參數(shù)的函數(shù)轉(zhuǎn)換成一系列使用一個參數(shù)的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)
實際應(yīng)用
延遲計算:部分求和、bind 函數(shù)
動態(tài)創(chuàng)建函數(shù):添加監(jiān)聽 addEvent、惰性函數(shù)
參數(shù)復(fù)用:Function.prototype.call.bind(Object.prototype.toString)
實現(xiàn) currying 函數(shù):用閉包把傳入?yún)?shù)保存起來,當(dāng)傳入?yún)?shù)的數(shù)量足夠執(zhí)行函數(shù)時,就開始執(zhí)行函數(shù)
函數(shù)參數(shù) length:獲取的是形參的個數(shù),但是形參的數(shù)量不包括剩余參數(shù)個數(shù),而且僅包括第一個具有默認(rèn)值之前的參數(shù)個數(shù)
參考資料文章穿梭機(jī)JavaScript 專題之函數(shù)柯里化
JavaScript 專題之惰性函數(shù)
柯里化在工程中有什么好處");
【進(jìn)階 6-1 期】JavaScript 高階函數(shù)淺析
【進(jìn)階 5-3 期】深入探究 Function & Object 雞蛋問題
【進(jìn)階 5-2 期】圖解原型鏈及其繼承優(yōu)缺點
【進(jìn)階 5-1 期】重新認(rèn)識構(gòu)造函數(shù)、原型和原型鏈
交流進(jìn)階系列文章匯總?cè)缦拢X得不錯點個Star,歡迎 加群 互相學(xué)習(xí)。
github.com/yygmind/blo…
我是木易楊,公眾號「高級前端進(jìn)階」作者,跟著我每周重點攻克一個前端面試重難點。接下來讓我?guī)阕哌M(jìn)高級前端的世界,在進(jìn)階的路上,共勉!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/6862.html
摘要:引言上一節(jié)我們詳細(xì)聊了聊高階函數(shù)之柯里化,通過介紹其定義和三種柯里化應(yīng)用,并在最后實現(xiàn)了一個通用的函數(shù)。第二種方案來實現(xiàn)也存在一個問題,因為定時器是延遲執(zhí)行的,所以事件停止觸發(fā)時必然會響應(yīng)回調(diào),所以時無法生效。 引言 上一節(jié)我們詳細(xì)聊了聊高階函數(shù)之柯里化,通過介紹其定義和三種柯里化應(yīng)用,并在最后實現(xiàn)了一個通用的 currying 函數(shù)。這一小節(jié)會繼續(xù)之前的篇幅聊聊函數(shù)節(jié)流 thrott...
摘要:在嚴(yán)格模式下調(diào)用函數(shù)則不影響默認(rèn)綁定?;卣{(diào)函數(shù)丟失綁定是非常常見的。因為直接指定的綁定對象,稱之為顯示綁定。調(diào)用時強(qiáng)制把的綁定到上顯示綁定無法解決丟失綁定問題。 (關(guān)注福利,關(guān)注本公眾號回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第三期,本周的主題是this全面解析,今天是第9天。 本計劃一共28期,每期重點攻克一個面試重...
摘要:函數(shù)式編程,一看這個詞,簡直就是學(xué)院派的典范。所以這期周刊,我們就重點引入的函數(shù)式編程,淺入淺出,一窺函數(shù)式編程的思想,可能讓你對編程語言的理解更加融會貫通一些。但從根本上來說,函數(shù)式編程就是關(guān)于如使用通用的可復(fù)用函數(shù)進(jìn)行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數(shù)式編程(Functional Programming),一...
摘要:在前端基礎(chǔ)進(jìn)階八深入詳解函數(shù)的柯里化一文中,我有分享柯里化相關(guān)的知識。雖然說高階組件與柯里化都屬于比較難以理解的知識點,但是他們組合在一起使用時并沒有新增更多的難點。 可能看過我以前文章的同學(xué)應(yīng)該會猜得到當(dāng)我用New的方法來舉例學(xué)習(xí)高階組件時,接下來要分享的就是柯里化了。高階組件與函數(shù)柯里化的運用是非常能夠提高代碼逼格的技巧,如果你有剩余的精力,完全可以花點時間學(xué)習(xí)一下。 在前端基礎(chǔ)進(jìn)...
摘要:返回的綁定函數(shù)也能使用操作符創(chuàng)建對象這種行為就像把原函數(shù)當(dāng)成構(gòu)造器,提供的值被忽略,同時調(diào)用時的參數(shù)被提供給模擬函數(shù)。 bind() bind() 方法會創(chuàng)建一個新函數(shù),當(dāng)這個新函數(shù)被調(diào)用時,它的 this 值是傳遞給 bind() 的第一個參數(shù),傳入bind方法的第二個以及以后的參數(shù)加上綁定函數(shù)運行時本身的參數(shù)按照順序作為原函數(shù)的參數(shù)來調(diào)用原函數(shù)。bind返回的綁定函數(shù)也能使用 n...
閱讀 858·2021-11-24 10:44
閱讀 2794·2021-11-11 16:54
閱讀 3203·2021-10-08 10:21
閱讀 2105·2021-08-25 09:39
閱讀 2915·2019-08-30 15:56
閱讀 3467·2019-08-30 13:46
閱讀 3504·2019-08-23 18:09
閱讀 2096·2019-08-23 17:05