高階函數(shù)應(yīng)用 —— 柯里化與反柯里化

參考https://www.pandashen.com/2018/06/23/20180623084025/

前言

在 JavaScript 中劣砍,柯里化和反柯里化是高階函數(shù)的一種應(yīng)用捎迫,在這之前我們應(yīng)該清楚什么是高階函數(shù)凰萨,通俗的說环疼,函數(shù)可以作為參數(shù)傳遞到函數(shù)中聪姿,這個作為參數(shù)的函數(shù)叫回調(diào)函數(shù)哎垦,而擁有這個參數(shù)的函數(shù)就是高階函數(shù)努咐,回調(diào)函數(shù)在高階函數(shù)中調(diào)用并傳遞相應(yīng)的參數(shù)角虫,在高階函數(shù)執(zhí)行時,由于回調(diào)函數(shù)的內(nèi)部邏輯不同委造,高階函數(shù)的執(zhí)行結(jié)果也不同戳鹅,非常靈活,也被叫做函數(shù)式編程昏兆。

柯里化

在 JavaScript 中枫虏,函數(shù)柯里化是函數(shù)式編程的重要思想,也是高階函數(shù)中一個重要的應(yīng)用爬虱,其含義是給函數(shù)分步傳遞參數(shù)隶债,每次傳遞部分參數(shù),并返回一個更具體的函數(shù)接收剩下的參數(shù)饮潦,這中間可嵌套多層這樣的接收部分參數(shù)的函數(shù)燃异,直至返回最后結(jié)果。

1继蜡、最基本的柯里化拆分

// 原函數(shù)
function add(a, b, c) {
    return a + b + c;
}

// 柯里化函數(shù)
function addCurrying(a) {
    return function (b) {
        return function (c) {
            return a + b + c;
        }
    }
}

// 調(diào)用原函數(shù)
add(1, 2, 3); // 6

// 調(diào)用柯里化函數(shù)
addCurrying(1)(2)(3) // 6

被柯里化的函數(shù) addCurrying 每次的返回值都為一個函數(shù),并使用下一個參數(shù)作為形參逛腿,直到三個參數(shù)都被傳入后稀并,返回的最后一個函數(shù)內(nèi)部執(zhí)行求和操作,其實是充分的利用了閉包的特性來實現(xiàn)的单默。

2碘举、柯里化通用式

上面的柯里化函數(shù)沒涉及到高階函數(shù),也不具備通用性搁廓,無法轉(zhuǎn)換形參個數(shù)任意或未知的函數(shù)引颈,我們接下來封裝一個通用的柯里化轉(zhuǎn)換函數(shù),可以將任意函數(shù)轉(zhuǎn)換成柯里化境蜕。

柯里化通用式 ES5

function currying(func, args) {
    // 形參個數(shù)
    var arity = func.length;
    // 上一次傳入的參數(shù)
    var args = args || [];

    return function () {
        // 將參數(shù)轉(zhuǎn)化為數(shù)組
        var _args = [].slice.call(arguments);

        // 將上次的參數(shù)與當(dāng)前參數(shù)進行組合并修正傳參順序
        Array.prototype.unshift.apply(_args, args);

        // 如果參數(shù)不夠蝙场,返回閉包函數(shù)繼續(xù)收集參數(shù)
        if(_args.length < arity) {
            return currying.call(null, func, _args);
        }

        // 參數(shù)夠了則直接執(zhí)行被轉(zhuǎn)化的函數(shù)
        return func.apply(null, _args);
    }
}

//test
currying(add, [1,2,3])()
currying(add, [2,3])(1)
currying(add, [3])(1)(2)
currying(add, [])(1)(2)(3)
currying(add)(1)(2)(3)

上面主要使用的是 ES5 的語法來實現(xiàn),大量的使用了 call 和 apply粱年,下面我們通過 ES6 的方式實現(xiàn)功能完全相同的柯里化轉(zhuǎn)換通用式售滤。

柯里化通用式 ES6

function currying(func, args = []) {
    let arity = func.length;

    return function (..._args) {
        _args.unshift(...args);

        if(_args.length < arity) {
            return currying(func, _args);
        }

        return func(..._args);
    }
}

