call和apply区别

我们正在寻求调校JavaScript的方式,使得我们可以做些真正的函数式编程。为了做到这一点,详细理解函数调用和函数原型是非常有必要的。

函数原型
如果我们点开了我们喜欢的浏览器+JavaScript控制台,让我们看一下Function.prototype对象的属性:
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”函数来帮助我们弄清楚发生了什么。
var tester = function (a, b, c){
console.log({
this: this,
a: a,
b: b,
c: c
});
};

这个函数简单记录了输入参数的值,和“上下文变量”,即this的值。 现在,让我们尝试一些事情:
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设置为第一个输入参数的值,然后其他的的参数一个跟一个的也传进函数。
语法:

fn.call(thisArg[, arg1[, arg2[, …]]])

因此,下面这两行是等效的:
tester(“this”, “is”, “cool”);
tester.call(window, “this”, “is”, “cool”);

当然,我们能够随需传入任何参数:
tester.call(“this”, “is”, “even”, “cooler”);
//=> {this: “this”, a: “is”, b: “even”, c: “cooler”}

这个方法主要的功能是设置你所调用函数的this变量的值。

使用Function.prototype.apply

函数的.apply方法比.call更实用一些。和.call类似,.apply的调用方式也是把上下文变量this设置为输入参数序列中的第一个参数的值。输入参数序列的第二个参数也是最后一个,以数组(或者类数组对象)的方式传入。

语法:
fun.apply(thisArg, [argsArray])
因此,下面三行全部等效:
tester(“this”, “is”, “cool”);
tester.call(window, “this”, “is”, “cool”);
tester.apply(window, [“this”, “is”, “cool”]);

能够以数组的方式指定一个参数列表在多数时候非常有用(我们会发现这样做的好处的)。例如,Math.max是一个可变参数函数(一个函数可以接受任意数目的参数)。

Math.max(1,3,2);
//=> 3

Math.max(2,1);
//=> 2

这样,如果我有一个数值数组,并且我需要利用Math.max函数找出其中最大的那个,我怎么用一行代码来做这个事儿呢?
var numbers = [3, 8, 7, 3, 1];
Math.max.apply(null, numbers);
//=> 8

apply方法真正开始显示出它的重要是当配上特殊参数:Arguments对象。
每个函数表达式在它的作用域中都有一个特殊的、可使用的局部变量:arguments。为了研究它的属性,让我们创建另一个tester函数:
var tester = function(a, b, c) {
console.log(Object.getOwnPropertyNames(arguments));
};

注:在这种情况下我们必须像上面这样使用Object.getOwnPropertyNames,因为arguments有一些属性没有标记为可以被枚举的,于是如果仅仅使用console.log(arguments)这种方式它们将不会被显示出来。

现在我们按照老办法,通过调用tester函数来测试下:
tester(“a”, “b”, “c”);
//=> [“0”, “1”, “2”, “length”, “callee”]

tester.apply(null, [“a”]);
//=> [“0”, “length”, “callee”]

arguments变量的属性中包括了对应于传入函数的每个参数的属性,这些和.length属性、.callee属性没什么不同。

.callee属性提供了调用当前函数的函数的引用,但是这并不被所有的浏览器支持。就目前而言,我们忽略这个属性。
让我们重新定义一下我们的tester函数,让它丰富一点:
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转换成一个数组,这有个非常不错的快捷小函数:

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函数,但是它只是在一元函数下正确工作。

// old version
var logWrapper = function (f) {
return function (a) {
console.log(‘calling “’ + f.name + ‘” with argument “’ + a);
return f(a);
};
};

当然了,我们既有的知识让我们能够构建一个可以服务于任何函数的logWrapper函数:

// new version
var logWrapper = function (f) {
return function () {
console.log(‘calling “’ + f.name + ‘”’, arguments);
return f.apply(this, arguments);
};
};

通过调用
f.apply(this, arguments);
我们确定这个函数f会在和它之前完全相同的上下文中被调用。于是,如果我们愿意用新的”wrapped”版本替换掉我们的代码中的那些日志记录函数是完全理所当然没有唐突感

把原生的prototype方法放到公共函数库中
浏览器有大量超有用的方法我们可以“借用”到我们的代码里。方法常常把this变量作为“data”来处理。在函数式编程,我们没有this变量,但是我们无论如何要使用函数的!
var demethodize = function(fn){
return function(){
var args = [].slice.call(arguments, 1);
return fn.apply(arguments[0], args);
};
};

一些别的例子:
// String.prototype
var split = demethodize(String.prototype.split);
var slice = demethodize(String.prototype.slice);
var indexOfStr = demethodize(String.prototype.indexOf);
var toLowerCase = demethodize(String.prototype.toLowerCase);

// Array.prototype
var join = demethodize(Array.prototype.join);
var forEach = demethodize(Array.prototype.forEach);
var map = demethodize(Array.prototype.map);

当然,许多许多。来看看这些是怎么执行的:

(“abc,def”).split(“,”);
//=> [“abc”,”def”]

split(“abc,def”, “,”);
//=> [“abc”,”def”]

[“a”,”b”,”c”].join(” “);
//=> “a b c”

join([“a”,”b”,”c”], ” “);
// => “a b c”

管理参数顺序
// shift the parameters of a function by one
var ignoreFirstArg = function (f) {
return function(){
var args = [].slice.call(arguments,1);
return f.apply(this, args);
};
};

// reverse the order that a function accepts arguments
var reverseArgs = function (f) {
return function(){
return f.apply(this, toArray(arguments).reverse());
};
};

组合函数
在函数式编程世界里组合函数到一起是极其重要的。通常的想法是创建小的、可测试的函数来表现一个“单元逻辑”,这些可以组装到一个更大的可以做更复杂工作的“结构”。

// 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];
};
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值