「前端發動機」從 bind 聊到 curry (柯里化)

前言

很長時間沒有更新,一方面是工做比較忙,另外一方面本身也處於一個學習的過程。接下來應該會逐漸恢復到穩定更新的狀態,分享一些有趣的知識點以及我我的的思考。感興趣的朋友能夠關注下呀!javascript

若是有不對的地方麻煩你們斧正,我會及時更新,感謝。前端

博客地址 🍹🍰 fe-codevue

bind

bind() 方法建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被 bind 的第一個參數指定,其他的參數將做爲新函數的參數供調用時使用。 —— MDNjava

bind 方法想必你們都很熟悉,一般用來作 this 的綁定,它會返回一個新函數。可是還有一點,咱們每每會忽略掉:其他的參數將做爲新函數的參數供調用時使用node

栗子:git

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

const addBind = add.bind(null, 1);
addBind(2); // 3
複製代碼

顯而易見,bind 調用時傳入的參數會在 addBind 調用時和新參數一塊兒傳入。github

Polyfill

那這是怎麼作到的呢?咱們簡單看下 bind 的 Polyfill 版本,網絡上相似的實現也有不少。面試

Function.prototype.mybind = function(context, ...args) {
    const f = this;
    const fpro = function() {};
    const fBound = function(..._arg) {
        // fpro.prototype.isPrototypeOf(this) 判斷是不是 new 調用的 fBound
        return f.apply(fpro.prototype.isPrototypeOf(this)
                ? this
                : context,
                [...args, ..._arg]);
    };
    if (this.prototype) {
        fpro.prototype = this.prototype;
    }
    fBound.prototype = new fpro();
    return fBound;
};
複製代碼

能夠看到,bind 實際返回的是相似於 fBound 的函數,咱們簡化一下看看。mongodb

// 刪減部分代碼
// f 
// args
const fBound = function(..._arg) {
    return f.apply(null, [...args, ..._arg]);
};
複製代碼

其實就是利用閉包保存了上文的參數,最後再一塊兒傳給目標函數使用。這樣若是不考慮 this,不使用 bind,咱們能夠直接把 add 函數改爲這樣:編程

function add(a) {
    return function (b) {
        return a + b;
    }
}
add(1)(2); // 2
複製代碼

是否是很熟悉,一道很常見的面試題,這就是咱們要說的柯里化。

curry

在計算機科學中,柯里化(英語:Currying),又譯爲卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。—— 維基百科

curry 實際上是函數式編程中很是重要的一個思想,經過簡單的局部調用,讓函數具備預加載能力以及減小樣板代碼。

正由於咱們有相似於add(1)(2)這樣的需求,可是每次手動實現又很繁瑣,因此可使用一個特殊的 curry 函數,來幫助咱們達到需求(在 lodash 等一些函數庫中均有相似實現,感興趣的同窗能夠去看看源碼)。

簡單實現

function curry(fn) {
    const len = fn.length;
    return function bindfn() {
        if(arguments.length < len) {
            return bindfn.bind(null, ...arguments); 
            // 關鍵:保存參數,並在調用時和後面的參數一塊兒傳入 bindfn
        } else {
            return fn.call(null, ...arguments);
        }
    }
}
複製代碼

這個實現方案正是利用咱們上面提到的 bind 的特性,因此咱們再要實現 add 需求的時候就能夠這樣作:

// 普通寫法
function add(a, b) {
    return a + b
}
const curryAdd = curry(add);
curryAdd(1)(2); // 3
複製代碼

其餘版本

不使用 bind 咱們要怎麼實現一個 curry 呢?

function _curry(fn) {
    const len = fn.length;
    function warp (..._arguments) {
        let _arg = _arguments;
        // 容許一次傳完全部參數,雖然這和不用 curry 同樣 add(1, 2);
        if (_arg.length >= len) {
            return fn(..._arg);
        }
        function fnc(...args) {
            _arg = [..._arg, ...args];
            if (_arg.length < len) { // 參數不夠,繼續返回函數
                return fnc;
            } else { // 參數足夠,執行原函數
                return fn(..._arg);
            }
        }
        return fnc;
    }
    return warp;
}
複製代碼

可是需求每每是多變的,以上兩種方案,咱們都是用形參和實參的個數來判斷的函數是否須要執行,可是若是這種需求呢?

add(1)(2)() // 3add(1)(2)(3)() // 6,參數不肯定且只有手動調用(),纔會執行函數。很容易會想到須要改判斷函數返回的判斷條件。

function _curry1(fn) {
    function warp (..._arguments) {
        let _arg = _arguments;
        function fnc(...args) {
            _arg = [..._arg, ...args];
            if (args.length > 0) { // 經過判斷當前是否傳入了參數,來決定函數返回
                return fnc;
            } else {
                return fn(..._arg);
            }
        }
        return fnc;
    }
    return warp;
}
複製代碼

固然,這種 add 函數自己也得作調整,用來知足任意參數累加的需求。

function addInfinity(...args) {
    return args.reduce((pre, cur) => pre + cur);
}
複製代碼

應用

前面囉嗦這麼多固然不是爲了水這一篇文章,並且add(1)(2) 這種東西除了能應付面試還能作什麼?咱們真正須要的,仍是實際應用到業務中。

  • request

相信你們都寫過或者看過相似的代碼。

function request(url, params) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(url), 1000);
    })
}
//
function getData(params) {
    return request('/api/getData', params);
}
function setData(params) {
    return request('/api/setData', params);
}
//...
複製代碼

這就很是適合用 curry 來作處理,減小樣板代碼。

const _curryRequest = _curry(request);
const getData = _curryRequest('/api/getData'); // 默認入參
const setData = _curryRequest('/api/setData');
複製代碼
  • map

數組的 map 函數你們都用過,簡單看一個場景。

[1,2,3].map(x => 2 * x);
複製代碼

若是但願這個是一個通用型的功能應該怎麼處理呢?很容易會想到這麼寫。

function map(fn, arr) {
    return arr.map(fn);
}
function multiply2(x) {
    return 2 * x;
}
// map(multiply2, arr);
複製代碼

好像看起來很不錯,簡潔易用。那換成 curry 咱們來看下。

const mapMultiply2 = curry(map, multiply2);
// 可能有同窗會發現,以前寫的 curry 函數並不支持這種寫法。沒錯,不過稍微處理一下就好,我這裏就不作處理了,你們能夠本身試試。
// mapMultiply2(arr);
複製代碼

能夠看見,curry 更利於咱們去抽取函數,組合函數,讓咱們把更多的精力放在 multiply2 上。

總結

到這裏,這篇文章就結束了。相信你們對 curry 也有了一些瞭解,好比,經過局部調用,讓函數具備預加載能力以及減小樣板代碼。固然,關於柯里化的東西其實還不少,我這裏講的比較淺顯,之後有機會再分享這方面的東西。

參考文章

  • JS 函數式編程指南

交流羣

qq前端交流羣:960807765,歡迎各類技術交流,期待你的加入;

微信羣:有須要的同窗能夠加我好友(q1324210213),我拉你入羣。

後記

若是你看到了這裏,且本文對你有一點幫助的話,但願你能夠動動小手支持一下做者,感謝🍻。文中若有不對之處,也歡迎你們指出,共勉。好了,又耽誤你們的時間了,感謝閱讀,下次再見!

往期文章:

感興趣的同窗能夠關注下個人公衆號 前端發動機,好玩又有料。

相關文章
相關標籤/搜索