23_函數(shù)的柯里化了解嗎?說下函數(shù)柯里化應(yīng)用的場景

一、什么是柯里化?

我們先來看看維基百科中是如何定義的:在計算機科學(xué)中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。

我們可以舉個簡單的例子,如下函數(shù)add是一般的一個函數(shù),就是將傳進來的參數(shù)a和b相加;函數(shù)curryingAdd就是對函數(shù)add進行柯里化的函數(shù);
這樣一來,原來我們需要直接傳進去兩個參數(shù)來進行運算的函數(shù),現(xiàn)在需要分別傳入?yún)?shù)a和b,函數(shù)如下:

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

function curryingAdd(a) {
    return function(b) {
        return a + b;
    }
}

add(1, 2); // 3
curryingAdd(1)(2); // 3

看到這里你可能會想,這樣做有什么用?為什么要這樣做?這樣做能夠給我們的應(yīng)用帶來什么樣的好處?先別著急,我們接著往下看。

二、為什么要對函數(shù)進行柯里化?

  • 可以使用一些小技巧(見下文)
  • 提前綁定好函數(shù)里面的某些參數(shù),達到參數(shù)復(fù)用的效果,提高了適用性.
  • 固定易變因素
  • 延遲計算

總之,函數(shù)的柯里化能夠讓你重新組合你的應(yīng)用,把你的復(fù)雜功能拆分成一個一個的小部分,每一個小的部分都是簡單的,便于理解的,而且是容易測試的;

三、如何對函數(shù)進行柯里化?

在這一部分里,我們由淺入深的一步步來告訴大家如何對一個多參數(shù)的函數(shù)進行柯里化.其中用到的知識有閉包,高階函數(shù),不完全函數(shù)等等.

1、開胃菜

假如我們要實現(xiàn)一個功能,就是輸出語句name喜歡song,其中name和song都是可變參數(shù);那么一般情況下我們會這樣寫:

function printInfo(name, song) {
    console.log(name + '喜歡的歌曲是: ' + song);
}
printInfo('Tom', '七里香');
printInfo('Jerry', '雅俗共賞');

對上面的函數(shù)進行柯里化之后,我們可以這樣寫:

function curryingPrintInfo(name) {
    return function(song) {
        console.log(name + '喜歡的歌曲是: ' + song);
    }
}
var tomLike = curryingPrintInfo('Tom');
tomLike('七里香');
var jerryLike = curryingPrintInfo('Jerry');
jerryLike('雅俗共賞');

2、小雞燉蘑菇

上面我們雖然對函數(shù)printInfo進行了柯里化,但是我們可不想在需要柯里化的時候,都像上面那樣不斷地進行函數(shù)的嵌套,那簡直是噩夢;
所以我們要創(chuàng)造一些幫助其它函數(shù)進行柯里化的函數(shù),我們暫且叫它為curryingHelper吧,一個簡單的curryingHelper函數(shù)如下所示:

function curryingHelper(fn) {
    var _args = Array.prototype.slice.call(arguments, 1);
    return function() {
        var _newArgs = Array.prototype.slice.call(arguments);
        var _totalArgs = _args.concat(_newArgs);
        return fn.apply(this, _totalArgs);
    }
}

這里解釋一點東西,首先函數(shù)的arguments表示的是傳遞到函數(shù)中的參數(shù)對象,它不是一個數(shù)組,它是一個類數(shù)組對象;
所以我們可以使用函數(shù)的Array.prototype.slice方法,然后使用.call方法來獲取arguments里面的內(nèi)容。
我們使用fn.apply(this, _totalArgs)來給函數(shù)fn傳遞正確的參數(shù).

接下來我們來寫一個簡單的函數(shù)驗證上面的輔助柯里化函數(shù)的正確性, 代碼部分如下:

function showMsg(name, age, fruit) {
    console.log('My name is ' + name + ', I\'m ' + age + ' years old, ' + ' and I like eat ' + fruit);
}

