JS柯里化

什么是柯里化?

官方的說法

在計(jì)算機(jī)科學(xué)中,柯里化(英語:Currying,又譯為卡瑞化加里化,是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。這個(gè)技術(shù)由克里斯托弗·斯特雷奇以邏輯學(xué)家哈斯凱爾·加里命名的,盡管它是Moses Sch?nfinkel戈特洛布·弗雷格發(fā)明的。

  • 在直覺上,柯里化聲稱如果你固定某些參數(shù),你將得到接受余下參數(shù)的一個(gè)函數(shù)。
  • 在理論計(jì)算機(jī)科學(xué)中,柯里化提供了在簡單的理論模型中,比如:只接受一個(gè)單一參數(shù)的lambda演算中,研究帶有多個(gè)參數(shù)的函數(shù)的方式。
  • 函數(shù)柯里化的對(duì)偶是Uncurrying,一種使用匿名單參數(shù)函數(shù)來實(shí)現(xiàn)多參數(shù)函數(shù)的方法。

方便的理解

Currying概念其實(shí)很簡單,只傳遞給函數(shù)一部分參數(shù)來調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)。

如果我們需要實(shí)現(xiàn)一個(gè)求三個(gè)數(shù)之和的函數(shù):

function add(x, y, z) {
  return x + y + z;
}
console.log(add(1, 2, 3)); // 6
var add = function(x) {
  return function(y) {
    return function(z) {
      return x + y + z;
    }
  }
}

var addOne = add(1);
var addOneAndTwo = addOne(2);
var addOneAndTwoAndThree = addOneAndTwo(3);

console.log(addOneAndTwoAndThree);
  • 這里我們定義了一個(gè)add函數(shù),它接受一個(gè)參數(shù)并返回一個(gè)新的函數(shù)。調(diào)用add之后,返回的函數(shù)就通過閉包的方式記住了add的第一個(gè)參數(shù)。一次性地調(diào)用它實(shí)在是有點(diǎn)繁瑣,好在我們可以使用一個(gè)特殊的curry幫助函數(shù)(helper function)使這類函數(shù)的定義和調(diào)用更加容易。

ES6的箭頭函數(shù),我們可以將上面的add實(shí)現(xiàn)成這樣:

const add = x => y => z => x + y + z;

好像使用箭頭函數(shù)更清晰了許多。

偏函數(shù)?

來看這個(gè)函數(shù):

function ajax(url, data, callback) {
  // ..
}

有這樣的一個(gè)場景:我們需要對(duì)多個(gè)不同的接口發(fā)起HTTP請(qǐng)求,有下列兩種做法:

  • 在調(diào)用ajax()函數(shù)時(shí),傳入全局URL常量。
  • 創(chuàng)建一個(gè)已經(jīng)預(yù)設(shè)URL實(shí)參的函數(shù)引用。

下面我們創(chuàng)建一個(gè)新函數(shù),其內(nèi)部仍然發(fā)起ajax()請(qǐng)求,此外在等待接收另外兩個(gè)實(shí)參的同時(shí),我們手動(dòng)將ajax()第一個(gè)實(shí)參設(shè)置成你關(guān)心的API地址。

對(duì)于第一種做法,我們可能產(chǎn)生如下調(diào)用方式:

function ajaxTest1(data, callback) {
  ajax('http://www.test.com/test1', data, callback);
}

function ajaxTest2(data, callback) {
  ajax('http://www.test.com/test2', data, callback);
}

對(duì)于這兩個(gè)類似的函數(shù),我們還可以提取出如下的模式:

function beginTest(callback) {
  ajaxTest1({
    data: GLOBAL_TEST_1,
  }, callback);
}
  • 相信您已經(jīng)看到了這樣的模式:我們?cè)诤瘮?shù)調(diào)用現(xiàn)場(function call-site),將實(shí)參應(yīng)用(apply) 于形參。如你所見,我們一開始僅應(yīng)用了部分實(shí)參 —— 具體是將實(shí)參應(yīng)用到URL形參 —— 剩下的實(shí)參稍后再應(yīng)用。

上述概念即為偏函數(shù)的定義,偏函數(shù)一個(gè)減少函數(shù)參數(shù)個(gè)數(shù)的過程;這里的參數(shù)個(gè)數(shù)指的是希望傳入的形參的數(shù)量。我們通過ajaxTest1()把原函數(shù)ajax()的參數(shù)個(gè)數(shù)從3個(gè)減少到了2個(gè)。

我們這樣定義一個(gè)partial()函數(shù):

