高階函數應用 —— 柯里化與反柯里化


閱讀原文


前言

在 JavaScript 中,柯里化和反柯里化是高階函數的一種應用,在這以前咱們應該清楚什麼是高階函數,通俗的說,函數能夠做爲參數傳遞到函數中,這個做爲參數的函數叫回調函數,而擁有這個參數的函數就是高階函數,回調函數在高階函數中調用並傳遞相應的參數,在高階函數執行時,因爲回調函數的內部邏輯不一樣,高階函數的執行結果也不一樣,很是靈活,也被叫作函數式編程。javascript


柯里化

在 JavaScript 中,函數柯里化是函數式編程的重要思想,也是高階函數中一個重要的應用,其含義是給函數分步傳遞參數,每次傳遞部分參數,並返回一個更具體的函數接收剩下的參數,這中間可嵌套多層這樣的接收部分參數的函數,直至返回最後結果。java

一、最基本的柯里化拆分

// 原函數
function add(a, b, c) {
    return a + b + c;
}

// 柯里化函數
function addCurrying(a) {
    return function (b) {
        return function (c) {
            return a + b + c;
        }
    }
}

// 調用原函數
add(1, 2, 3); // 6

// 調用柯里化函數
addCurrying(1)(2)(3) // 6
複製代碼

被柯里化的函數 addCurrying 每次的返回值都爲一個函數,並使用下一個參數做爲形參,直到三個參數都被傳入後,返回的最後一個函數內部執行求和操做,實際上是充分的利用了閉包的特性來實現的。正則表達式

二、柯里化通用式

上面的柯里化函數沒涉及到高階函數,也不具有通用性,沒法轉換形參個數任意或未知的函數,咱們接下來封裝一個通用的柯里化轉換函數,能夠將任意函數轉換成柯里化。編程

// ES5 的實現
function currying(func, args) {
    // 形參個數
    var arity = func.length;
    // 上一次傳入的參數
    var args = args || [];

    return function () {
        // 將參數轉化爲數組
        var _args = [].slice.call(arguments);

        // 將上次的參數與當前參數進行組合並修正傳參順序
        Array.prototype.unshift.apply(_args, args);

        // 若是參數不夠,返回閉包函數繼續收集參數
        if(_args.length < arity) {
            return currying.call(null, func, _args);
        }

        // 參數夠了則直接執行被轉化的函數
        return func.apply(null, _args);
    }
}
複製代碼

上面主要使用的是 ES5 的語法來實現,大量的使用了 callapply,下面咱們經過 ES6 的方式實現功能徹底相同的柯里化轉換通用式。數組

// ES6 的實現
function currying(func, args = []) {
    let arity = func.length;

    return function (..._args) {
        _args.unshift(...args);

        if(_args.length < arity) {
            return currying.call(null, func, _args);
        }

        return func(..._args);
    }
}
複製代碼

函數 currying 算是比較高級的轉換柯里化的通用式,能夠隨意拆分參數,假設一個被轉換的函數有多個形參,咱們能夠在任意環節傳入任意個數的參數進行拆分,舉一個例子,假如 5 個參數,第一次能夠傳入 2 個,第二次能夠傳入 1 個, 第三次能夠傳入剩下的,也有其餘的多種傳參和拆分方案,由於在 currying 內部收集參數的同時按照被轉換函數的形參順序進行了更正。閉包

柯里化的一個很大的好處是能夠幫助咱們基於一個被轉換函數,經過對參數的拆分實現不一樣功能的函數,以下面的例子。app

// 被轉換函數,用於檢測傳入的字符串是否符合正則表達式
function checkFun(reg, str) {
    return reg.test(str);
}

// 轉換柯里化
let check = currying(checkFun);

// 產生新的功能函數
let checkPhone = check(/^1[34578]\d{9}$/);
let checkEmail = check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
複製代碼

上面的例子根據一個被轉換的函數經過轉換變成柯里化函數,並用 _check 變量接收,之後每次調用 _check 傳遞不一樣的正則就會產生一個檢測不一樣類型字符串的功能函數。函數式編程

這種使用方式一樣適用於被轉換函數是高階函數的狀況,好比下面的例子。函數

// 被轉換函數,按照傳入的回調函數對傳入的數組進行映射
function mapFun(func, array) {
    return array.map(func);
}

// 轉換柯里化
let getNewArray = currying(mapFun);

// 產生新的功能函數
let createPercentArr = getNewArray(item => `${item * 100}%`);
let createDoubleArr = getNewArray(item => item * 2);

// 使用新的功能函數
let arr = [1, 2, 3, 4, 5];
let percentArr = createPercentArr(arr); // ['100%', '200%', '300%', '400%', '500%',]
let doubleArr = createDoubleArr(arr); // [2, 4, 6, 8, 10]
複製代碼

三、柯里化與 bind

bind 方法是常用的一個方法,它的做用是幫咱們將調用 bind 函數內部的上下文對象 this 替換成咱們傳遞的第一個參數,並將後面其餘的參數做爲調用 bind 函數的參數。ui