函數(shù) currying 算是比較高級的轉(zhuǎn)換柯里化的通用式,可以隨意拆分參數(shù)台诗,假設(shè)一個被轉(zhuǎn)換的函數(shù)有多個形參完箩,我們可以在任意環(huán)節(jié)傳入任意個數(shù)的參數(shù)進行拆分,舉一個例子拉队,假如 5 個參數(shù)弊知,第一次可以傳入 2 個,第二次可以傳入 1 個, 第三次可以傳入剩下的粱快,也有其他的多種傳參和拆分方案秩彤,因為在 currying 內(nèi)部收集參數(shù)的同時按照被轉(zhuǎn)換函數(shù)的形參順序進行了更正叔扼。

柯里化的一個很大的好處是可以幫助我們基于一個被轉(zhuǎn)換函數(shù),通過對參數(shù)的拆分實現(xiàn)不同功能的函數(shù)呐舔,如下面的例子币励。

柯里化通用式應(yīng)用 —— 普通函數(shù)

// 被轉(zhuǎn)換函數(shù),用于檢測傳入的字符串是否符合正則表達式
function checkFun(reg, str) {
    return reg.test(str);
}

// 轉(zhuǎn)換柯里化
const check = currying(checkFun);

// 產(chǎn)生新的功能函數(shù)
const checkPhone = check(/^1[34578]\d{9}$/);
const checkEmail = check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

上面的例子根據(jù)一個被轉(zhuǎn)換的函數(shù)通過轉(zhuǎn)換變成柯里化函數(shù)珊拼,并用 check 變量接收食呻,以后每次調(diào)用 check 傳遞不同的正則就會產(chǎn)生一個檢測不同類型字符串的功能函數(shù)。

這種使用方式同樣適用于被轉(zhuǎn)換函數(shù)是高階函數(shù)的情況澎现,比如下面的例子仅胞。

柯里化通用式應(yīng)用 —— 高階函數(shù)

// 被轉(zhuǎn)換函數(shù),按照傳入的回調(diào)函數(shù)對傳入的數(shù)組進行映射
function mapFun(func, array) {
    return array.map(func);
}

// 轉(zhuǎn)換柯里化
const getNewArray = currying(mapFun);

// 產(chǎn)生新的功能函數(shù)
const createPercentArr = getNewArray(item => `${item * 100}%`);
const createDoubleArr = getNewArray(item => item * 2);

// 使用新的功能函數(shù)
let arr = [1, 2, 3, 4, 5];
let percentArr = createPercentArr(arr); // ['100%', '200%', '300%', '400%', '500%',]
let doubleArr = createDoubleArr(arr); // [2, 4, 6, 8, 10]

3剑辫、柯里化與 bind

bind 方法是經(jīng)常使用的一個方法干旧,它的作用是幫我們將調(diào)用 bind 函數(shù)內(nèi)部的上下文對象 this 替換成我們傳遞的第一個參數(shù),并將后面其他的參數(shù)作為調(diào)用 bind 函數(shù)的參數(shù)妹蔽。

bind 方法原理模擬

// bind 方法的模擬
Function.prototype.bind = function (context) {
    var self = this;
    var args = [].slice.call(arguments, 1);

    return function () {
        return self.apply(context, args);
    }
}

通過上面代碼可以看出椎眯,其實 bind 方法就是一個柯里化轉(zhuǎn)換函數(shù),將調(diào)用 bind 方法的函數(shù)進行轉(zhuǎn)換胳岂,即通過閉包返回一個柯里化函數(shù)编整,執(zhí)行該柯里化函數(shù)的時候,借用 apply 將調(diào)用 bind 的函數(shù)的執(zhí)行上下文轉(zhuǎn)換成了 context 并執(zhí)行乳丰,只是這個轉(zhuǎn)換函數(shù)沒有那么復(fù)雜掌测,沒有進行參數(shù)拆分,而是函數(shù)在調(diào)用的時候傳入了所有的參數(shù)产园。


反柯里化

