博客 / 詳情

返回

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

在這裏插入圖片描述


閲讀原文


前言

在 JavaScript 中,柯里化和反柯里化是高階函數的一種應用,在這之前我們應該清楚什麼是高階函數,通俗的説,函數可以作為參數傳遞到函數中,這個作為參數的函數叫回調函數,而擁有這個參數的函數就是高階函數,回調函數在高階函數中調用並傳遞相應的參數,在高階函數執行時,由於回調函數的內部邏輯不同,高階函數的執行結果也不同,非常靈活,也被叫做函數式編程。


柯里化

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

1、最基本的柯里化拆分

// 柯里化拆分
// 原函數
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 每次的返回值都為一個函數,並使用下一個參數作為形參,直到三個參數都被傳入後,返回的最後一個函數內部執行求和操作,其實是充分的利用了閉包的特性來實現的。

2、柯里化通用式

上面的柯里化函數沒涉及到高階函數,也不具備通用性,無法轉換形參個數任意或未知的函數,我們接下來封裝一個通用的柯里化轉換函數,可以將任意函數轉換成柯里化。

// 柯里化通用式 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(func, _args);
        }

        return func(..._args);
    }
}

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

柯里化的一個很大的好處是可以幫助我們基於一個被轉換函數,通過對參數的拆分實現不同功能的函數,如下面的例子。

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

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

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

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

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

// 柯里化通用式應用 —— 高階函數
// 被轉換函數,按照傳入的回調函數對傳入的數組進行映射
function mapFun(func, array) {
    return array.map(func);
}

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

// 產生新的功能函數
const createPercentArr = getNewArray(item => `${item * 100}%`);
const 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]

3、柯里化與 bind

bind 方法是經常使用的一個方法,它的作用是幫我們將調用 bind 函數內部的上下文對象 this 替換成我們傳遞的第一個參數,並將後面其他的參數作為調用 bind 函數的參數。

// bind 方法原理模擬
// bind 方法的模擬
Function.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 並執行,只是這個轉換函數沒有那麼複雜,沒有進行參數拆分,而是函數在調用的時候傳入了所有的參數。


反柯里化

反柯里化的思想與柯里化正好相反,如果説柯里化的過程是將函數拆分成功能更具體化的函數,那反柯里化的作用則在於擴大函數的適用性,使本來作為特定對象所擁有的功能函數可以被任意對象所使用。

1、反柯里化通用式

反柯里化通用式的參數為一個希望可以被其他對象調用的方法或函數,通過調用通用式返回一個函數,這個函數的第一個參數為要執行方法的對象,後面的參數為執行這個方法時需要傳遞的參數。

// 反柯里化通用式 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
};

// 使用反柯里化進行轉化
const 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 通用式就可以了。

// 反柯里化創建檢測類型函數
const checkType = uncurring(Object.prototype.toString);

checkType(1); // [object Number]
checkType("hello"); // [object String]
checkType(true); // [object Boolean]

2、通過函數調用生成反柯里化函數

在 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 的對象。


總結

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


user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.