邂逅函數柯里化

引子javascript


有這樣一道題目,實現一個函數,實現以下功能:html

var result = sum(1)(2)(3);

console.log(result);//6

這道題目,印象中是一道技術筆試題。結合查到的資料,在這裏作一下簡單的分析和總結。前端

一個簡單的例子java


題目給的仍是比較寬的,沒多少限制,給了不少自由發揮的空間。數組

下面咱們就一步一步的去實現,一種簡單的作法能夠是這樣的:瀏覽器

function add(a){
    var sum = 0;
        sum += a;
    return function(b){
        sum += b;
        return function(c){
            sum += c;
            return sum;
        }
    }
}

add(1)(2)(3);//6

嗯,沒什麼問題。閉包

在此基礎上,咱們再進一步:app

若是對調用的次數不加限制,好比 四次,那上面的代碼就不行了。
那該怎麼辦呢?

觀察一下,咱們能夠發現返回的每個函數執行的邏輯其實都是同樣的。
就此咱們能夠精簡下代碼,讓函數返回後返回自身。函數

來試一下:性能

function add(a){

    var sum = 0;
    sum += a; 
    
    return function temp(b) {
        sum += b;
        return temp;
    }
}

add(2)(3)(4)(5);

輸出的結果:

//function temp(b) {
//        sum += b;
//        return temp;
//    }

並無像咱們預期的那樣輸出 14,實際上是這樣的,每次函數調用後返回的就是一個函數對象,那最後的結果,確定是一個字符串表示啊。

要修正的話,有兩個辦法。

  1. 判斷參數,當沒有輸入參數時,返回調用結果:
function add(a){

    var sum = 0;
    sum += a; 
    
    return function temp(b) { 
    
        if (arguments.length === 0) {
            return sum;
        } else {
            sum= sum+ b;
            return temp;
        }
    }
}

add(2)(3)(4)(5)(); //14

若是要使用匿名函數,也能夠:

function add() {

        var _args = [];

        return function(){ 

            if(arguments.length === 0) { 
                return _args.reduce(function(a,b) {
                    return a + b;
                });
            }

            [].push.apply(_args, [].slice.call(arguments));

            return arguments.callee;
        }
    }

    var sum = add();
    sum(2,3)(4)(5)(); //14

2 . 利用JS中對象到原始值的轉換規則。

當一個對象轉換成原始值時,先查看對象是否有valueOf方法。
若是有而且返回值是一個原始值,那麼直接返回這個值。
若是沒有valueOf 或 返回的不是原始值,那麼調用toString方法,返回字符串表示。

那咱們就爲函數對象添加一個valueOf方法 和 toString方法:

function add(a) {

    var sum = 0;
    
        sum += a;
        
    var temp = function(b) {
    
        if(arguments.length===0){
            return sum;
        } else {
            sum = sum+ b;
            return temp;
        }
        

    }
    
    temp.toString = temp.valueOf = function() {
        return sum; 
    }
    
    
    return temp;
}

add(2)(3)(4)(5); //14

寫到這裏,咱們來簡單總結下。

柯里化的定義


柯里化一般也稱部分求值,其含義是給函數分步傳遞參數,每次傳遞參數後,部分應用參數,並返回一個更具體的函數接受剩下的參數,中間可嵌套多層這樣的接受部分參數函數,逐步縮小函數的適用範圍,逐步求解,直至返回最後結果。

一個通用的柯里化函數


var curring = function(fn){
        var _args = [];
        return function cb(){

            if(arguments.length === 0) {
                return fn.apply(this, _args);
            }

            Array.prototype.push.apply(_args, [].slice.call(arguments));

            return cb;
        }


    }

    var multi = function(){

        var total = 0;
        var argsArray = Array.prototype.slice.call(arguments);
            argsArray.forEach(function(item){
                total += item;
            })

        return total
    };

    var calc = curring(multi);

    calc(1,2)(3)(4,5,6);

    console.log(calc()); //空白調用時才真正計算

這樣 calc = currying(multi),調用很是清晰.
若是要 累加多個值,能夠把多個值做爲作個參數 calc(1,2,3),也能夠支持鏈式的調用,如 calc(1)(2)(3);

到這裏, 不難看出,柯里化函數具備如下特色:

  • 函數能夠做爲參數傳遞
  • 函數可以做爲函數的返回值
  • 閉包