反柯里化的思想與柯里化正好相反汞斧,如果說柯里化的過程是將函數(shù)拆分成功能更具體化的函數(shù),那反柯里化的作用則在于擴大函數(shù)的適用性什燕,使本來作為特定對象所擁有的功能函數(shù)可以被任意對象所使用粘勒。

1、反柯里化通用式

反柯里化通用式的參數(shù)為一個希望可以被其他對象調(diào)用的方法或函數(shù)秋冰,通過調(diào)用通用式返回一個函數(shù)仲义,這個函數(shù)的第一個參數(shù)為要執(zhí)行方法的對象,后面的參數(shù)為執(zhí)行這個方法時需要傳遞的參數(shù)剑勾。

反柯里化通用式 ES5

function uncurring(fn) {
    return function () {
        // 取出要執(zhí)行 fn 方法的對象埃撵,同時從 arguments 中刪除
        var obj = [].shift.call(arguments);
        return fn.apply(obj, arguments);
    }
}

反柯里化通用式 ES6

function uncurring(fn) {
    return function (...args) {
        return fn.call(...args);
    }
}

下面我們通過一個例子來感受一下反柯里化的應(yīng)用。

反柯里化通用式應(yīng)用

// 構(gòu)造函數(shù) F
function F() {}

// 拼接屬性值的方法
F.prototype.concatProps = function () {
    let args = Array.from(arguments);
    return args.reduce((prev, next) => `${this[prev]}&${this[next]}`);
}

// 使用 concatProps 的對象
let obj = {
    name: "Panda",
    age: 16
};

// 使用反柯里化進行轉(zhuǎn)化
const concatProps = uncurring(F.prototype.concatProps);

concatProps(obj, "name", "age"); // Panda&16

反柯里化還有另外一個應(yīng)用虽另,用來代替直接使用 call 和 apply暂刘,比如檢測數(shù)據(jù)類型的 Object.prototype.toString 等方法,以往我們使用時是在這個方法后面直接調(diào)用 call 更改上下文并傳參捂刺,如果項目中多處需要對不同的數(shù)據(jù)類型進行驗證是很麻的谣拣,常規(guī)的解決方案是封裝成一個檢測數(shù)據(jù)類型的模塊募寨。

檢測數(shù)據(jù)類型常規(guī)方案

function checkType(val) {
    return Object.prototype.toString.call(val);
}

如果需要這樣封裝的功能很多就麻煩了,代碼量也會隨之增大森缠,其實我們也可以使用另一種解決方案拔鹰,就是利用反柯里化通用式將這個函數(shù)轉(zhuǎn)換并將返回的函數(shù)用變量接收,這樣我們只需要封裝一個 uncurring 通用式就可以了贵涵。

反柯里化創(chuàng)建檢測類型函數(shù)

const checkType = uncurring(Object.prototype.toString);

checkType(1); // [object Number]
checkType("hello"); // [object String]
checkType(true); // [object Boolean]

2列肢、通過函數(shù)調(diào)用生成反柯里化函數(shù)

在 JavaScript 我們經(jīng)常使用面向?qū)ο蟮木幊谭绞剑趦蓚€類或構(gòu)造函數(shù)之間建立聯(lián)系實現(xiàn)繼承宾茂,如果我們對繼承的需求僅僅是希望一個構(gòu)造函數(shù)的實例能夠使用另一個構(gòu)造函數(shù)原型上的方法瓷马,那進行繁瑣的繼承很浪費,簡單的繼承父子類的關(guān)系又不那么的優(yōu)雅跨晴,還不如之間不存在聯(lián)系欧聘。

將反柯里化方法擴展到函數(shù)原型

Function.prototype.uncurring = function () {
    var self = this;
    return function () {
        return Function.prototype.call.apply(self, arguments);
    }
}

之前的問題通過上面給函數(shù)擴展的 uncurring 方法完全得到了解決,比如下面的例子端盆。

函數(shù)應(yīng)用反柯里化原型方法

// 構(gòu)造函數(shù)
function F() {}

F.prototype.sayHi = function () {
    return "I'm " + this.name + ", " + this.age + " years old.";
}

