JavaScript柯里化

zhaiyy發表於2018-06-07

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

假設函式庫裡提供這樣一個拼接URL地址的函式:

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

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

顯然你不想每次呼叫時都手動敲一下前兩個引數,麻煩不說,還容易出錯。怎麼辦呢?你會想直接將庫函式改成單參不就行了?

function simpleURL(path) {
    return "http://www.jackzxl.net/" + path;
}
複製程式碼

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

所謂柯里化就是:將函式與其引數的一個子集繫結起來後返回個新函式。如果感覺比較抽象,可以做一些類比,比如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來實現柯里化。再回過頭體會一下柯里化定義:將函式與其引數的一個子集繫結起來後返回個新函式。柯里化後發現函式變得更靈活,更流暢,是一種簡潔的實現函式委託的方式

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

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函式,首先引數fn是需要柯里化的simpleURL函式,後面均為可變引數(函式的arguments可參考這裡),currying裡每行程式碼的執行結果如下:

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");
    };
};
複製程式碼

上面已經說明了柯里化的原理和實現。那究竟柯里化有什麼作用呢?常見的作用是:

  • 引數複用
  • 延遲執行
  • 扁平化

引數複用上面例子已經展示了,不贅述。

延遲執行其實非常直觀,因為不是返回運算結果,而是返回新函式,當然是延遲執行啦。例如bind就是延遲執行的代表,不贅述

扁平化的函式更加易讀。例如你要從站點的JSON資料裡獲取所有文章的title:

//JSON資料
{
    "user": "Jack",
    "posts": [
        { "title": "JavaScript Curry", "contents": "..." },
        { "title": " JavaScript Function", "contents": "..." }
    ]
}

//從JSON資料中獲取所有文章的title
fetchFromServer()
    .then(JSON.parse)
    .then(function(data){ return data.posts })
    .then(function(posts){
        return posts.map(function(post){ return post.title })
    })
複製程式碼

當然你可能寫出更優雅的程式碼…但這不是重點。重點是用柯里化將程式碼更加易讀易維護:

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

fetchFromServer()
    .then(JSON.parse)
    .then(get('posts'))
    .then(map(get('title')))
複製程式碼

提前返回?

最後網上還有個作用是提前返回,例如IE的事件和其他瀏覽器不同,為實現相容性,可以這樣實現:

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

但上面這樣有個問題,每次呼叫addHandler函式都要進行一次if…else的判斷。常識告訴我們,除非使用者在執行過程中更換瀏覽器(如果能現實的話),否則只需要在使用者第一次連線站點時判定一次即可,之後的呼叫不必再次檢查了。

用柯里化返回新函式的特性可以實現:

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);
        };
    }
})();   
複製程式碼

但在我看來,這裡用柯里化意義不大。因為柯里化雖然優點很多,缺點同樣明顯,就是學習成本有點高。用柯里化實現“提前返回”,維護的成本大於收益。

不用柯里化怎麼實現呢?一個三元運算子就搞定了:

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

或者函式內部重寫該函式也行:

function addHandler(target, eventType, handler){
    if (target.addEventListener){
        addHandler = function(target, eventType, handler){  //重寫該函式
            target.addEventListener(eventType, handler, false);
        };
    } else {        //IE
        addHandler = function(target, eventType, handler){  //重寫該函式
            target.attachEvent("on" + eventType, handler);
        };
    }
    addHandler(target, eventType, handler); //呼叫新函式
}
複製程式碼

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

總結

柯里化雖然有一個神祕的名字,但其實說穿了並不神祕。在前端它的應用場並不多(當然也可能我經驗比較淺),更多的應該是用在後端非同步函式裡,如Node.js,對於非同步API用柯里化可以減少回撥巢狀。

https://www.jianshu.com/p/9b6b5c7527fc

相關文章