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