function partial(fn, ...presetArgs) {
  return function partiallyApplied(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  }
}
  • partial()函數(shù)接收fn參數(shù),來表示被我們偏應(yīng)用實(shí)參(partially apply)的函數(shù)。接著,fn形參之后,presetArgs數(shù)組收集了后面?zhèn)魅氲膶?shí)參,保存起來稍后使用。

  • 我們創(chuàng)建并return了一個(gè)新的內(nèi)部函數(shù)(為了清晰明了,我們把它命名為partiallyApplied(..)),該函數(shù)中,laterArgs數(shù)組收集了全部實(shí)參。

使用箭頭函數(shù),則更為簡潔:

var partial =  (fn, ...presetArgs) =>(...laterArgs) =>fn(...presetArgs, ...laterArgs);

使用偏函數(shù)的這種模式,我們重構(gòu)之前的代碼:

function ajax(url, data, callback) {
  // ..
}

var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');
var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');

再次思考beginTest()函數(shù),我們使用partial()來重構(gòu)它應(yīng)該怎么做呢?

function ajax(url, data, callback) {
  // ..
}

// 版本1
var beginTest = partial(ajax, 'http://www.test.com/test1', {
  data: GLOBAL_TEST_1,
});

// 版本2
var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');
var beginTest = partial(ajaxTest1, {
  data: GLOBAL_TEST_1,
});

一次傳一個(gè)

相信你已經(jīng)在上述例子中看到了版本2比起版本1的優(yōu)勢所在了,沒錯(cuò),柯里化就是:將一個(gè)帶有多個(gè)參數(shù)的函數(shù)轉(zhuǎn)換為一次一個(gè)的函數(shù)的過程。每次調(diào)用函數(shù)時(shí),它只接受一個(gè)參數(shù),并返回一個(gè)函數(shù),直到傳遞所有參數(shù)為止。

The process of converting a function that takes multiple arguments into a function that takes them one at a time.Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.

假設(shè)我們已經(jīng)創(chuàng)建了一個(gè)柯里化版本的ajax()函數(shù)curriedAjax()

curriedAjax('http://www.test.com/test1')({
  data: GLOBAL_TEST_1,
})
(function callback(data) {
  // dosomething
});

我們將三次調(diào)用分別拆解開來,這也許有助于我們理解整個(gè)過程:

var ajaxTest1 = curriedAjax('http://www.test.com/test1');

var beginTest = ajaxTest1({
  data: GLOBAL_TEST_1,
});

var ajaxCallback = beginTest(function callback(data) {
  // dosomething
});

實(shí)現(xiàn)柯里化

那么,我們?nèi)绾蝸韺?shí)現(xiàn)一個(gè)自動(dòng)的柯里化的函數(shù)呢?

var currying = function(fn) {
  var args = [];

  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 沒傳參數(shù)時(shí),調(diào)用這個(gè)函數(shù)
    } else {
      [].push.apply(args, arguments); // 傳入了參數(shù),把參數(shù)保存下來
      return arguments.callee; // 返回這個(gè)函數(shù)的引用
    }
  }
}

調(diào)用上述currying()函數(shù):

var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  }
})();

var cost = currying(cost);

cost(100); // 傳入了參數(shù),不真正求值
cost(200); // 傳入了參數(shù),不真正求值
cost(300); // 傳入了參數(shù),不真正求值

console.log(cost()); // 求值并且輸出600

我們?cè)谑褂每吕锘瘯r(shí),要注意同時(shí)為函數(shù)預(yù)傳的參數(shù)的情況。

因此把上述柯里化函數(shù)更改如下:

var currying = function(fn) {
  var args = Array.prototype.slice.call(arguments, 1);
  
  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 沒傳參數(shù)時(shí),調(diào)用這個(gè)函數(shù)
    } else {
      [].push.apply(args, arguments); // 傳入了參數(shù),把參數(shù)保存下來
      return arguments.callee; // 返回這個(gè)函數(shù)的引用
    }
  }
}

使用實(shí)例:

var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  }
})();

var cost = currying(cost, 100);
cost(200); // 傳入了參數(shù),不真正求值
cost(300); // 傳入了參數(shù),不真正求值

console.log(cost()); // 求值并且輸出600

你可能會(huì)覺得每次都要在最后調(diào)用一下不帶參數(shù)的cost()函數(shù)比較麻煩,并且在cost()函數(shù)都要使用arguments參數(shù)不符合你的預(yù)期。我們知道函數(shù)都有一個(gè)length屬性,表明函數(shù)期望接受的參數(shù)個(gè)數(shù)。因此我們可以充分利用預(yù)傳參數(shù)的這個(gè)特點(diǎn)。

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);
    }
  };
}

在上述函數(shù)中,我們?cè)赾urrying的返回函數(shù)中,每次把arguments.lengthfn.length作比較,一旦arguments.length達(dá)到了fn.length的數(shù)量,我們就去調(diào)用fn(return fn.apply(this, arguments);)

