前言
在 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)在的含義。