你不知道的JS專欄 - 函數柯里化(柯里化含義, 作用, 寫法, 大廠面試題)

你不知道的JS - 函數柯里化

目錄:

  • 柯里化函數的概覽

  • 手撕柯里化

    • 阿里面試題

    • 柯里化的寫法

  • 柯里化函數的好處和作用

    • 參數複用

    • 延遲執行

    • 柯里化的應用舉例

柯里化函數的概覽

(如果你已經對柯里化有了一個比較好的瞭解, 也知道柯里化的寫法, 只是對柯里化的作用和應用場景抱有一定的疑問的話, 我建議你直接看第三節柯里化的作用)

這哥們就是說讓你把本來一次性傳幾個參數的函數分成好幾次傳參的方式最終執行

官方定義: 將使用多個參數的函數轉換成一系列使用一個參數的函數的技術

這到底是啥意思呢看個例子你就明白了

function add(x, y, z) {
    return x + y + z; 
}

// 正常調用我們是
add(1, 2, 3); // 輸出6

// 就上面這個累加函數, 我們用柯里化函數來包裝一下他然後他就可以像下面這樣執行
function curry(func) {}; // 假裝柯里化函數已經寫好了
add = curry(add); // 轉換一下
add(1)(2)(3); // 輸出6

很多朋友會說我爲什麼要進行柯里化啊, 跟神經一樣的操作, 去百度搜一個柯里化到底有什麼用給我出一大堆博客, 博客一打開就是維基百科說柯里化是xxxxx, 半天沒看明白到底爲什麼用柯里化

其實用柯里化可以有幾個好處, 由於我們這裏還沒有正兒八經的學習柯里化, 我先給你滲透一個好處

你給我仔細想想Object.prototype.bind方法, 沒錯就是改變this指向的這哥們, 你有沒有發現這哥們給你來了
一個神奇的操作

call和apply都給你一調用就執行了, 而bind卻是返回給你一個新的函數, 等你想什麼時候執行再去執行, 你仔細想想bind不就是柯里化嗎, 柯里化的定義是什麼, 把本來一次性要傳完的參數分成幾次傳遞, 你看我下面的實例newFoo是不是傳了兩次參數纔出的結果, 所以bind就是柯里化函數

通過這裏, 我想你應該可以發現柯里化的第一個好處就是可以延遲函數的運行時間, 不用馬上就執行

function foo(x, y, z) { console.log(this.name); console.log(x, y, z)};
foo.call(window, 2 ,2 ,2); // '' 2, 2, 2 
foo.apply({name: 'loki'}, [3, 3, 3]); // loki, 3, 3, 3

let newFoo = foo.bind({name:'thor'}, 1); 
newFoo(2, 3); // thor [1, 2, 3]

到這你看不懂不能理解都沒關係, 但是這只是個概覽, 讓你混個眼熟, 我希望你往下看

手撕柯里化

OK經過上面我相信你已經基本認識柯里化到底他的官方定義是什麼了, 但是即使我給你列舉了bind的例子, 你可能還是無法理解柯里化, 因爲你根本不知道他到底是爲了解決什麼問題的, 即使是bind他又是解決什麼問題呢? 我們爲什麼要延遲函數執行時間呢? 小小的腦袋裝着大大的問號, 這一塊我用一些實例來告訴你柯里化到底會應用於哪些場景, 及有些操作爲什麼要這麼做, 在這一節裏我給你答案