var curryingShowMsg1 = curryingHelper(showMsg, 'dreamapple');
curryingShowMsg1(22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple

var curryingShowMsg2 = curryingHelper(showMsg, 'dreamapple', 20);
curryingShowMsg2('watermelon'); // My name is dreamapple, I'm 20 years old,  and I like eat watermelon

上面的結(jié)果表示,我們的這個柯里化的函數(shù)是正確的。上面的curryingHelper就是一個高階函數(shù)。

3、牛肉火鍋

上面的柯里化幫助函數(shù)確實已經(jīng)能夠達到我們的一般性需求了,但是它還不夠好,我們希望那些經(jīng)過柯里化后的函數(shù)可以每次只傳遞進去一個參數(shù),
然后可以進行多次參數(shù)的傳遞,那么應(yīng)該怎么辦呢?我們可以再花費一些腦筋,寫出一個betterCurryingHelper函數(shù),實現(xiàn)我們上面說的那些功能.代碼如下:

function betterCurryingHelper(fn, len) {
    var length = len || fn.length;
    return function () {
        var allArgsFulfilled = (arguments.length >= length);

        // 如果參數(shù)全部滿足,就可以終止遞歸調(diào)用
        if (allArgsFulfilled) {
            return fn.apply(this, arguments);
        }
        else {
            var argsNeedFulfilled = [fn].concat(Array.prototype.slice.call(arguments));
            return betterCurryingHelper(curryingHelper.apply(this, argsNeedFulfilled), length - arguments.length);
        }
    };
}

其中curryingHelper就是上面II 小雞燉蘑菇中提及的那個函數(shù).需要注意的是fn.length表示的是這個函數(shù)的參數(shù)長度.
接下來我們來檢驗一下這個函數(shù)的正確性:

var betterShowMsg = betterCurryingHelper(showMsg);
betterShowMsg('dreamapple', 22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
betterShowMsg('dreamapple', 22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
betterShowMsg('dreamapple')(22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
betterShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple

其中showMsg就是II 小雞燉蘑菇部分提及的那個函數(shù).
我們可以看出來,這個betterCurryingHelper確實實現(xiàn)了我們想要的那個功能.并且我們也可以像使用原來的那個函數(shù)一樣使用柯里化后的函數(shù)。

4、泡椒鳳爪

我們已經(jīng)能夠?qū)懗龊芎玫目吕锘o助函數(shù)了,但是這還不算是最刺激的,如果我們在傳遞參數(shù)的時候可以不按照順序來那一定很酷;當(dāng)然我們也可以寫出這樣的函數(shù)來,
這個crazyCurryingHelper函數(shù)如下所示:

var _ = {};
function crazyCurryingHelper(fn, length, args, holes) {
    length = length || fn.length;
    args   = args   || [];
    holes  = holes  || [];

    return function() {
        var _args       = args.slice(),
            _holes      = holes.slice();

        // 存儲接收到的args和holes的長度
        var argLength   = _args.length,
            holeLength  = _holes.length;

        var allArgumentsSpecified = false;

        // 循環(huán)
        var arg     = null,
            i       = 0,
            aLength = arguments.length;

        for(; i < aLength; i++) {
            arg = arguments[i];

            if(arg === _ && holeLength) {
                // 循環(huán)holes的位置
                holeLength--;
                _holes.push(_holes.shift());
            } else if (arg === _) {
                // 存儲hole就是_的位置
                _holes.push(argLength + i);
            } else if (holeLength) {
                // 是否還有沒有填補的hole
                // 在參數(shù)列表指定hole的地方插入當(dāng)前參數(shù)
                holeLength--;
                _args.splice(_holes.shift(), 0, arg);
            } else {
                // 不需要填補hole,直接添加到參數(shù)列表里面
                _args.push(arg);
            }
        }

        // 判斷是否所有的參數(shù)都已滿足
        allArgumentsSpecified = (_args.length >= length);
        if(allArgumentsSpecified) {
            return fn.apply(this, _args);
        }

        // 遞歸的進行柯里化
        return crazyCurryingHelper.call(this, fn, length, _args, _holes);
    };
}

一些解釋,我們使用_來表示參數(shù)中的那些缺失的參數(shù),如果你使用了lodash的話,會有沖突的;那么你可以使用別的符號替代.
按照一貫的尿性,我們還是要驗證一下這個crazyCurryingHelper是不是實現(xiàn)了我們所說的哪些功能,代碼如下:

var crazyShowMsg = crazyCurryingHelper(showMsg);
crazyShowMsg(_, 22)('dreamapple')('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
crazyShowMsg( _, 22, 'apple')('dreamapple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
crazyShowMsg( _, 22, _)('dreamapple', _, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
crazyShowMsg( 'dreamapple', _, _)(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
crazyShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple

結(jié)果顯示,我們這個函數(shù)也實現(xiàn)了我們所說的那些功能。

四、柯里化的一些應(yīng)用場景

說了那么多,其實這部分才是最重要的部分;學(xué)習(xí)某個知識要一定可以用得到,不然學(xué)習(xí)它干嘛。

1、關(guān)于函數(shù)柯里化的一些小技巧

(1)給setTimeout傳遞進來的函數(shù)添加參數(shù)

一般情況下,我們?nèi)绻虢o一個setTimeout傳遞進來的函數(shù)添加參數(shù)的話,一般會使用這種方法:

function hello(name) {
    console.log('Hello, ' + name);
}
setTimeout(hello('dreamapple'), 3600); //立即執(zhí)行,不會在3.6s后執(zhí)行
setTimeout(function() {
    hello('dreamapple');
}, 3600); // 3.6s 后執(zhí)行

我們使用了一個新的匿名函數(shù)包裹我們要執(zhí)行的函數(shù),然后在函數(shù)體里面給那個函數(shù)傳遞參數(shù)值。

當(dāng)然,在ES5里面,我們也可以使用函數(shù)的bind方法,如下所示:

setTimeout(hello.bind(this, 'dreamapple'), 3600); // 3.6s 之后執(zhí)行函數(shù)

這樣也是非常的方便快捷,并且可以綁定函數(shù)執(zhí)行的上下文.

我們本篇文章是討論函數(shù)的柯里化,當(dāng)然我們這里也可以使用函數(shù)的柯里化來達到這個效果:

setTimeout(curryingHelper(hello, 'dreamapple'), 3600); // 其中curryingHelper是上面已經(jīng)提及過的

這樣也是可以的,是不是很酷.

(2)寫出這樣一個函數(shù)multiply(1)(2)(3) == 6結(jié)果為true,multiply(1)(2)(3)(...)(n) == (1)(2)(3)(...)(n)結(jié)果為true

這個題目不知道大家碰到過沒有,不過通過函數(shù)的柯里化,也是有辦法解決的,看下面的代碼:

function multiply(x) {
    var y = function(x) {
        return multiply(x * y);
    };
    y.toString = y.valueOf = function() {
        return x;
    };
    return y;
}

console.log(multiply(1)(2)(3) == 6); // true
console.log(multiply(1)(2)(3)(4)(5) == 120); // true

因為multiply(1)(2)(3)的直接結(jié)果并不是6,而是一個函數(shù)對象{ [Number: 6] valueOf: [Function], toString: [Function] },我們之后使用了==會將左邊這個函數(shù)對象轉(zhuǎn)換成為一個數(shù)字,所以就達到了我們想要的結(jié)果.還有關(guān)于為什么使用toString和valueOf方法
可以看看這里的解釋Number.prototype.valueOf(),Function.prototype.toString().

(3)上面的那個函數(shù)不夠純粹,我們也可以實現(xiàn)一個更純粹的函數(shù),但是可以會不太符合題目的要求。我們可以這樣做,先把函數(shù)的參數(shù)存儲,然后再對這些參數(shù)做處理,一旦有了這個思路,我們就不難寫出下面的代碼:

function add() {
    var args = Array.prototype.slice.call(arguments);
    var _that = this;
    return function() {
        var newArgs = Array.prototype.slice.call(arguments);
        var total = args.concat(newArgs);
        if(!arguments.length) {
            var result = 1;
            for(var i = 0; i < total.length; i++) {
                result *= total[i];
            }
            return result;
        }
        else {
            return add.apply(_that, total);
        }
    }
}
console.log(add(1)(2)(3)()); // 6
console.log(add(1, 2, 3)()); // 6

(4)當(dāng)我們的需要兼容IE9之前版本的IE瀏覽器的話,我們可能需要寫出一些兼容的方案 ,比如事件監(jiān)聽;一般情況下我們應(yīng)該會這樣寫:

var addEvent = function (el, type, fn, capture) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, capture);
    }
    else {
        el.attachEvent('on' + type, fn);
    }
};

這樣寫也是可以的,但是性能上會差一點,因為如果是在低版本的IE瀏覽器上每一次都會運行if()語句,產(chǎn)生了不必要的性能開銷.
我們也可以這樣寫:

var addEvent = (function () {
    if (window.addEventListener) {
        return function (el, type, fn, capture) {
            el.addEventListener(type, fn, capture);
        }
    }
    else {
        return function (el, type, fn) {
            var IEtype = 'on' + type;
            el.attachEvent(IEtype, fn);
        }
    }
})();

這樣就減少了不必要的開支,整個函數(shù)運行一次就可以了.

2、延遲計算 上面的那兩個函數(shù)multiply()和add()實際上就是延遲計算的例子.

3、提前綁定好函數(shù)里面的某些參數(shù),達到參數(shù)復(fù)用的效果,提高了適用性.

我們的I 開胃菜部分的tomLike和jerryLike其實就是屬于這種的,綁定好函數(shù)里面的第一個參數(shù),然后后面根據(jù)情況分別使用不同的函數(shù).

4、固定易變因素

我們經(jīng)常使用的函數(shù)的bind方法就是一個固定易變因素的很好的例子.

五、關(guān)于柯里化的性能

當(dāng)然,使用柯里化意味著有一些額外的開銷;這些開銷一般涉及到這些方面,首先是關(guān)于函數(shù)參數(shù)的調(diào)用,操作arguments對象通常會比操作命名的參數(shù)要慢一點;
還有,在一些老的版本的瀏覽器中arguments.length的實現(xiàn)是很慢的;直接調(diào)用函數(shù)fn要比使用fn.apply()或者fn.call()要快一點;產(chǎn)生大量的嵌套作用域還有閉包會帶來一些性能還有速度的降低.但是,大多數(shù)的web應(yīng)用的性能瓶頸是發(fā)生在操作DOM上的,所以上面的那些開銷比起DOM操作的開銷還是比較小的.

六、一些知識點的解釋

1、瑣碎的知識點

fn.length: 表示的是這個函數(shù)中參數(shù)的個數(shù).
arguments.callee: 指向的是當(dāng)前運行的函數(shù).callee是arguments對象的屬性。

在該函數(shù)的函數(shù)體內(nèi),它可以指向當(dāng)前正在執(zhí)行的函數(shù).當(dāng)函數(shù)是匿名函數(shù)時,這是很有用的,比如沒有名字的函數(shù)表達式(也被叫做"匿名函數(shù)").
詳細(xì)解釋可以看這里arguments.callee.我們可以看一下下面的例子:

function hello() {
    return function() {
        console.log('hello');
        if(!arguments.length) {
            console.log('from a anonymous function.');
            return arguments.callee;
        }
    }
}

hello()(1); // hello

/*
 * hello
 * from a anonymous function.
 * hello
 * from a anonymous function.
 */
hello()()();

fn.caller: 返回調(diào)用指定函數(shù)的函數(shù).詳細(xì)的解釋可以看這里Function.caller,下面是示例代碼:

function hello() {
    console.log('hello');
    console.log(hello.caller);
}

function callHello(fn) {
    return fn();
}

callHello(hello); // hello [Function: callHello]

2、高階函數(shù)(high-order function)

高階函數(shù)就是操作函數(shù)的函數(shù),它接受一個或多個函數(shù)作為參數(shù),并返回一個新的函數(shù).

我們來看一個例子,來幫助我們理解這個概念.就舉一個我們高中經(jīng)常遇到的場景,如下:

f1(x, y) = x + y;
f2(x) = x * x;
f3 = f2(f3(x, y));

我們來實現(xiàn)f3函數(shù),看看應(yīng)該如何實現(xiàn),具體的代碼如下所示:

function f1(x, y) {
    return x + y;
}

function f2(x) {
    return x * x;
}

function func3(func1, func2) {
    return function() {
        return func2.call(this, func1.apply(this, arguments));
    }
}

var f3 = func3(f1, f2);
console.log(f3(2, 3)); // 25

我們通過函數(shù)func3將函數(shù)f1,f2結(jié)合到了一起,然后返回了一個新的函數(shù)f3;這個函數(shù)就是我們期望的那個函數(shù).

3、不完全函數(shù)(partial function)

什么是不完全函數(shù)呢?所謂的不完全函數(shù)和我們上面所說的柯里化基本差不多;所謂的不完全函數(shù),就是給你想要運行的那個函數(shù)綁定一個固定的參數(shù)值;
然后后面的運行或者說傳遞參數(shù)都是在前面的基礎(chǔ)上進行運行的.看下面的例子:

// 一個將函數(shù)的arguments對象變成一個數(shù)組的方法
function array(a, n) {
    return Array.prototype.slice.call(a, n || 0);
}
// 我們要運行的函數(shù)
function showMsg(a, b, c){
    return a * (b - c);
}
function partialLeft(f) {
    var args = arguments;
    return function() {
        var a = array(args, 1);
        a = a.concat(array(arguments));
        console.log(a); // 打印實際傳遞到函數(shù)中的參數(shù)列表
        return f.apply(this, a);
    }
}
function partialRight(f) {
    var args = arguments;
    return function() {
        var a = array(arguments);
        a = a.concat(array(args, 1));
        console.log(a); // 打印實際傳遞到函數(shù)中的參數(shù)列表
        return f.apply(this, a);
    }
}
function partial(f) {
    var args = arguments;
    return function() {
        var a = array(args, 1);
        var i = 0; j = 0;
        for(; i < a.length; i++) {
            if(a[i] === undefined) {
                a[i] = arguments[j++];
            }
        }
        a = a.concat(array(arguments, j));
        console.log(a); // 打印實際傳遞到函數(shù)中的參數(shù)列表
        return f.apply(this, a);
    }
}
partialLeft(showMsg, 1)(2, 3); // 實際參數(shù)列表: [1, 2, 3] 所以結(jié)果是 1 * (2 - 3) = -1
partialRight(showMsg, 1)(2, 3); // 實際參數(shù)列表: [2, 3, 1] 所以結(jié)果是 2 * (3 - 1) = 4
partial(showMsg, undefined, 1)(2, 3); // 實際參數(shù)列表: [2, 1, 3] 所以結(jié)果是 2 * (1 - 3) = -4

七、一些JS庫

JavaScript的柯里化與JavaScript的函數(shù)式編程密不可分,下面列舉了一些關(guān)于JavaScript函數(shù)式編程的庫,大家可以看一下:

八、參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容