JavaScript專題之函數柯里化

JavaScript 專題系列第十三篇,講解函數柯里化以及如何實現一個 curry 函數git

定義

維基百科中對柯里化 (Currying) 的定義爲:github

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.ajax

翻譯成中文:app

在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。函數

舉個例子:post

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

// 執行 add 函數,一次傳入兩個參數便可
add(1, 2) // 3

// 假設有一個 curry 函數能夠作到柯里化
var addCurry = curry(add);
addCurry(1)(2) // 3

用途

咱們會講到如何寫出這個 curry 函數,而且會將這個 curry 函數寫的很強大,可是在編寫以前,咱們須要知道柯里化到底有什麼用?this

舉個例子:lua

// 示意而已
function ajax(type, url, data) {
    var xhr = new XMLHttpRequest();
    xhr.open(type, url, true);
    xhr.send(data);
}

// 雖然 ajax 這個函數很是通用,但在重複調用的時候參數冗餘
ajax('POST', 'www.test.com', "name=kevin")
ajax('POST', 'www.test2.com', "name=kevin")
ajax('POST', 'www.test3.com', "name=kevin")

// 利用 curry
var ajaxCurry = curry(ajax);

// 以 POST 類型請求數據
var post = ajaxCurry('POST');
post('www.test.com', "name=kevin");

// 以 POST 類型請求來自於 www.test.com 的數據
var postFromTest = post('www.test.com');
postFromTest("name=kevin");

想一想 jQuery 雖然有 $.ajax 這樣通用的方法,可是也有 $.get 和 $.post 的語法糖。(固然 jQuery 底層是不是這樣作的,我就沒有研究了)。url

curry 的這種用途能夠理解爲:參數複用。本質上是下降通用性,提升適用性。prototype

但是即使如此,是否是依然感受沒什麼用呢?

若是咱們僅僅是把參數一個一個傳進去,意義可能不大,可是若是咱們是把柯里化後的函數傳給其餘函數好比 map 呢?

舉個例子:

好比咱們有這樣一段數據:

var person = [{name: 'kevin'}, {name: 'daisy'}]

若是咱們要獲取全部的 name 值,咱們能夠這樣作:

var name = person.map(function (item) {
    return item.name;
})

不過若是咱們有 curry 函數:

var prop = curry(function (key, obj) {
    return obj[key]
});

var name = person.map(prop('name'))

咱們爲了獲取 name 屬性還要再編寫一個 prop 函數,是否是又麻煩了些?

可是要注意,prop 函數編寫一次後,之後能夠屢次使用,實際上代碼從本來的三行精簡成了一行,並且你看代碼是否是更加易懂了?

person.map(prop('name')) 就好像直白的告訴你:person 對象遍歷(map)獲取(prop) name 屬性。

是否是感受有點意思了呢?

初版

將來咱們會接觸到更多有關柯里化的應用,不過那是將來的事情了,如今咱們該編寫這個 curry 函數了。

一個常常會看到的 curry 函數的實現爲:

// 初版
var curry = function (fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(this, newArgs);
    };
};

咱們能夠這樣使用:

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

var addCurry = curry(add, 1, 2);
addCurry() // 3
//或者
var addCurry = curry(add, 1);
addCurry(2) // 3
//或者
var addCurry = curry(add);
addCurry(1, 2) // 3

已經有柯里化的感受了,可是尚未達到要求,不過咱們能夠把這個函數用做輔助函數,幫助咱們寫真正的 curry 函數。

第二版

// 第二版
function sub_curry(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        return fn.apply(this, args.concat([].slice.call(arguments)));
    };
}

function curry(fn, length) {

    length = length || fn.length;

    var slice = Array.prototype.slice;

    return function() {
        if (arguments.length < length) {
            var combined = [fn].concat(slice.call(arguments));
            return curry(sub_curry.apply(this, combined), length - arguments.length);
        } else {
            return fn.apply(this, arguments);
        }
    };
}

咱們驗證下這個函數:

