函数式JavaScript(3):.apply()、.call() 和arguments对象
我们正在寻求调校JavaScript的方式,使得我们可以做些真正的函数式编程。为了做到这一点,详细理解函数调用和函数原型是非常有必要的。
这是一个(待定)关于使用JavaScript进行函数式编程系列的第三篇文章。如果你是刚刚加入的,你可以跳回去看看之前的文章。
- 第一部分:引言
- 第二部分:如何打造“函数式”编程语言
函数原型
现在,不管你是已经读了还是忽略掉上面的链接所对应的文章,我们准备继续前进!
如果我们点开了我们喜欢的浏览器+JavaScript控制台,让我们看一下Function.prototype对象的属性:
|
1
2
3
|
Object.getOwnPropertyNames(Function.prototype)//=> ["length", "name", "arguments", "caller", // "constructor", "bind", "toString", "call", "apply"] |
这里的输出依赖于你使用的浏览器和JavaScript版本。(我用的是Chrome 33)
我们看到一些我们感兴趣的几个属性。鉴于这篇文章的目的,我会讨论下这几个:
- Function.prototype.length
- Function.prototype.call
- Function.prototype.apply
第一个是个属性,另外两个是方法。除了这三个,我还会愿意讨论下这个特殊的变量arguments,它和Function.prototype.arguments(已被弃用)稍有不同。
首先,我将定义一个“tester”函数来帮助我们弄清楚发生了什么。
|
1
2
3
4
5
6
7
8
|
var tester = function (a, b, c){ console.log({ this: this, a: a, b: b, c: c });}; |
这个函数简单记录了输入参数的值,和“上下文变量”,即this的值。
现在,让我们尝试一些事情:
|
1
2
3
4
5
|
tester("a"); //=> {this: Window, a: "a", b: (undefined), c: (undefined)}tester("this", "is", "cool"); //=> {this: Window, a: "this", b: "is", c: "cool"} |
我们注意到如果我们不输入第2、3个参数,程序将会显示它们为undefined(未定义)。此外,我们注意到这个函数默认的“上下文”是全局对象window。
使用Function.prototype.call
一个函数的 .call 方法以这样的方式调用这个函数,它把上下文变量this设置为第一个输入参数的值,然后其他的的参数一个跟一个的也传进函数。
语法:
|
1
|
fn.call(thisArg[, arg1[, arg2[, ...]]]) |
因此,下面这两行是等效的:
|
1
2
|
tester("this", "is", "cool"); tester.call(window, "this", "is", "cool"); |
当然,我们能够随需传入任何参数:
|
1
2
|
tester.call("this?", "is", "even", "cooler"); //=> {this: "this?", a: "is", b: "even", c: "cooler"} |
这个方法主要的功能是设置你所调用函数的this变量的值。
使用Function.prototype.apply
函数的.apply方法比.call更实用一些。和.call类似,.apply的调用方式也是把上下文变量this设置为输入参数序列中的第一个参数的值。输入参数序列的第二个参数也是最后一个,以数组(或者类数组对象)的方式传入。
语法:
|
1
|
fun.apply(thisArg, [argsArray]) |
因此,下面三行全部等效:
|
1
2
3
|
tester("this", "is", "cool"); tester.call(window, "this", "is", "cool"); tester.apply(window, ["this", "is", "cool"]); |
能够以数组的方式指定一个参数列表在多数时候非常有用(我们会发现这样做的好处的)。
例如,Math.max是一个可变参数函数(一个函数可以接受任意数目的参数)。
|
1
2
3
4
5
|
Math.max(1,3,2);//=> 3Math.max(2,1);//=> 2 |
这样,如果我有一个数值数组,并且我需要利用Math.max函数找出其中最大的那个,我怎么用一行代码来做这个事儿呢?
|
1
2
3
|
var numbers = [3, 8, 7, 3, 1];Math.max.apply(null, numbers);//=> 8 |
The .apply method really starts to show it’s importance when coupled with the special arguments variable: The arguments object
.apply方法真正开始显示出它的重要是当配上特殊参数:Arguments对象。
每个函数表达式在它的作用域中都有一个特殊的、可使用的局部变量:arguments。为了研究它的属性,让我们创建另一个tester函数:
|
1
2
3
|
var tester = function(a, b, c) { console.log(Object.getOwnPropertyNames(arguments));}; |
注:在这种情况下我们必须像上面这样使用Object.getOwnPropertyNames,因为arguments有一些属性没有标记为可以被枚举的,于是如果仅仅使用console.log(arguments)这种方式它们将不会被显示出来。
现在我们按照老办法,通过调用tester函数来测试下:
|
1
2
3
4
5
|
tester("a", "b", "c");//=> ["0", "1", "2", "length", "callee"]tester.apply(null, ["a"]);//=> ["0", "length", "callee"] |
arguments变量的属性中包括了对应于传入函数的每个参数的属性,这些和.length属性、.callee属性没什么不同。
.callee属性提供了调用当前函数的函数的引用,但是这并不被所有的浏览器支持。就目前而言,我们忽略这个属性。
让我们重新定义一下我们的tester函数,让它丰富一点:
|
1
2
3
4
5
6
7
8
9
10
|
var tester = function() { console.log({ 'this': this, 'arguments': arguments, 'length': arguments.length });};tester.apply(null, ["a", "b", "c"]);//=> { this: null, arguments: { 0: "a", 1: "b", 2: "c" }, length: 3 } |
Arguments:是对象还是数组?
我们看得出,arguments完全不是一个数组,虽然多多少少有点像。在很多情况下,尽管不是,我们还是希望把它当作数组来处理。把arguments转换成一个数组,这有个非常不错的快捷小函数:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
function toArray(args) { return Array.prototype.slice.call(args);}var example = function(){ console.log(arguments); console.log(toArray(arguments));};example("a", "b", "c");//=> { 0: "a", 1: "b", 2: "c" }//=> ["a", "b", "c"] |
这里我们利用Array.prototype.slice方法把类数组对象转换成数组。因为这个,在与.apply同时使用的时候arguments对象最终会极其有用。
一些有用例子
Log Wrapper(日志包装器)
我们在上一篇文章中构建了logWrapper函数,但是它只是在一元函数下正确工作。
|
1
2
3
4
5
6
7
|
// old versionvar logWrapper = function (f) { return function (a) { console.log('calling "' + f.name + '" with argument "' + a); return f(a); };}; |
当然了,我们既有的知识让我们能够构建一个可以服务于任何函数的logWrapper函数:
|
1
2
3
4
5
6
7
|
// new versionvar logWrapper = function (f) { return function () { console.log('calling "' + f.name + '"', arguments); return f.apply(this, arguments); };}; |
通过调用
|
1
|
f.apply(this, arguments); |
我们确定这个函数f会在和它之前完全相同的上下文中被调用。于是,如果我们愿意用新的”wrapped”版本替换掉我们的代码中的那些日志记录函数是完全理所当然没有唐突感的。
把原生的prototype方法放到公共函数库中
浏览器有大量超有用的方法我们可以“借用”到我们的代码里。方法常常把this变量作为“data”来处理。在函数式编程,我们没有this变量,但是我们无论如何要使用函数的!
|
1
2
3
4
5
6
|
var demethodize = function(fn){ return function(){ var args = [].slice.call(arguments, 1); return fn.apply(arguments[0], args); };}; |
一些别的例子:
|
1
2
3
4
5
6
7
8
9
10
|
// String.prototypevar split = demethodize(String.prototype.split);var slice = demethodize(String.prototype.slice);var indexOfStr = demethodize(String.prototype.indexOf);var toLowerCase = demethodize(String.prototype.toLowerCase);// Array.prototypevar join = demethodize(Array.prototype.join);var forEach = demethodize(Array.prototype.forEach);var map = demethodize(Array.prototype.map); |
当然,许多许多。来看看这些是怎么执行的:
|
1
2
3
4
5
6
7
8
9
10
11
|
("abc,def").split(",");//=> ["abc","def"]split("abc,def", ",");//=> ["abc","def"]["a","b","c"].join(" ");//=> "a b c"join(["a","b","c"], " ");// => "a b c" |
题外话:
后面我们会演示,实际上更好的使用demethodize函数的方式是参数翻转。
在函数式编程情况下,你通常需要把“data”或“input data”参数作为函数的最右边的参数。方法通常会把this变量绑定到“data”参数上。举个例子,String.prototype方法通常操作的是实际的字符串(即”data”)。Array方法也是这样。
为什么这样可能不会马上被理解,但是一旦你使用柯里化或是组合函数来表达更丰富的逻辑的时候情况会这样。这正是我在引言部分说到UnderScore.js所存在的问题,之后在以后的文章中还会详细介绍。几乎每个Underscore.js的函数都会有“data”参数,并且作为最左参数。这最终导致非常难重用,代码也很难阅读或者是分析。:-(
管理参数顺序
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// shift the parameters of a function by onevar ignoreFirstArg = function (f) { return function(){ var args = [].slice.call(arguments,1); return f.apply(this, args); };};// reverse the order that a function accepts argumentsvar reverseArgs = function (f) { return function(){ return f.apply(this, toArray(arguments).reverse()); };}; |
组合函数
在函数式编程世界里组合函数到一起是极其重要的。通常的想法是创建小的、可测试的函数来表现一个“单元逻辑”,这些可以组装到一个更大的可以做更复杂工作的“结构”。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// compose(f1, f2, f3..., fn)(args) == f1(f2(f3(...(fn(args...)))))var compose = function (/* f1, f2, ..., fn */) { var fns = arguments, length = arguments.length; return function () { var i = length; // we need to go in reverse order while ( --i >= 0 ) { arguments = [fns[i].apply(this, arguments)]; } return arguments[0]; };};// sequence(f1, f2, f3..., fn)(args...) == fn(...(f3(f2(f1(args...)))))var sequence = function (/* f1, f2, ..., fn */) { var fns = arguments, length = arguments.length; return function () { var i = 0; // we need to go in normal order here while ( i++ < length ) { arguments = [fns[i].apply(this, arguments)]; } return arguments[0]; };}; |
例子:
|
1
2
3
4
|
// abs(x) = Sqrt(x^2)var abs = compose(sqrt, square);abs(-2); // 2 |
这就是今天要说的,在接下来的文章中我们会在函数柯里化上深入研究下。
接下来 -> 第四部分:函数柯里化
更多内容预告:
- 第一部分:引言
- 第二部分:如何打造“函数式”编程语言
- 第三部分:.apply()、.call()以及arguments对象
- 第四部分:函数柯里化
- 第五部分:参数可变函数(敬请期待)
- 第六部分:一个真实的例子——2048 Game & Solver(敬请期待)
- 第七部分:惰性序列 / 集合(敬请期待)
- 第八部分:参数顺序为何重要(敬请期待)
- 第九部分:Functors 和 Monads(敬请期待)

浙公网安备 33010602011771号