JavaScript柯里化

Currying柯里化是函數(shù)式語言都有的一個特性,如Perl,Python,JavaScript。本篇就借用一下JavaScript,介紹一下柯里化的思想及應(yīng)用。

假設(shè)函數(shù)庫里提供這樣一個拼接URL地址的函數(shù):

function simpleURL(protocol, domain, path) {
    return protocol + "://" + domain + "/" + path;
}
simpleURL('http','www.jackzxl.net', 'index.html');      //http://www.jackzxl.net/index.html

這是個最普通的函數(shù)毫無新意。但對于你的站點來說,第一個參數(shù)固定為http,第二個參數(shù)固定為www.jackzxl.net,唯一需要改變的是第三個參數(shù)。即你的站點中的任何頁面或資源,前兩個參數(shù)永遠固定,只需要改變第三個參數(shù)。

顯然你不想每次調(diào)用時都手動敲一下前兩個參數(shù),麻煩不說,還容易出錯。怎么辦呢?你會想直接將庫函數(shù)改成單參不就行了?

function simpleURL(path) {
    return "http://www.jackzxl.net/" + path;
}

這樣改有兩個問題,首先如果該庫函數(shù)還需要被其他人或其他地方使用,直接改庫函數(shù)這條路是絕對行不通的。其次就算你對函數(shù)有絕對的控制權(quán),這樣改顯得也非常的不靈活,如果哪天你的站點要加上SSL呢?總不能把第一個參數(shù)再放回去吧。因此你正確的選擇是柯里化。

所謂柯里化就是:將函數(shù)與其參數(shù)的一個子集綁定起來后返回個新函數(shù)。如果感覺比較抽象,可以做一些類比,比如C++模板里的偏特化,這樣理解起來能容易點。將上例柯里化一下:

var myURL = simpleURL.bind(null, 'http', 'www.jackzxl.net');
myURL('myfile.js');     //http://www.jackzxl.net/myfile.js

//站點加上SSL
var mySslURL = simpleURL.bind(null, 'https', 'www.jackzxl.net');
mySslURL('myfile.js');  //https://www.jackzxl.net/myfile.js

上述代碼用bind來實現(xiàn)柯里化。再回過頭體會一下柯里化定義:將函數(shù)與其參數(shù)的一個子集綁定起來后返回個新函數(shù)。柯里化后發(fā)現(xiàn)函數(shù)變得更靈活,更流暢,是一種簡潔的實現(xiàn)函數(shù)委托的方式

為何用bind來實現(xiàn)柯里化呢?因為簡單嘛,有現(xiàn)成的就不必自己造輪子了。但因為本篇介紹的是柯里化,所以我們自己實現(xiàn)一下柯里化,來加深理解。它需要滿足兩點:參數(shù)子集,返回新函數(shù):

var currying = function(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(null, newArgs);
    };
};

var myURL2 = currying(simpleURL, 'https', 'www.jackzxl.net');
myURL2('myfile.js');    //http://www.jackzxl.net/myfile.js

效果和用bind是一樣的,我們仔細分析一下自定義的currying函數(shù),首先參數(shù)fn是需要柯里化的simpleURL函數(shù),后面均為可變參數(shù)(函數(shù)的arguments可參考這里),currying里每行代碼的執(zhí)行結(jié)果如下:

var currying = function(fn) {
    var args = [].slice.call(arguments, 1);
    //args為["https", "www.jackzxl.net"]

    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        //newArgs為["https", "www.jackzxl.net", "myFile.js"]

        return fn.apply(null, newArgs);
        //相當于return simpleURL("https", "www.jackzxl.net", "myFile.js");
    };
};

上面已經(jīng)說明了柯里化的原理和實現(xiàn)。那究竟柯里化有什么作用呢?常見的作用是:

  • 參數(shù)復(fù)用
  • 延遲運行
  • 扁平化

參數(shù)復(fù)用上面例子已經(jīng)展示了,不贅述。

延遲運行其實非常直觀,因為不是返回運算結(jié)果,而是返回新函數(shù),當然是延遲運行啦。例如bind就是延遲執(zhí)行的代表,不贅述

扁平化的函數(shù)更加易讀。例如你要從站點的JSON數(shù)據(jù)里獲取所有文章的title:

//JSON數(shù)據(jù)
{
    "user": "Jack",
    "posts": [
        { "title": "JavaScript Curry", "contents": "..." },
        { "title": " JavaScript Function", "contents": "..." }
    ]
}

//從JSON數(shù)據(jù)中獲取所有文章的title
fetchFromServer()
    .then(JSON.parse)
    .then(function(data){ return data.posts })
    .then(function(posts){
        return posts.map(function(post){ return post.title })
    })

當然你可能寫出更優(yōu)雅的代碼…但這不是重點。重點是用柯里化將代碼更加易讀易維護:

var curry = require('curry');
var get = curry(function(property, object){ return object[property] });

fetchFromServer()
    .then(JSON.parse)
    .then(get('posts'))
    .then(map(get('title')))

提前返回?

最后網(wǎng)上還有個作用是提前返回,例如IE的事件和其他瀏覽器不同,為實現(xiàn)兼容性,可以這樣實現(xiàn):

function addHandler(target, eventType, handler){
    if (target.addEventListener){
        target.addEventListener(eventType, handler, false);
    } else {        //IE
        target.attachEvent("on" + eventType, handler);
    }
}

但上面這樣有個問題,每次調(diào)用addHandler函數(shù)都要進行一次if…else的判斷。常識告訴我們,除非用戶在執(zhí)行過程中更換瀏覽器(如果能現(xiàn)實的話),否則只需要在用戶第一次連接站點時判定一次即可,之后的調(diào)用不必再次檢查了。

用柯里化返回新函數(shù)的特性可以實現(xiàn):

var addEvent = (function(){
    if (target.addEventListener) {
        return function(target, eventType, handler) {
            target.addEventListener(eventType, handler, false);
        };
    } else {        //IE
        return function(target, eventType, handler) {
            target.attachEvent("on" + eventType, handler);
        };
    }
})();   

但在我看來,這里用柯里化意義不大。因為柯里化雖然優(yōu)點很多,缺點同樣明顯,就是學(xué)習(xí)成本有點高。用柯里化實現(xiàn)“提前返回”,維護的成本大于收益。

不用柯里化怎么實現(xiàn)呢?一個三元運算符就搞定了:

var addHandler = document.body.addEventListener ?
    function(target, eventType, handler){
        target.addEventListener(eventType, handler, false);
    } :
    function(target, eventType, handler){
        target.attachEvent("on" + eventType, handler);
    };

或者函數(shù)內(nèi)部重寫該函數(shù)也行:

function addHandler(target, eventType, handler){
    if (target.addEventListener){
        addHandler = function(target, eventType, handler){  //重寫該函數(shù)
            target.addEventListener(eventType, handler, false);
        };
    } else {        //IE
        addHandler = function(target, eventType, handler){  //重寫該函數(shù)
            target.attachEvent("on" + eventType, handler);
        };
    }
    addHandler(target, eventType, handler); //調(diào)用新函數(shù)
}

兩種方法都非常直觀,簡單明了,不要為了用柯里化而用柯里化。

總結(jié)

柯里化雖然有一個神秘的名字,但其實說穿了并不神秘。在前端它的應(yīng)用場并不多(當然也可能我經(jīng)驗比較淺),更多的應(yīng)該是用在后端異步函數(shù)里,如Node.js,對于異步API用柯里化可以減少回調(diào)嵌套。

最后編輯于
?著作權(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)容