Skip to content

JavaScript中的不完全函数调用

jnotnull edited this page Jul 16, 2014 · 15 revisions

除非你使用另一种函数式编程语言,比如ML或者Haskell,否则,你可能对不完全调用函数调用和柯里化函数并不熟悉。但是一旦你理解了这些概念,你就可以在自己的代码中使用它们了。

注:这篇文章之前发布在MSDN上,但是已经被大幅重写了。这个最新版本更加准确。

函数
如果你是新手,即使你已经明白了js方法如何返回方法,并且也了解了传递方法作为函数参数,我还是建议你阅读第一章节。如果你已经掌握了这些知识,那可以跳过这个章节直接去阅读不完全函数调用章节
现在让我们潜下心来看个非常简单的例子:

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

  add(1, 2);    // 3
  add(1, 3);    // 4
  add(1, 10);   // 11
  add(1, 9000); // 9001

这个例子很简单,设想一下,你必须重复的调用一个函数,传递的第一个参数又是一样的。因为不必要的重复传递是导致错误的主要因素,所以一个方法就是你把重复的常量提取出来放到一个变量中,然后再在进行调用。

function add(a, b) {
    return a + b;
  }
var value = 1;
add(value, 2);    // 3
add(value, 3);    // 4
add(value, 10);   // 11
add(value, 9000); // 9001

如你所见,使用变量替换使得代码具有更强的可修改性,当然也就更加可维护了。但是,这个代码还是有一些不必要的重复。如果新增一个固定的取值能够被执行无数次,那将需要我们去创建一个具有内置行为的专门函数。

函数调用函数
无论你是自己写代码,还是提供API,如果创建一个针对通用功能的专有包装函数,将会非常有用的。它可以被传递给到函数中并被执行。
一个方法就是准对通用功能,手工定义一个更加专有的函数。这在javascript中非常容易,因为函数可以调用其他函数

// More general function.
  function add(a, b) {
    return a + b;
  }

  add(1, 2);  // 3
  add(10, 3); // 13

  // More specific functions.
  function addOne(b) {
    return add(1, b);
  }

  addOne(2);  // 3
  addOne(3);  // 4

  function addTen(b) {
    return add(10, b);
  }

  addTen(2);  // 12
  addTen(3);  // 13

通过这种方式定义的专有函数非常容易和直观,如果你拥有太多这样的函数,那就需要维护一些额外的代码了。

函数返回函数
这里你可以看到函数makeAdder,当被以传递一个变量调用的时候,它返回了一个新的函数(函数生成其他函数或者对象一般被认为是工厂)。返回的函数会加上传递的参数到之前制定的函数参数值,得到最终的结果。

// More general function.
  function add(a, b) {
    return a + b;
  }
add(1, 2);  // 3
add(10, 3); // 13
// More specific function generator.
function makeAdder(a) {
return function(b) {
return a + b;
};
}
// More specific functions.
var addOne = makeAdder(1);
addOne(2);  // 3
addOne(3);  // 4
var addTen = makeAdder(10);
addTen(2);  // 12
addTen(3);  // 13

在JS中可以这么做,因为JS支持闭包。闭包允许函数访问外层变量,甚至是当前运行环境之外的变量。 同时在JS中,函数是一类公民(译者注:first-class是指函数能够被当成参数进行传递到函数中),正因为如此,函数可以接受函数作为参数,也能够返回函数。闭包和函数经常一起配合,返回函数作为参数继续进行运算。

这个例子提供了一个方便的渠道,使你直接调用addOne(2)而不是add(1, 2),但是这个不是没有代价的。首先,通用增加函数和makeAdder 工厂的实际加逻辑是重复的。这样的代码就不够DRY了。第二,通过这种方式处理的不同函数,你需要手工创建一个工厂。

函数接收函数
下一步就是创建一个更加通用的工厂函数。这个工厂函数不仅仅可以接收一个参数,而且还可以接收一个函数,并且能够运行改函数。
(函数当成参数传递给其他函数一般指回调)。
通过这种方式,你能够创建一个简单的工厂函数去创建一个新的函数。
注意:原始函数不会被修改,他们行为不会变。他们可以可以被wrapper函数轻松的引用到并且被调用。