要了解柯里化, 我們至少要先知道柯里化怎麼寫

  • 阿里面試題

    看看阿里2017年的秋招面試題,如何實現一個add(1)(2)(3) = 6

    實現add(1)(2)(3)我們知道這個add函數被分開調用了三次,每次傳入一個參數, 當第三個參數傳入的時候,返回了結果, 那麼我們其實可以得出幾點結論:

    1. 用過jquery我們就知道, 如果想要這樣進行鏈式操作, 勢必在每單次執行以後返回了這個函數本身

    2. 當add(1)執行的時候函數沒有計算值出來, 但是1這個參數卻在本次執行中被保留了下來並且在最後一次3傳入的時候被拿出來進行累加, 那麼函數執行完畢作用域銷燬, 這個1跟2應該是跳出了函數作用域進行了生命週期的延長, 所以肯定進行了閉包操作

    3. 能夠恰好在第三個函數傳遞進來就真正得到執行, 那麼內部肯定對實參的個數進行了計算

    根據這三點, 我們可以摸索着來寫寫

    const add = (function () {
        var _args = []; // 這個_args用來存儲所有傳入的形參
        return function () {
            if (arguments.length) { // 如果形參存在就直接進行循環, 不存在直接返回函數
                for (let i = 0, len = arguments.length; i < len; i++) {
                    _args.push(arguments[i]) // 循環形參, 將他們一一放入_args中
                    console.log(_args);
                    if (_args.length === 3) { 
                        // 但是每添加一次要記得判斷一下_args.length是不是等於3了
                        // 一旦_args等於3了就要真正的去進行計算並且將值返回了
                        let num = 0;
                        _args.forEach(ele => num += ele)
                        return num;
                    }
                }
                return add;
            }
        }
    }())
    
    console.log(add(1)(2)(3)); // 輸出6
    
  • 手寫柯里化
    上面我們確實實現了一個比較簡單的add(1)(2)(3) = 6的效果, 但是我們知道這個是無法複用的, 我們渴望有一個Curry函數, 當我們將一個需要進行柯里化的函數傳遞進Curry函數中後, curry函數會返回一個新的函數給我們, 這個新的函數就可以進行柯里化操作了

    function curry(func) {} // 假設我們已經寫好了curry函數
    
    // 這是正兒八經的累加函數
    function add(x, y, z) {
        return x + y + z; 
    }
    
    // 當我們將add方法傳入curry方法中得到一個新的addCurry時
    // addCurry將可以實現addCurry(1)(2)(3)的效果
    const addCurry = curry(add);
    addCurry(1)(2)(3); // 6
    //我們還可以進行隨意搭配比如
    // addCurry(1)(2,3); // 6
    // addCurry(1, 2)(3); // 6
    

    OK, 那麼這個curry函數我們到底應該怎麼來寫

    1. 首先, 它接收一個函數作爲參數, 最終肯定也是返回了一個函數

    2. 同時傳入的這個函數一定要是固定了形參個數的函數

    function curry(func, argNum) {
            // func: 傳遞進來的要進行柯里化的方法
            // length: 總共有多少個形參
            const length = argNum || func.length; // 直接拿到形參的最大個數
            let _args = []; // 定義的用來緩存每一次傳入的參數數組
            return function () {
                // 本次是否真的傳輸了參數
                if (arguments.length != 0) {
                    console.log(arguments);
                    // 如果真的存在, 那麼我們開始計算
                    for (var i = 0, len = arguments.length; i < len; i++) {
                        _args.push(arguments[i]);
                        if (_args.length === length) {
                            return func(..._args);
                        }
                    }
                }
                // 如果沒有傳輸參數進來或者參數不滿
                return arguments.callee;
            }
        }
    
        function add(x, y, z) {
            return x + y + z;
        }
    
        var newAdd = curry(add);
        // console.log(newAdd(1)(2)(3)); 6
        // console.log(newAdd(1)(2, 3)); 6
        console.log(newAdd(1, 2)(3));  // 6
        
    

    大致操作差不多, 只是我們提取公共的curry方法讓其會更加的靈活

    OK, 關於柯里化的寫法我是已經都寫出來了, 希望你理解它的這個寫法, 我們接下來來看看, 柯里化的作用及在實際項目中的一些使用

柯里化的好處和作用

上面花了大篇幅介紹柯里化, 可能也僅僅是讓大家明白了柯里化的寫法和概念, 而柯里化帶來的作用和好處其實我寫的還不夠詳細, 那麼這裏我將通過一些實例來寫寫柯里化的好處和具體場景

