Partial Application in JavaScript

2014-11-13 W.Y. 更多博文 » 博客 » GitHub »

Partial Application

原文链接 https://bubkoo.github.io/2014/11/13/Partial-Application-in-JavaScript/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


概述

Partial Application?不要被字面意思误解,这里要说的并不是 Application,而是 JavaScript 中的 function。可以这样来描述 Partial Application,一个接受多个参数的函数,预先给该函数绑定一些参数,并返回一个新的函数来接受剩下未绑定的参数。貌似有点像柯里化(currying)函数,但不尽然。

典型的柯里化函数定义如下:

Function.prototype.curry = function() {
    var fn = this, args = Array.prototype.slice.call(arguments);
    return function() {
      return fn.apply(this, args.concat(
        Array.prototype.slice.call(arguments)));
    };
};

上面代码预先绑定函数参数列表左侧的参数到新返回的函数中,新函数接受右侧剩下的参数,相比起来 Partial Application 更加灵活。 <!--more-->

分情况看看 Partial Application

From the Left

这里和上面的柯里化类似,预先绑定函数左侧的参数,调用时传入右侧剩下的参数:

function partial(fn /*, args...*/) {
  var slice = Array.prototype.slice;
  // 将参数转换为数组,除开第一个参数
  var args = slice.call(arguments, 1);

  return function() {
    // 调用原来的方法,并将参数拼接到预先绑定的参数后面
    return fn.apply(this, args.concat(slice.call(arguments, 0)));
  };
}

使用也比较简单:

// 将传入的所有参数求和
function addAllTheThings() {
  var sum = 0;
  for (var i = 0; i < arguments.length; i++) {
    sum += arguments[i];
  }
  return sum;
}

// 正常调用
addAllTheThings(1, 2);            // 3
addAllTheThings(1, 2, 3);         // 6
addAllTheThings(1, 4, 9, 16, 25); // 55

// 预先绑定左侧参数
var addOne = partial(addAllTheThings, 1);
addOne()                          // 1
addOne(2);                        // 3
addOne(2, 3);                     // 6
addOne(4, 9, 16, 25);             // 55

var addTen = partial(addAllTheThings, 1, 2, 3, 4);
addTen();                         // 10
addTen(2);                        // 12
addTen(2, 3);                     // 15
addTen(4, 9, 16, 25);             // 64

From the Right

实现方式类似:

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

  return function() {
    // 将剩下参数拼接在预先绑定参数的左侧 
    return fn.apply(this, slice.call(arguments, 0).concat(args));
  };
}

使用例子:

function wedgie(a, b) {
  return a + ' gives ' + b + ' a wedgie.';
}

var joeGivesWedgie = partial(wedgie, 'Joe');
joeGivesWedgie('Ron');    // "Joe gives Ron a wedgie."
joeGivesWedgie('Bob');    // "Joe gives Bob a wedgie."

var joeReceivesWedgie = partialRight(wedgie, 'Joe');
joeReceivesWedgie('Ron'); // "Ron gives Joe a wedgie."
joeReceivesWedgie('Bob'); // "Bob gives Joe a wedgie."

上面代码需要注意的是,如果使用时给函数传递不止一个参数,那么预先绑定的参数将不起任何作用。更加健壮的代码需要将函数参数的个数也考虑进来。

From Anywhere

上面两种情况预先绑定的参数和后传入的参数都要求有一定顺序,而我们可能需要随机替换参数中的某些值,为了达到这个目的我们可以给预绑定的参数赋值为某个占位符,函数实际调用时,再用传入的参数来替换这些占位符,请看下面代码:

var partialAny = (function() {

  var slice = Array.prototype.slice;

  function partialAny(fn /*, args...*/) {
    // 预先绑定的参数
    var orig = slice.call(arguments, 1);

    return function() {
      // 后面传入的参数
      var partial = slice.call(arguments, 0);
      var args = [];

      // 如果预绑定的参数为占位符,则用传入的参数替换
      for (var i = 0; i < orig.length; i++) {
        args[i] = orig[i] === partialAny._ ? partial.shift() : orig[i];
      }

      // 占位符替换结束后,将替换后的预绑定参数与剩余参数拼接为参数数组
      return fn.apply(this, args.concat(partial));
    };
  }

  // 定义参数占位符
  partialAny._ = {};

  return partialAny;
}());

请看实例:

function hex(r, g, b) {
  return '#' + r + g + b;
}

hex('11', '22', '33'); // "#112233"

// A more visually-appealing placeholder.
var __ = partialAny._;

var redMax = partialAny(hex, 'ff', __, __);
redMax('11', '22');    // "#ff1122"

var greenMax = partialAny(hex, __, 'ff');
greenMax('33', '44');  // "#33ff44"

var blueMax = partialAny(hex, __, __, 'ff');
blueMax('55', '66');   // "#5566ff"

var magentaMax = partialAny(hex, 'ff', __, 'ff');
magentaMax('77');      // "#ff77ff"

"Full" Application?

如果给一个函数预先绑定了所有参数,那么这里的 partial 就失去了意义,看下面例子:

function add(a, b) {
    // 这里没有使用 arguments,而是直接使用了形参
    return a + b;
}

// 这里已经绑定了所有参数
var alwaysNine = partial(add, 4, 5);
alwaysNine();     // 9
alwaysNine(1);    // 9 - 等于调用 add(4, 5, 1)
alwaysNine(9001); // 9 - 等于调用 add(4, 5, 9001)

使用 bind()

熟悉 bind() 的同学大概知道,bind() 方法不仅可以指定函数的执行上下文,还可以给函数预绑定一些参数:

var add = function (a, b) {
  return a + b;
};
var add2 = add.bind(null, 2);

add2(10) === 12;

我们通常的 DOM 事件绑定方式如下:

this.setup = function () {
  this.on('tweet', function (e, data) {
    this.handleStreamEvent('tweet', e, data);
  }.bind(this));
  this.on('retweet', function (e, data) {
    this.handleStreamEvent('retweet', e, data);
  }.bind(this));
};

如果 tweetretweet 事件回调的内部逻辑差不多,这样组织代码非常不错,但是,还是有一些冗余代码,两个绑定都需要创建一个匿名函数,并在匿名函数上调用 bind 来绑定 this,确保上下文,然后在匿名函数内部调用绑定方法。

其实我们有更简单的方式:

this.setup = function () {
  this.on('tweet', this.handleStreamEvent.bind(this, 'tweet'));
  this.on('retweet', this.handleStreamEvent.bind(this, 'retweet'));
};

代码非常清爽吧!这里,我们创建了两个 partially applied 的函数,绑定了 this,并分别预先传入 tweetretweet 两个参数,当事件触发时,再分别传入 edata 两个参数。

参考文章