
最近面試的時候遇到了一個面試題:add(1)(2)(3)(4)輸出結(jié)果為10。
const addFn = (...args) => args.reduce((total, cur) => total + cur, 0)
const curry = (fn) => {
// Your Code Here
}
const add = curry(addFn)
const value = add(1,2)(3)(4)()
console.log(value) // 10
看到這道面試題的時候,有點(diǎn)迷茫,不知所措~~~
就像是寶強(qiáng)在《人在囧途》中的反應(yīng):啥!啥!啥!這寫的都是啥?
一開始,我發(fā)現(xiàn)1+2+3+4=10,寫了以下的代碼
function add (a, b, c, d) {
return a + b + c + d
}
add(1, 2, 3, 4) // 10
add(1,2)(3)(4)()//10
function add (a) {
return function (b) {
return function (c) {
return function (d) {
return a + b + c + d
}
}
}
}
又覺得不會這么簡單吧,要是累加到100、1000呢~
沒有思路~
面試結(jié)束后,在網(wǎng)上找相關(guān)的知識點(diǎn)學(xué)習(xí),了解到一個概念叫:函數(shù)柯里化。
什么是函數(shù)柯里化(curry)
函數(shù)柯里化(curry)是函數(shù)式編程里面的概念。
curry的概念很簡單:只傳遞給函數(shù)一部分參數(shù)來調(diào)用它,讓它返回一個函數(shù)去處理剩下的參數(shù)。
簡單點(diǎn)來說就是:每次調(diào)用函數(shù)時,它只接受一部分參數(shù),并返回一個函數(shù),直到傳遞所有參數(shù)為止。
舉個?? 將下面接受兩個參數(shù)的函數(shù)改為接受一個參數(shù)的函數(shù)。
const add = (x, y) => x + y;
add(1, 2);
改成每次只接受一個參數(shù)的函數(shù)
const add = x => y => x + y;
add(1)(2);`
我們可以自己先嘗試寫一個add(1)(2)(3)
const add = x => y => z => x + y + z;
console.log(add(1)(2)(3));
看起來并不是那么難,但是如果面試官的要求是實現(xiàn)一個add 函數(shù),同時支持下面這幾種的用法呢
add(1, 2, 3);
add(1, 2)(3);
add(1)(2, 3);
如果還是按照上面的這種思路,我們是不是要寫很多種呢...
我們當(dāng)然可以自己實現(xiàn)一個工具函數(shù)專門來生成柯里化函數(shù)。
主要思路是什么呢,要判斷當(dāng)前傳入函數(shù)的參數(shù)個數(shù) (args.length) 是否大于等于原函數(shù)所需參數(shù)個數(shù) (fn.length) ,如果是,則執(zhí)行當(dāng)前函數(shù);如果是小于,則返回一個函數(shù)。
const curry = (fn, ...args) =>
// 函數(shù)的參數(shù)個數(shù)可以直接通過函數(shù)數(shù)的.length屬性來訪問
args.length >= fn.length // 這個判斷很關(guān)鍵?。。? // 傳入的參數(shù)大于等于原始函數(shù)fn的參數(shù)個數(shù),則直接執(zhí)行該函數(shù)
? fn(...args)
/**
* 傳入的參數(shù)小于原始函數(shù)fn的參數(shù)個數(shù)時
* 則繼續(xù)對當(dāng)前函數(shù)進(jìn)行柯里化,返回一個接受所有參數(shù)(當(dāng)前參數(shù)和剩余參數(shù)) 的函數(shù)
*/
: (..._args) => curry(fn, ...args, ..._args);
function add1(x, y, z) {
return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
柯里化有什么作用
主要有3個作用: 參數(shù)復(fù)用、提前返回和 延遲執(zhí)行
我們來簡單的解釋一下: 參數(shù)復(fù)用:拿上面 f這個函數(shù)舉例,只要傳入一個參數(shù) z,執(zhí)行,計算結(jié)果就是 1 + 2 + z 的結(jié)果,1 和 2 這兩個參數(shù)就直接可以復(fù)用了。
提前返回 和 延遲執(zhí)行 也很好理解,因為每次調(diào)用函數(shù)時,它只接受一部分參數(shù),并返回一個函數(shù)(提前返回),直到(延遲執(zhí)行)傳遞所有參數(shù)為止。
函數(shù)柯里化解決方案
函數(shù)柯里化有兩種不同的場景,一種為函數(shù)參數(shù)個數(shù)定長的函數(shù),另外一種為函數(shù)參數(shù)個數(shù)不定長的函數(shù)。
函數(shù)參數(shù)個數(shù)定長的柯里化解決方案
// 定長參數(shù)
function add (a, b, c, d) {
return [
...arguments
].reduce((a, b) => a + b)
}
function currying (fn) {
let len = fn.length
let args = []
return function _c (...newArgs) {
// 合并參數(shù)
args = [
...args,
...newArgs
]
// 判斷當(dāng)前參數(shù)集合args的長度是否 < 目標(biāo)函數(shù)fn的需求參數(shù)長度
if (args.length < len) {
// 繼續(xù)返回函數(shù)
return _c
} else {
// 返回執(zhí)行結(jié)果
return fn.apply(this, args.slice(0, len))
}
}
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3)(4) // 同時支持addCurry(1)(2, 3)(4)該方式調(diào)用
console.log(total) // 10
函數(shù)參數(shù)個數(shù)不定長的柯里化解決方案
問題升級:那這個問題再升級一下,函數(shù)的參數(shù)個數(shù)不確定時,如何實現(xiàn)呢?
function add (...args) {
return args.reduce((a, b) => a + b)
}
function currying (fn) {
let args = []
return function _c (...newArgs) {
if (newArgs.length) {
args = [
...args,
...newArgs
]
return _c
} else {
return fn.apply(this, args)
}
}
}
let addCurry = currying(add)
// 注意調(diào)用方式的變化
console.log(addCurry(1)(2)(3)(4, 5)())
函數(shù)柯里化是一種技術(shù),一種將多入?yún)⒑瘮?shù)變成單入?yún)⒑瘮?shù)。
這樣做會讓函數(shù)變得更復(fù)雜,但同時也提升了函數(shù)的普適性。
舉個例子
//正常函數(shù)
function sum(a,b){
console.log(a+b);
}
sum(1,2); //輸出3
sum(1,3); //輸出4
//柯里化函數(shù)
function curry(a){
return (b) =>{
console.log(a+b)
}
}
const sum = curry(1);
sum(2); //輸出3
sum(3); //輸出4
例子里,為使用柯里化的函數(shù)在入?yún)⒌臅r候即使在某一個入?yún)⑹枪潭ǖ那闆r下。也需要一樣的去輸入,那么這個輸入就變得冗余了。
柯里化之后的函數(shù)可以省略掉一個固定的入?yún)ⅰ?/p>
但到這里,還有一個問題?,F(xiàn)在只是一層封裝的柯里化。如果是四層,五層呢。
假設(shè)有這樣一個場景
//柯里化之前
function sum(a,b,c,d,e){
console.log(a+b+c+d+e)
}
sum(1,2,3,4,5);
//柯里化
function sum1(a){
return function sum2(b){
return function sum3(c){
return function sum4(d){
return function sum5(e){
console.log(a+b+c+d+e)
}
}
}
}
}
sum1(1)(2)(3)(4)(5);
多層柯里化的時候代碼會不美觀,可讀性非常差。
但需求總是在的。我們總會需要多層柯里化的時候。
所有,我們可以封裝一個函數(shù)來幫助我們完成函數(shù)向柯里化的轉(zhuǎn)換。
//函數(shù)柯里化封裝(這個封裝可以直接復(fù)制走使用)
function curry(fn, args) {
var length = fn.length;
var args = args || [];
return function () {
newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {
return curry.call(this, fn, newArgs);
} else {
return fn.apply(this, newArgs);
}
}
}
//需要被柯里化的函數(shù)
function multiFn(a, b, c) {
return a * b * c;
}
//multi是柯里化之后的函數(shù)
var multi = curry(multiFn);
console.log(multi(2)(3)(4));
console.log(multi(2, 3, 4));
console.log(multi(2)(3, 4));
console.log(multi(2, 3)(4));
柯里化的應(yīng)用場景
其實柯里化大多是情況下是為了減少重復(fù)傳遞的不變參數(shù)。
舉個最簡單的例子吧。手機(jī)號正則校驗。
//校驗手機(jī)號
function validatePhone(regExp,warn,phone){
const reg = regExp;
if (phone && reg.test(phone) === false) {
return Promise.reject(warn);
}
return Promise.resolve();
}
//調(diào)用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機(jī)號格式不符",187****3311)
這種寫法乍一看好像沒什么問題。但是,如果你需要多次調(diào)用呢?
//調(diào)用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機(jī)號格式不符",137****1234)
//調(diào)用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機(jī)號格式不符",159****6204)
//調(diào)用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機(jī)號格式不符",137****2125)
//調(diào)用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機(jī)號格式不符",191****5236)
會發(fā)現(xiàn),正則和提示入?yún)⑹枪潭ǖ?。很冗余?/p>
我們可以使用我們上面封裝的柯里化工具(curry函數(shù))進(jìn)行如下修改。
//完成柯里化
const curryValid = curry(validatePhone);
const validatePhoneCurry =curryValid(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機(jī)號格式不符");
//調(diào)用柯里化之后的函數(shù)
validatePhoneCurry(159****6204);
validatePhoneCurry(137****1234);
validatePhoneCurry(137****2125);
validatePhoneCurry(191****5236);
如上,我們可以省略很多不必要的參數(shù)。
當(dāng)然,柯里化的應(yīng)用場景還有延時執(zhí)行(閉包也可以實現(xiàn),而且更簡單),還有提前返回(主要針對IE,IE也馬上退休了,這里不認(rèn)為有贅述的意義)