驗(yàn)證:

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"]

bind方法的實(shí)現(xiàn)

使用柯里化,能夠很方便地借用call()或者apply()實(shí)現(xiàn)bind()方法的polyfill。

Function.prototype.bind = Function.prototype.bind || function(context) {
  var me = this;
  var args = Array.prototype.slice.call(arguments, 1);
  return function() {
    var innerArgs = Array.prototype.slice.call(arguments);
    var finalArgs = args.concat(innerArgs);
    return me.apply(contenxt, finalArgs);
  }
}
  • 上述函數(shù)有的問題在于不能兼容構(gòu)造函數(shù)。我們通過判斷this指向的對(duì)象的原型屬性,來判斷這個(gè)函數(shù)是否通過new作為構(gòu)造函數(shù)調(diào)用,來使得上述bind方法兼容構(gòu)造函數(shù)。

  • 綁定函數(shù)適用于用new操作符 new 去構(gòu)造一個(gè)由目標(biāo)函數(shù)創(chuàng)建的新的實(shí)例。當(dāng)一個(gè)綁定函數(shù)是用來構(gòu)建一個(gè)值的,原來提供的this 就會(huì)被忽略。然而, 原先提供的那些參數(shù)仍然會(huì)被前置到構(gòu)造函數(shù)調(diào)用的前面。

這是基于MVCJavaScript Web富應(yīng)用開發(fā)的bind()方法實(shí)現(xiàn):

Function.prototype.bind = function(oThis) {
  if (typeof this !== "function") {
    throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  }

  var aArgs = Array.prototype.slice.call(arguments, 1),
      fToBind = this,
      fNOP = function() {},
  var fBound = function() {
        return fToBind.apply(
          this instanceof fNOP && oThis ? this : oThis || window,aArgs.concat(Array.prototype.slice.call(arguments))
        );
  };
  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();
  return fBound;
};

反柯里化(uncurrying)

可能遇到這種情況:拿到一個(gè)柯里化后的函數(shù),卻想要它柯里化之前的版本,這本質(zhì)上就是想將類似f(1)(2)(3)的函數(shù)變回類似g(1,2,3)的函數(shù)。

下面是簡單的uncurrying的實(shí)現(xiàn)方式:

function uncurrying(fn) {
  return function(...args) {
    var ret = fn;
    for (let i = 0; i < args.length; i++) {
      ret = ret(args[i]); // 反復(fù)調(diào)用currying版本的函數(shù)
    }
    return ret; // 返回結(jié)果
  };
}

注意,不要以為uncurrying后的函數(shù)和currying之前的函數(shù)一模一樣,它們只是行為類似!

var currying = function(fn) {
  var args = Array.prototype.slice.call(arguments, 1);

  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 沒傳參數(shù)時(shí),調(diào)用這個(gè)函數(shù)
    } else {
      [].push.apply(args, arguments); // 傳入了參數(shù),把參數(shù)保存下來
      return arguments.callee; // 返回這個(gè)函數(shù)的引用
    }
  }
}

function uncurrying(fn) {
  return function(...args) {
    var ret = fn;

    for (let i = 0; i < args.length; i++) {
      ret = ret(args[i]); // 反復(fù)調(diào)用currying版本的函數(shù)
    }
    return ret; // 返回結(jié)果
  };
}

var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  }
})();

var curryingCost = currying(cost);
var uncurryingCost = uncurrying(curryingCost);
console.log(uncurryingCost(100, 200, 300)()); // 600

柯里化或偏函數(shù)有什么用?

無論是柯里化還是偏應(yīng)用,我們都能進(jìn)行部分傳值,而傳統(tǒng)函數(shù)調(diào)用則需要預(yù)先確定所有實(shí)參。如果你在代碼某一處只獲取了部分實(shí)參,然后在另一處確定另一部分實(shí)參,這個(gè)時(shí)候柯里化和偏應(yīng)用就能派上用場。

另一個(gè)最能體現(xiàn)柯里化應(yīng)用的的是,當(dāng)函數(shù)只有一個(gè)形參時(shí),我們能夠比較容易地組合它們(單一職責(zé)原則(Single responsibility principle))。因此,如果一個(gè)函數(shù)最終需要三個(gè)實(shí)參,那么它被柯里化以后會(huì)變成需要三次調(diào)用,每次調(diào)用需要一個(gè)實(shí)參的函數(shù)。當(dāng)我們組合函數(shù)時(shí),這種單元函數(shù)的形式會(huì)讓我們處理起來更簡單。

歸納下來,主要為以下常見的三個(gè)用途:

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

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

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