JavaScript函式柯里化

起風了發表於2019-02-16

JavaScript函式柯里化

一、定義:

柯里化(Currying)是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數且返回結果的新函式的技術。

透過一個簡單的例子解釋一下:

function add(a, b) {
    return a + b
}

add(1, 2); // 3

將函式add轉化為柯里化函式_add

function _add(a){
    return function(b){
        return a + b
    }
}

_add(1)(2);  // 3

函式add和函式_add是等價的。

_add能夠處理add的所有剩餘引數,因此柯里化也被稱為部分求值

實際上就是把add函式的a,b兩個引數變成了先用一個函式接收a然後返回一個函式去處理b引數。

現在思路應該就比較清晰了:只傳遞給函式一部分引數來呼叫,讓它返回一個函式去處理剩下的引數。

二、柯里化函式的作用

1、引數複用

案例:拼接地址

按照普通思路去拼接一個地址

// 拼接地址
function getUrl(protocol, hostname, pathname) {
    return `${protocol}${hostname}${pathname}`;
}

const url1 = getUrl('https://', 'www.baidu.com', '/hasa');
const url2 = getUrl('https://', 'www.zhihu.com', '/saandsa');
const url3 = getUrl('https://', 'www.segmentfault.com', '/hasak');

console.log(url1, url2, url3)

每次呼叫getUrl引數的時候都要重複的傳入引數'https://'。

柯里化封裝之後:

function curry(protocol) {
    return function (hostname, pathname) {
        return `${protocol}${hostname}${pathname}`;
    }
}

const url_curry = curry('https://');

const url1 = url_curry('www.baidu.com', '/hasa');
const url2 = url_curry('www.zhihu.com', '/saandsa');
const url3 = url_curry('www.segmentfault.com', '/hasak');

console.log(url1, url2, url3)

很明顯,經過柯里化封裝之後,之後再進行地址拼接的時候,減少了引數個數,降低了程式碼重複率。

2、提前確認/提前返回

