-
Notifications
You must be signed in to change notification settings - Fork 2
JavaScript中的不完全函数调用
原文:http://benalman.com/news/2012/09/partial-application-in-javascript/#functions 翻译:jnotnull
除非你使用另一种函数式编程语言,比如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)));
};
}
下面是使用partial函数的例子
// Add all arguments passed in by iterating over the `arguments` object.
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
// More specific functions.
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
这个能够运行,是因为传递的参数,减去第一个fn参数,都被存储在参数数组中。arguments对象会在函数调用的时候生成。每当返回的函数被调用后,它就会通过apply调用传递过去的fn方法。因为apply()接受一个数组作为参数,这就是的触发fun函数可以带上任意多个参数了。
“全”应用
值得注意的是,不完全调用只是在函数参数不固定的时候有用,如果传递的参数固定,那就没有必要做不完全调用了。
function add(a, b) {
return a + b;
}
var alwaysNine = partial(add, 4, 5);
alwaysNine(); // 9
alwaysNine(1); // 9 - this is just like calling add(4, 5, 1)
alwaysNine(9001); // 9 - this is just like calling add(4, 5, 9001)
在JS中,如果你传递的参数多于函数定义的,那多余的部分就会被忽略掉(除非通过arguments对象访问)。正因为如此,不论你传递多少个参数,不论你传递多少个参数给alwaysNine函数,结果不会变的。
Partial Application: From the Right
Until this point, all examples of partial application have only shown one specific variation of partial application, in which the leftmost function arguments are bound. And while this is the most commonly seen variation of partial application, it’s not the only one.
Using very similar code as with the partial function, it’s easy to make a partialRight function that binds the rightmost function arguments. In fact, all that needs to be changed is the order in which the originally-specified arguments are concatenated with the just-specified arguments.
The following partialRight function returns a function ƒ that, when invoked, invokes the fn function with the arguments passed to ƒ, followed by all the originally-specified (bound) arguments.
function partialRight(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 just-
// specified arguments, followed by any originally-specified arguments.
return fn.apply(this, slice.call(arguments, 0).concat(args));
};
}
And here’s a somewhat silly example of partial application, highlighting the differences between the partial and partialRight functions:
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."
The only problem with this partialRight implementation is that if too many arguments are passed into the partially applied function, the originally-specified (bound) arguments will be displaced, thus rendering them useless.
joeReceivesWedgie('Bob', 'Fred'); // "Bob gives Fred a wedgie."
While a more robust example could be written to take the function’s arity (the number of arguments a function takes) into consideration, it will add additional complexity.
In JavaScript, partially applying from the left will always be simpler and more robust than partially applying from the right.