// Relatively flexible, more specific function generator.
  function bindFirstArg(fn, a) {
    return function(b) {
      return fn(a, b);
    };
  }
// More general functions.
function add(a, b) {
return a + b;
}
add(1, 2);           // 3
function multiply(a, b) {
return a * b;
}
multiply(10, 2);     // 20
// More specific functions.
var addOne = bindFirstArg(add, 1);
addOne(2);           // 3
addOne(3);           // 4
addOne(10);          // 11
addOne(9000);        // 9001
var multiplyByTen = bindFirstArg(multiply, 10);
multiplyByTen(2);    // 20
multiplyByTen(3);    // 30
multiplyByTen(10);   // 100
multiplyByTen(9000); // 90000

这里特别有趣的是,bindFirstArg方法不仅仅可以绑定任何函数到第一个参数,同时还可以被用来绑定它自己到第一个参数。这里创建的就是一个可绑定函数。
设想一下:如果bindFirstArg函数能够绑定一个带有两个参数的函数作为一个参数,比如加1或者乘以10,bindFirstArg 函数本身带有两个参数,我们坚持认为bindFirstArg 函数可以绑定自身作为函数参数。
这里,我们创建了一个更加特别版本的bindFirstArg,add函数绑定了他它自身。

// More specific function generator.
  var makeAdder = bindFirstArg(bindFirstArg, add);
// More specific functions.
var addOne = makeAdder(1);
addOne(2); // 3
addOne(3); // 4
var addTen = makeAdder(10);
addTen(2); // 12
addTen(3); // 13

熟悉么?非常熟悉。这个就是makeAdder函数,只是通过一个更加通用的途径创建了它

现在,bindFirstArg 比以前更加灵活。当然也只是稍微灵活了一点。如果你想去绑定多个函数怎么办?如果你有一个函数可以接受3个或者更多的参数,但是要根据场景去绑定第一个参数,或者前两个参数或者任何个数的参数,那该怎么办。
但是毕竟这个方案比之前更加灵活了,能够被通用了。

不完全调用
不完全调用可以概括为:这个函数额可以接受一些参数,这些参数中有一些参数可以被绑定成其他函数,然后返回一个新的函数,这个新的函数接收剩下的为绑定的参数。
意思就是说,随意给出一个函数,可以生成一个新的带有一个或者多个绑定参数的函数。如果你稍有留意的话,你会发现上面的例子已经展示了不完全调用了,尽管在某些方面还有些缺陷。
ECMAScript 5的bind方法允许一个函数同时拥有this和一些绑定的参数,如果你曾经使用过它,那么你对不完全调用就已经熟悉了。通过bind,发现this其实是一个含蓄的第一个参数—Extra Credit章节最后部分会展示很多关于bind的例子。
不完全调用: 从左侧开始
这个例子明显比前面的例子复杂多了,因为这里使用了arguments对象去动态判断绑定的参数数量。

注意,arguments对象是一个类似数组的对象,它在函数被调用的时候生成,同时也只能在函数内部访问,它包含了所有传递进这个方法的参数。arguments是类数组,但是不是数组。这就意味这个它没有length属性和游标取值,也没有数组的方法,比如concat或者slice。为了转换arguments对象为数组,数组的slice方法被用来调用arguments去生成数组。
下面的partial函数返回了f函数。当它被调用时,会触发fn函数,该函数会被传递剩下的参数。

function partial(fn /*, args...*/) {
    // A reference to the Array#slice method.
    var slice = Array.prototype.slice;
    // Convert arguments object to an array, removing the first argument.
    var args = slice.call(arguments, 1);
return function() {
// Invoke the originally-specified function, passing in all originally-
// specified arguments, followed by any just-specified arguments.
return fn.apply(this, args.concat(slice.call(arguments, 0)));
};
}
Clone this wiki locally