案例:相容IE瀏覽器事件的監聽方法(IE is dead

傳統的方法:

/*
* @param    element       Object      DOM元素物件
* @param    type          String      事件型別
* @param    listener      Function    事件處理函式
* @param    useCapture    Boolean     是否捕獲
*/
var addEvent = function (element, type, listener, useCapture) {
    if (window.addEventListener) {
        element.addEventListener(type, function (e) {
            listener.call(element, e)
        }, useCapture)
    } else {
        element.attachEvent('on' + type, function (e) {
            listener.call(element, e)
        })
    }
}

缺陷就是,每次對DOM元素進行事件繫結時都需要重新進行判斷,其實對於事件監聽網頁一發布瀏覽器已經確定了,就可以知曉瀏覽器到底是需要哪一種監聽方式。所以我們可以讓判斷只執行一次。

柯里化封裝之後:

var addEvent = (function () {
    if (window.addEventListener) {
        return function (element, type, listener, useCapture) { // return funtion
            element.addEventListener(type, function () {
                listener.call(element)
            }, useCapture)
        }
    } else {
        return function (element, type, listener) {
            element.attachEvent('on' + type, function () {
                listener.call(element)
            })
        }
    }
})()

addEvent(element, "click", listener)

立即執行函式,在觸發多次事件也依舊只會觸發一次if條件判斷。

3、延遲執行

案例:釣魚統計重量

傳統的方法:

let fishWeight = 0;
const addWeight = function(weight) {
    fishWeight += weight;
};

addWeight(2.3);
addWeight(6.5);
addWeight(1.2);
addWeight(2.5);

console.log(fishWeight);  

每次執行addWeight方法時,都進行一次魚的體重的加和。

柯里化封裝後:

function curryWeight(fn) {
    let _fishWeight = [];
    return function () {
        if (arguments.length === 0) {
            return fn.apply(null, _fishWeight);
        } else {
            _fishWeight = _fishWeight.concat([...arguments]);
        }
    }
}

function addWeight() {
    let fishWeight = 0;
    for (let i = 0, len = arguments.length; i < len; i++) {
        fishWeight += arguments[i];
    }
    return fishWeight;
}

const _addWeight = curryWeight(addWeight);
_addWeight(6.5);
_addWeight(1.2);
_addWeight(2.3);
_addWeight(2.5);
console.log(_addWeight())

在執行_addWeight方法時,並沒有做魚的體重的加和,之後在最後一次執行_addWeight()時,才做了加和。做到了延遲執行addWeight方法的效果。

-----擴充:對魚的體重進行排序-----

function curryWeight(fn) {
    let _fishWeight = [];
    return function () {
        if (arguments.length === 0) {
            return fn.apply(null, _fishWeight);
        } else {
            _fishWeight = _fishWeight.concat([...arguments]);
        }
    }
}

function sortWeight() {
    return [...arguments].sort((a, b) => a - b);
}

const _addWeight = curryWeight(sortWeight);
_addWeight(6.5);
_addWeight(1.2);
_addWeight(2.3);
_addWeight(2.5);
console.log(_addWeight())

很簡單,只需要修改addWeight函式即可。

三、柯里化函式的實現

Done1:
step1:實現一個函式add(1)(2)

function add(num){
    let sum = num;
    return function(num){
      return sum+num
    }
  }
console.log(add(1)(2));

step2:實現add(1)(2)(3)呢?
不能一直無限巢狀,所以,返回一個函式名,在函式內部判斷是否還有引數傳進來

 function add(num){
    let sum =num;
    return adds = function (num1){
     
      if(num1) {
        sum = sum+num1;
        return adds
      }
      return sum
    }
  }
console.log(add(1)(2)(3)());

step3:如果實現一個add(1,2,3)(2,1)(3)(2,1,3)呢?
因為現在每次呼叫傳入的引數是不定長的。所以並不能直接把形參給寫死。

function add() {
    // 收集引數
    let params = [...arguments];

    const getParams = function () {
        // 當傳入的引數為空時,對引數陣列遍歷求和。
        if ([...arguments].length === 0) return params.reduce((pre, next) => pre + next, 0);
        // 收集引數
        params.push(...arguments);
        return getParams;
    }

    // 重複呼叫,直到傳入引數為空
    return getParams;
}
let a = add(1, 2)(2, 1)(3)();
console.log(a);

Done2:

function curry() {
    let args = [...arguments];
    let inner = function () {
        args.push(...arguments);
        return inner;
    }
    
    // 核心內容:隱式轉換,呼叫了內部的toString
    inner.toString = function () {
        return args.reduce(function (pre, next) {
            return pre + next;
        })
    }
    return inner;
}

const result = curry(1)(2);
console.log(+result);

有關隱式轉換
1、在JavaScript中,toString()方法和valueOf()方法都可以被改寫,如果被用以操作JavaScript解析器就會自動呼叫。所以在柯里化函式中最後重寫toString恰好能滿足在收集完所有引數後再去執行。
2、有關JavaScript隱式呼叫的帖子

四、柯里化總結

函式的柯里化,核心思想是收集引數,需要依賴引數以及遞迴,透過拆分引數的方式,來呼叫一個多引數的函式方法,以達到減少程式碼冗餘,增加可讀性的目的。

優點:

  • 柯里化之後,我們沒有丟失任何引數:log 依然可以被正常呼叫;
  • 我們可以輕鬆地生成偏函式,例如用於生成今天的日誌的偏函式;
  • 入口單一;
  • 易於測試和複用。

缺點

  • 函式巢狀多;
  • 佔記憶體,有可能導致記憶體洩漏(因為本質是配合閉包實現的);
  • 效率差(因為使用遞迴);
  • 變數存取慢,訪問性很差(因為使用了arguments);

應用場景:

  1. 減少重複傳遞不變的部分引數;
  2. 將柯里化後的callback引數傳遞給其他函式。

相關文章