我來寫寫柯里化比較顯著的兩個優勢:

  • 參數複用, 減少重複代碼

    所謂參數複用, 就是緩存之前傳入的參數, 讓他不被釋放掉, 從而不用進行重複傳參, 我們來寫寫

    來看個實例

    // ajax請求大家都玩過, 我這裏不再進行ajax的封裝, 而是假裝自己已經寫好了ajax, 進行請求
    ajax(methods = 'GET', flag, url, data = {},  cb) {}; // 假設已經寫好了
    ajax('POST', true, '/api/getData', {name: 'loki', }, cb1); 
    ajax('POST', true, '/api/getData', {id: '1010', }, cb2); 
    ajax('POST', true, '/api/getTel', {tel: '1234',key: 10 }, cb3); 
    ajax('POST', true, '/api/getTel', {arr: [1, 2, 3] }, cb4);
    
    // 我們總是在進行很多的ajax請求, 上放發送了四次POST請求, 我們明顯發現
    // 代碼的冗餘非常高, 重複代碼非常的多, 特別是那個POST和那個true, 看的人頭大
    // 這時候我們可以使用柯里化來解決
    
    const ajaxCurry = curry(ajax);
    var post = ajaxCurry('POST', true);
    post('/api/getData', {name: 'loki', }, cb1);
    post('/api/getData', {id: '1010', }, cb2);
    post('/api/getTel', {tel: '1234',key: 10 }, cb3);
    post('/api/getTel', {arr: [1, 2, 3] }, cb4);
    
    // 至少我們把POST和true就搞定了, 其實你還可以繼續減少重複度, 
    // 但是我怕有的朋友看不懂了, 所以就不寫了
    
  • 延遲運行

    延遲運行這個看看bind方法就知道了, 我上面應該寫過了

    function foo(x, y, z) { console.log(this.name); console.log(x, y, z)};
    foo.call(window, 2 ,2 ,2); // '' 2, 2, 2 
    foo.apply({name: 'loki'}, [3, 3, 3]); // loki, 3, 3, 3
    
    let newFoo = foo.bind({name:'thor'}, 1); 
    newFoo(2, 3); // thor [1, 2, 3]
    

    bind函數從來不用馬上就被執行, 這給了我們某些操作的空間

  • 柯里化的應用實例

    我們來看一個需求, 我們公司有一個記賬系統, 每天的錢都會入賬, 而公司每個月底都會把賬目列出來, 計算這個月到底入賬了多少錢,這個需求你怎麼寫

    // 我們先來看看普通寫法會有哪些問題

    // 爲了避免全局變量的問題, 我們必須封閉作用域
    const addMoney = (function() {
        let sum = 0; // 初始入賬值爲0
        function caculateMoney(money) {
            sum += money;
            return sum;
        }
        return function(everyDayMoney) {
            return caculateMoney(everyDayMoney); 
        }
    }())
    
    // 然後我們每天都會進行錢的累計, 這種寫法可不可以呢, 確實可以
    // 但是卻有一點的小瑕疵
    // 1. 過多的立即執行函數其實會讓代碼變得難以閱讀
    // 2. 我們其實追求的實際上是月底的那個累加值, 
    // 而當前我們必須每天都必須計算一次運行一次caculate方法
    // 造成了不必要的浪費
    

    柯里化其實可以很好的幫我們解決上面的問題, 柯里化可以進行緩衝, 所以我們不必每天都計算一次money, 我們可以等到每個月的最後一天到來, 再進行最終的計算

    // 柯里化代碼curry如上, 就不在重新書寫了
        function caculateMoney(...moneyArr) {
        console.log(moneyArr);
        let num = 0;
        moneyArr.forEach(ele => num += ele);
        return num;
    }
    
    const caculateMoneyCurry = curry(caculateMoney, 5);
    
    console.log(caculateMoneyCurry(100)(10)(20)(30)(5)); // 假設我們計算5天的工資,這裏會輸出165
    

    代碼經過改寫以後, 相較之前其實代碼的風格已經有很大的改變, 也多了幾點優勢

    1. 代碼的觀賞性很強, 沒有使用過多的立即執行函數, 也很簡潔

    2. 代碼的可讀性變得更強了, 在之前的代碼中, 我們其實比較難知道當月到底計算了多少天的, 而柯里化過後我們可以很明顯的發現我們計算了多少天的入賬金額

    3. 不用頻繁的觸發caculateMoney方法, 節約了部分的性能

    本來想多寫一些實例的, 突然又覺得篇幅太大不太好, 所以這個對比實例就只舉一個了,你在下一節會看到更多的柯里化應用場景

OK, 柯里化函數到此爲止, 希望我寫清楚了, 也希望可以幫助到大家

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章