// 希望 sayHi 方法被任何對象使用
sayHi = F.prototype.sayHi.uncurring();

sayHi({ name: "Panda", age: 20}); // I'm Panda, 20 years old.

在 Function 的原型對象上擴展的 uncurring 中怀骤,難點是理解 Function.prototype.call.apply,我們知道在 call 的源碼邏輯中 this 指的是調(diào)用它的函數(shù)焕妙,在 call 內(nèi)部用第一個參數(shù)替換了這個函數(shù)中的 this晒喷,其余作為形參執(zhí)行了函數(shù)。

而在 Function.prototype.call.apply 中 apply 的第一個參數(shù)更換了 call 中的 this访敌,這個用于更換 this 的就是例子中調(diào)用 uncurring 的方法 F.prototype.sayHi,所以等同于 F.prototype.sayHi.call衣盾,arguments 內(nèi)的參數(shù)會傳入 call 中寺旺,而 arguments 的第一項正是用于修改 F.prototype.sayHi 中 this 的對象。

總結(jié)

看到這里你應(yīng)該對柯里化和反柯里化有了一個初步的認識了势决,但要熟練的運用在開發(fā)中阻塑,還需要我們更深入的去了解它們內(nèi)在的含義。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末果复,一起剝皮案震驚了整個濱河市陈莽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌虽抄,老刑警劉巖走搁,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異迈窟,居然都是意外死亡私植,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門车酣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來曲稼,“玉大人索绪,你說我怎么就攤上這事∑肚模” “怎么了瑞驱?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長窄坦。 經(jīng)常有香客問我唤反,道長,這世上最難降的妖魔是什么嫡丙? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任拴袭,我火速辦了婚禮,結(jié)果婚禮上曙博,老公的妹妹穿的比我還像新娘拥刻。我一直安慰自己,他們只是感情好父泳,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布般哼。 她就那樣靜靜地躺著,像睡著了一般惠窄。 火紅的嫁衣襯著肌膚如雪蒸眠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天杆融,我揣著相機與錄音楞卡,去河邊找鬼。 笑死脾歇,一個胖子當(dāng)著我的面吹牛蒋腮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播藕各,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼池摧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了激况?” 一聲冷哼從身側(cè)響起作彤,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎乌逐,沒想到半個月后竭讳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡黔帕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年代咸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片成黄。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡呐芥,死狀恐怖逻杖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情思瘟,我是刑警寧澤荸百,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站滨攻,受9級特大地震影響够话,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜光绕,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一女嘲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诞帐,春花似錦欣尼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至慧起,卻和暖如春菇晃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蚓挤。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工磺送, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人灿意。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓册着,卻偏偏與公主長得像,于是被迫代替她去往敵國和親脾歧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 柯里化是函數(shù)的一個高級應(yīng)用演熟,想要理解它并不簡單鞭执。因此我一直在思考應(yīng)該如何更加表達才能讓大家理解起來更加容易。 通過...
    這波能反殺閱讀 68,835評論 116 318
  • 函數(shù)和對象 1芒粹、函數(shù) 1.1 函數(shù)概述 函數(shù)對于任何一門語言來說都是核心的概念兄纺。通過函數(shù)可以封裝任意多條語句,而且...
    道無虛閱讀 4,543評論 0 5
  • 昨天在超市買完東西排隊時聽到不遠處一個媽媽訓(xùn)斥自己的孩子:“今天什么都不給你疙赠!不行付材!就是不買!”我沒聽到孩子的聲音...
    梅子Mey閱讀 1,561評論 1 3
  • 你有沒有這樣過,或者有沒有想這樣過圃阳,人生就是這樣厌衔,體驗人生百態(tài),品味酸甜苦辣捍岳,人生中的每一次經(jīng)歷都是收獲富寿,都是回贈...
    醉田園閱讀 205評論 0 0
  • 楊貴云焦點43期堅持原創(chuàng)分享第171天平頂山 大部分的孩子們明天就要開始期末考試了,那么作為家長的考時應(yīng)該怎么做呢...
    舒靜心閱讀 85評論 0 0