// bind 方法的模擬
Object.prototype.bind = function (context) {
    var self = this;
    var args = [].slice.call(arguments, 1);

    return function () {
        return self.apply(context, args);
    }
}
複製代碼

經過上面代碼能夠看出,其實 bind 方法就是一個柯里化轉換函數,將調用 bind 方法的函數進行轉換,即經過閉包返回一個柯里化函數,執行該柯里化函數的時候,借用 apply 將調用 bind 的函數的執行上下文轉換成了 context 並執行,只是這個轉換函數沒有那麼複雜,沒有進行參數拆分,而是函數在調用的時候傳入了全部的參數。


反柯里化

反柯里化的思想與柯里化正好相反,若是說柯里化的過程是將函數拆分紅功能更具體化的函數,那反柯里化的做用則在於擴大函數的適用性,使原本做爲特定對象所擁有的功能函數能夠被任意對象所使用。

一、反柯里化通用式

反柯里化通用式的參數爲一個但願能夠被其餘對象調用的方法或函數,經過調用通用式返回一個函數,這個函數的第一個參數爲要執行方法的對象,後面的參數爲執行這個方法時須要傳遞的參數。

// ES5 的實現
function uncurring(fn) {
    return function () {
        // 取出要執行 fn 方法的對象,同時從 arguments 中刪除
        var obj = [].shift.call(arguments);
        return fn.apply(obj, arguments);
    }
}
複製代碼
// ES6 的實現
function uncurring(fn) {
    return function (...args) {
        return fn.call(...args);
    }
}
複製代碼

下面咱們經過一個例子來感覺一下反柯里化的應用。

// 構造函數 F
function F() {}

// 拼接屬性值的方法
F.prototype.concatProps = function () {
    let args = Array.from(arguments);
    return args.reduce((prev, next) => `${this[prev]}&${this[next]}`);
}

// 使用 concatProps 的對象
let obj = {
    name: "Panda",
    age: 16
};

// 使用反柯里化進行轉化
let concatProps = uncurring(F.prototype.concatProps);

concatProps(obj, "name", "age"); // Panda&16
複製代碼

反柯里化還有另一個應用,用來代替直接使用 callapply,好比檢測數據類型的 Object.prototype.toString 等方法,以往咱們使用時是在這個方法後面直接調用 call 更改上下文並傳參,若是項目中多處須要對不一樣的數據類型進行驗證是很麻的,常規的解決方案是封裝成一個檢測數據類型的模塊。

// 常規方案
function checkType(val) {
    return Object.prototype.toString.call(val);
}
複製代碼

若是須要這樣封裝的功能不少就麻煩了,代碼量也會隨之增大,其實咱們也可使用另外一種解決方案,就是利用反柯里化通用式將這個函數轉換並將返回的函數用變量接收,這樣咱們只須要封裝一個 uncurring 通用式就能夠了。

// 利用反柯里化建立檢測數據類型的函數
let checkType = uncurring(Object.prototype.toString);

checkType(1); // [object Number]
checkType("hello"); // [object String]
checkType(true); // [object Boolean]
複製代碼

二、經過函數調用生成反柯里化函數

在 JavaScript 咱們常用面向對象的編程方式,在兩個類或構造函數之間創建聯繫實現繼承,若是咱們對繼承的需求僅僅是但願一個構造函數的實例可以使用另外一個構造函數原型上的方法,那進行繁瑣的繼承很浪費,簡單的繼承父子類的關係又不那麼的優雅,還不如之間不存在聯繫。

Function.prototype.uncurring = function () {
    var self = this;
    return function () {
        return Function.prototype.call.apply(self, arguments);
    }
}
複製代碼

以前的問題經過上面給函數擴展的 uncurring 方法徹底獲得瞭解決,好比下面的例子。

// 構造函數
function F() {}

F.prototype.sayHi = function () {
    return "I'm " + this.name + ", " + this.age + " years old.";
}

// 但願 sayHi 方法被任何對象使用
sayHi = F.prototype.sayHi.uncurring();

sayHi({ name: "Panda", age: 20}); // I'm Panda, 20 years old.
複製代碼

Function 的原型對象上擴展的 uncurring 中,難點是理解 Function.prototype.call.apply,咱們知道在 call 的源碼邏輯中 this 指的是調用它的函數,在 call 內部用第一個參數替換了這個函數中的 this,其他做爲形參執行了函數。

而在 Function.prototype.call.applyapply 的第一個參數更換了 call 中的 this,這個用於更換 this 的就是例子中調用 uncurring 的方法 F.prototype.sayHi,因此等同於 F.prototype.sayHi.callarguments 內的參數會傳入 call 中,而 arguments 的第一項正是用於修改 F.prototype.sayHithis 的對象。


總結

看到這裏你應該對柯里化和反柯里化有了一個初步的認識了,但要熟練的運用在開發中,還須要咱們更深刻的去了解它們內在的含義。

相關文章
相關標籤/搜索