var fn = curry(function(a, b, c) {
    return [a, b, c];
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

效果已經達到咱們的預期,然而這個 curry 函數的實現好難理解吶……

爲了讓你們更好的理解這個 curry 函數,我給你們寫個極簡版的代碼:

function sub_curry(fn){
    return function(){
        return fn()
    }
}

function curry(fn, length){
    length = length || 4;
    return function(){
        if (length > 1) {
            return curry(sub_curry(fn), --length)
        }
        else {
            return fn()
        }
    }
}

var fn0 = function(){
    console.log(1)
}

var fn1 = curry(fn0)

fn1()()()() // 1

你們先從理解這個 curry 函數開始。

當執行 fn1() 時,函數返回:

curry(sub_curry(fn0))
// 至關於
curry(function(){
    return fn0()
})

當執行 fn1()() 時,函數返回:

curry(sub_curry(function(){
    return fn0()
}))
// 至關於
curry(function(){
    return (function(){
        return fn0()
    })()
})
// 至關於
curry(function(){
    return fn0()
})

當執行 fn1()()() 時,函數返回:

// 跟 fn1()() 的分析過程同樣
curry(function(){
    return fn0()
})

當執行 fn1()()()() 時,由於此時 length > 2 爲 false,因此執行 fn():

fn()
// 至關於
(function(){
    return fn0()
})()
// 至關於
fn0()
// 執行 fn0 函數,打印 1

再回到真正的 curry 函數,咱們如下面的例子爲例:

var fn0 = function(a, b, c, d) {
    return [a, b, c, d];
}

var fn1 = curry(fn0);

fn1("a", "b")("c")("d")

當執行 fn1("a", "b") 時:

fn1("a", "b")
// 至關於
curry(fn0)("a", "b")
// 至關於
curry(sub_curry(fn0, "a", "b"))
// 至關於
// 注意 ... 只是一個示意,表示該函數執行時傳入的參數會做爲 fn0 後面的參數傳入
curry(function(...){
    return fn0("a", "b", ...)
})

當執行 fn1("a", "b")("c") 時,函數返回:

curry(sub_curry(function(...){
    return fn0("a", "b", ...)
}), "c")
// 至關於
curry(function(...){
    return (function(...) {return fn0("a", "b", ...)})("c")
})
// 至關於
curry(function(...){
     return fn0("a", "b", "c", ...)
})

當執行 fn1("a", "b")("c")("d") 時,此時 arguments.length < length 爲 false ,執行 fn(arguments),至關於:

(function(...){
    return fn0("a", "b", "c", ...)
})("d")
// 至關於
fn0("a", "b", "c", "d")

函數執行結束。

因此,其實整段代碼又很好理解:

sub_curry 的做用就是用函數包裹原函數,而後給原函數傳入以前的參數,當執行 fn0(...)(...) 的時候,執行包裹函數,返回原函數,而後再調用 sub_curry 再包裹原函數,而後將新的參數混合舊的參數再傳入原函數,直到函數參數的數目達到要求爲止。

若是要明白 curry 函數的運行原理,你們仍是要動手寫一遍,嘗試着分析執行步驟。

更易懂的實現

固然了,若是你以爲仍是沒法理解,你能夠選擇下面這種實現方式,能夠實現一樣的效果:

function curry(fn, args) {
    length = fn.length;

    args = args || [];

    return function() {

        var _args = args.slice(0),

            arg, i;

        for (i = 0; i < arguments.length; i++) {

            arg = arguments[i];

            _args.push(arg);

        }
        if (_args.length < length) {
            return curry.call(this, fn, _args);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}


var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

或許你們以爲這種方式更好理解,又能實現同樣的效果,爲何不直接就講這種呢?

由於想給你們介紹各類實現的方法嘛,不能由於難以理解就不給你們介紹吶~

第三版

curry 函數寫到這裏其實已經很完善了,可是注意這個函數的傳參順序必須是從左到右,根據形參的順序依次傳入,若是我不想根據這個順序傳呢?

咱們能夠建立一個佔位符,好比這樣:

var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", _, "c")("b") // ["a", "b", "c"]

咱們直接看第三版的代碼:

// 第三版
function curry(fn, args, holes) {
    length = fn.length;

    args = args || [];

    holes = holes || [];

    return function() {

        var _args = args.slice(0),
            _holes = holes.slice(0),
            argsLen = args.length,
            holesLen = holes.length,
            arg, i, index = 0;

        for (i = 0; i < arguments.length; i++) {
            arg = arguments[i];
            // 處理相似 fn(1, _, _, 4)(_, 3) 這種狀況,index 須要指向 holes 正確的下標
            if (arg === _ && holesLen) {
                index++
                if (index > holesLen) {
                    _args.push(arg);
                    _holes.push(argsLen - 1 + index - holesLen)
                }
            }
            // 處理相似 fn(1)(_) 這種狀況
            else if (arg === _) {
                _args.push(arg);
                _holes.push(argsLen + i);
            }
            // 處理相似 fn(_, 2)(1) 這種狀況
            else if (holesLen) {
                // fn(_, 2)(_, 3)
                if (index >= holesLen) {
                    _args.push(arg);
                }
                // fn(_, 2)(1) 用參數 1 替換佔位符
                else {
                    _args.splice(_holes[index], 1, arg);
                    _holes.splice(index, 1)
                }
            }
            else {
                _args.push(arg);
            }

        }
        if (_holes.length || _args.length < length) {
            return curry.call(this, fn, _args, _holes);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}

var _ = {};

var fn = curry(function(a, b, c, d, e) {
    console.log([a, b, c, d, e]);
});

// 驗證 輸出所有都是 [1, 2, 3, 4, 5]
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5)

寫在最後

至此,咱們已經實現了一個強大的 curry 函數,但是這個 curry 函數符合柯里化的定義嗎?柯里化但是將一個多參數的函數轉換成多個單參數的函數,可是如今咱們不只能夠傳入一個參數,還能夠一次傳入兩個參數,甚至更多參數……這看起來更像一個柯里化 (curry) 和偏函數 (partial application) 的綜合應用,但是什麼又是偏函數呢?下篇文章會講到。

專題系列

JavaScript專題系列目錄地址:https://github.com/mqyqingfeng/Blog

JavaScript專題系列預計寫二十篇左右,主要研究平常開發中一些功能點的實現,好比防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特色是研(chao)究(xi) underscore 和 jQuery 的實現方式。

若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。

相關文章
相關標籤/搜索