說了這麼多,柯里化函數到底可以幫我作什麼,或者說,我爲何要用柯里化函數呢? 咱們接着往下看。

柯里化函數的做用


函數柯里化容許和鼓勵你分隔複雜功能變成更小更容易分析的部分。這些小的邏輯單元顯然是更容易理解和測試的,而後你的應用就會變成乾淨而整潔的組合,由一些小單元組成的組合。

1.提升通用性
function square(i) {
    return i * i;
}

function double(i) {
    return i *= 2;
}

function map(handeler, list) {
    return list.map(handeler);
}

// 數組的每一項平方
map(square, [1, 2, 3, 4, 5]);
map(square, [6, 7, 8, 9, 10]);
map(square, [10, 20, 30, 40, 50]);
// ......

// 數組的每一項加倍
map(double, [1, 2, 3, 4, 5]);
map(double, [6, 7, 8, 9, 10]);
map(double, [10, 20, 30, 40, 50]);

例子中,建立了一個map通用函數,用於適應不一樣的應用場景。顯然,通用性不用懷疑。同時,例子中重複傳入了相同的處理函數:square和dubble。

應用中這種可能會更多。固然,通用性的加強必然帶來適用性的減弱。可是,咱們依然能夠在中間找到一種平衡。

看下面,咱們利用柯里化改造一下:

function currying(fn) {
            var slice = Array.prototype.slice,
            __args = slice.call(arguments, 1);
            return function () {
                var __inargs = slice.call(arguments);
                return fn.apply(null, __args.concat(__inargs));
            };
        }

function square(i) {
    return i * i;
}

function double(i) {
    return i *= 2;
}

function map(handeler, list) {
    return list.map(handeler);
}

var mapSQ = currying(map, square);
mapSQ([1, 2, 3, 4, 5]); //[1, 4, 9, 16, 25]


var mapDB = currying(map, double);
mapDB([1, 2, 3, 4, 5]); //[2, 4, 6, 8, 10]

咱們縮小了函數的適用範圍,但同時提升函數的適性.

2 延遲執行。

柯里化的另外一個應用場景是延遲執行。不斷的柯里化,累積傳入的參數,最後執行。

3.固定易變因素。

柯里化特性決定了它這應用場景。提早把易變因素,傳參固定下來,生成一個更明確的應用函數。最典型的表明應用,是bind函數用以固定this這個易變對象。
Function.prototype.bind = function(ctx) {
    var fn = this;
    return function() {
        fn.apply(ctx, arguments);
    };
};

Function.prototype.bind 方法也是柯里化應用與 call/apply 方法直接執行不一樣,bind 方法 將第一個參數設置爲函數執行的上下文,其餘參數依次傳遞給調用方法(函數的主體自己不執行,能夠當作是延遲執行),並動態建立返回一個新的函數, 這符合柯里化特色。

var foo = {
        x: 666
    };
    
var bar = function () {
    console.log(this.x);
}.bind(foo); // 綁定

bar(); //666

    // 下面是一個 bind 函數的模擬,testBind 建立並返回新的函數,在新的函數中將真正要執行業務的函數綁定到實參傳入的上下文,延遲執行了。
    Function.prototype.testBind = function (scope) {
        var self = this;   // this 指向的是調用 testBind 方法的一個函數, 
        return function () {
            return self.apply(scope);
        }
    };

    var testBindBar = bar.testBind(foo);  // 綁定 foo,延遲執行
    console.log(testBindBar); // Function (可見,bind以後返回的是一個延遲執行的新函數)
    testBindBar(); // 666

關於curry性能的備註


一般,使用柯里化會有一些開銷。取決於你正在作的是什麼,可能會或不會,以明顯的方式影響你。也就是說,幾乎大多數狀況,你的代碼的擁有性能瓶頸首先來自其餘緣由,而不是這個。

有關性能,這裏有一些事情必須牢記於心:

  • 存取arguments對象一般要比存取命名參數要慢一點.
  • 一些老版本的瀏覽器在arguments.length的實現上是至關慢的.
  • 建立大量嵌套做用域和閉包函數會帶來花銷,不管是在內存仍是速度上.

以上 ;)

最後

以爲內容有幫助能夠關注下個人公衆號 「 前端e進階 」,掌握最新動態,一塊兒學習成長!

clipboard.png

參考資料


http://blog.jobbole.com/77956/
http://www.cnblogs.com/pigtai...

相關文章
相關標籤/搜索