add(1)(2)(3)(4)() == 10

最近面試的時候遇到了一個面試題: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)為有贅述的意義)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容