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

最近面試的時候遇到了一個面試題:add(1)(2)(3)(4)輸出結果為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

看到這道面試題的時候,有點迷茫,不知所措~~~

就像是寶強在《人在囧途》中的反應:啥!啥!啥!這寫的都是啥?


一開始,我發現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呢~

沒有思路~

面試結束后,在網上找相關的知識點學習,了解到一個概念叫:函數柯里化


什么是函數柯里化(curry)

函數柯里化(curry)是函數式編程里面的概念。

curry的概念很簡單:只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數

簡單點來說就是:每次調用函數時,它只接受一部分參數,并返回一個函數,直到傳遞所有參數為止。

舉個?? 將下面接受兩個參數的函數改為接受一個參數的函數。

const add = (x, y) => x + y;
add(1, 2);

改成每次只接受一個參數的函數

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));

看起來并不是那么難,但是如果面試官的要求是實現一個add 函數,同時支持下面這幾種的用法呢

add(1, 2, 3);
add(1, 2)(3);
add(1)(2, 3);

如果還是按照上面的這種思路,我們是不是要寫很多種呢...

我們當然可以自己實現一個工具函數專門來生成柯里化函數。

主要思路是什么呢,要判斷當前傳入函數的參數個數 (args.length) 是否大于等于原函數所需參數個數 (fn.length) ,如果是,則執行當前函數;如果是小于,則返回一個函數。

const curry = (fn, ...args) => 
    // 函數的參數個數可以直接通過函數數的.length屬性來訪問
    args.length >= fn.length // 這個判斷很關鍵!!!
    // 傳入的參數大于等于原始函數fn的參數個數,則直接執行該函數
    ? fn(...args)
    /**
     * 傳入的參數小于原始函數fn的參數個數時
     * 則繼續對當前函數進行柯里化,返回一個接受所有參數(當前參數和剩余參數) 的函數
    */
    : (..._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個作用: 參數復用提前返回延遲執行

我們來簡單的解釋一下: 參數復用:拿上面 f這個函數舉例,只要傳入一個參數 z,執行,計算結果就是 1 + 2 + z 的結果,1 和 2 這兩個參數就直接可以復用了。

提前返回 和 延遲執行 也很好理解,因為每次調用函數時,它只接受一部分參數,并返回一個函數(提前返回),直到(延遲執行)傳遞所有參數為止。


函數柯里化解決方案

函數柯里化有兩種不同的場景,一種為函數參數個數定長的函數,另外一種為函數參數個數不定長的函數。

函數參數個數定長的柯里化解決方案
// 定長參數
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) {
        // 合并參數
        args = [
            ...args,
            ...newArgs
        ]
        // 判斷當前參數集合args的長度是否 < 目標函數fn的需求參數長度
        if (args.length < len) {
            // 繼續返回函數
            return _c
        } else {
            // 返回執行結果
            return fn.apply(this, args.slice(0, len))
        }
    }
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3)(4) // 同時支持addCurry(1)(2, 3)(4)該方式調用
console.log(total) // 10
函數參數個數不定長的柯里化解決方案

問題升級:那這個問題再升級一下,函數的參數個數不確定時,如何實現呢?

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)
// 注意調用方式的變化
console.log(addCurry(1)(2)(3)(4, 5)())

函數柯里化是一種技術,一種將多入參函數變成單入參函數

這樣做會讓函數變得更復雜,但同時也提升了函數的普適性。

舉個例子

//正常函數
function sum(a,b){
  console.log(a+b); 
}

sum(1,2);    //輸出3
sum(1,3);    //輸出4

//柯里化函數
function curry(a){
    return (b) =>{
        console.log(a+b)
    } 
}

const sum = curry(1);

sum(2);  //輸出3
sum(3);  //輸出4

例子里,為使用柯里化的函數在入參的時候即使在某一個入參是固定的情況下。也需要一樣的去輸入,那么這個輸入就變得冗余了。

柯里化之后的函數可以省略掉一個固定的入參。

但到這里,還有一個問題。現在只是一層封裝的柯里化。如果是四層,五層呢。

假設有這樣一個場景

//柯里化之前
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);

多層柯里化的時候代碼會不美觀,可讀性非常差。

但需求總是在的。我們總會需要多層柯里化的時候。

所有,我們可以封裝一個函數來幫助我們完成函數向柯里化的轉換。

 //函數柯里化封裝(這個封裝可以直接復制走使用)
    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);
                }
            }
        }
        
        //需要被柯里化的函數
        function multiFn(a, b, c) {
            return a * b * c;
        }
        
        //multi是柯里化之后的函數
        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));

柯里化的應用場景

其實柯里化大多是情況下是為了減少重復傳遞的不變參數。

舉個最簡單的例子吧。手機號正則校驗。

//校驗手機號
function validatePhone(regExp,warn,phone){
  const reg = regExp;
  if (phone && reg.test(phone) === false) {
    return Promise.reject(warn);
  }
  return Promise.resolve();
}

//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",187****3311)

這種寫法乍一看好像沒什么問題。但是,如果你需要多次調用呢?

//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",137****1234)
//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",159****6204)
//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",137****2125)
//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",191****5236)

會發現,正則和提示入參是固定的。很冗余。

我們可以使用我們上面封裝的柯里化工具(curry函數)進行如下修改。

//完成柯里化
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}$/,"手機號格式不符");

//調用柯里化之后的函數
validatePhoneCurry(159****6204);
validatePhoneCurry(137****1234);
validatePhoneCurry(137****2125);
validatePhoneCurry(191****5236);

如上,我們可以省略很多不必要的參數。

當然,柯里化的應用場景還有延時執行(閉包也可以實現,而且更簡單)還有提前返回(主要針對IE,IE也馬上退休了,這里不認為有贅述的意義)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容