谈谈我对call、apply的理解

2017-03-10

call在js面试中的地位不亚与prototype,但是可能连面试官都不一定真正搞懂call到底做了什么,不着急,先从标志答案说起

call是用来做什么的

call是什么?

1
Function.prototype.call(thisArg, arg1, arg2, arg3)

所以call是一个方法(函数),第一个参数是指定调用函数的上下文(this),后面的参数列表,就是调用函数需要传入的参数

返回值是执行函数的返回值

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

var foo = {
a: '1',
b: '2',
test: function(args1) {
console.log(args1);
return {ta: this.a, tb: this.b}
}
}

foo.test('normal invoked');

var obj = {a: '111', b: '222'};
foo.test.call(obj,'invoked by obj');

结果是:

1
2
3
4
5
输出 normal invoked
返回值是 {ta: '1', tb: '2'}

输出 invoked by obj
返回值是 {ta: '111', tb: '222'}

所以说,call就是用来改变运行函数执行的上下文环境,也就是this,这个就算是标准答案,我面试时也是这么说的,那么面试官一定会追问下面这个问题

call与apply有什么区别

基本用过call和apply的同学基本知道call是接收的参数列表,apply接收的是数组

而看过文档就知道,apply接收是类数组对象(array-like object),什么是类数组对象,举个例子 arguments

那为什么可以接收数组,因为数组不仅仅是类数组对象,同时他还是数组

这里举一个apply的实用案例:获取数组中的最大(小)值

1
Math.max.apply(null, [12,34,23423,235,32]);

返回值是:23423

Function.call

你知道["a", "b", "c"].map(Function.call, Number);的结果是什么吗?

终于进入正题!

首先map接收两个参数,一个是回调函数function(value, index, array) {},

第二参数一般不常用,是指定回调函数的上下文环境,也就是this

先来看下map的简单实现

1
2
3
4
5
6
7
Array.prototype.map = function(fn, ctx) {
var result = new Array(this.length);
for (var i = 0; i < this.length; i++) {
result[i] = fn.call(ctx, this[i], i, this);
}
return result;
}

完整版的map polyfill参考这里

因为map内部实现也是用了call方法

所以["a", "b", "c"].map(Function.call, Number);可以转换成如下

1
2
3
4
5
[
Function.call.call(Number, "a", 0, ["a", "b", "c"]),
Function.call.call(Number, "b", 1, ["a", "b", "c"]),
Function.call.call(Number, "c", 2, ["a", "b", "c"])
]

简化之后会变成如下,这里会让很多人疑惑

1
2
3
4
5
[
Number.call("a", 0, ["a", "b", "c"]),
Number.call("b", 1, ["a", "b", "c"]),
Number.call("c", 2, ["a", "b", "c"])
]

这时候就要看call的内部实现是怎样的了,根据规范的定义和,我猜测call的内部应该是类似如下的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function F() {
let args = arguments.length ===1 ? [arguments[0]] : Array.apply(null, arguments);
let ctx = args.shift();

if (this === F) {
let that = args.shift();
return ctx.apply(that, args);
} else {
return this.apply(ctx, args);
}

}

Function.prototype.fakeCall = F;

再来个简化版,只看执行逻辑,忽略掉参数的处理

1
2
3
4
5
6
7
function F(thisArg, arg1, arg2, arg3) {
if ( this === F ) {
return thisArg(arg2, arg3); // context: arg1
} else {
return this(arg1, arg2, arg3); // context: thisArg
}
}

也就是说call的内部是把this作为一个函数执行了,如果这时候你已经不知道this是什么了,请回去看第一个例子,一个函数中的this默认是指向它外层的对象,

正常情况使用call,如 Foo.test.call(obj),call内部的this就是test,执行this,就是执行test

再比如[].slice.call(arguments),call内部this就是slice,执行this,就是执行slice,而slice内部的this就是arguments

Function.call.call(Number, "a", 0, ["a", "b", "c"]), call内部的this指向call,那么call内部会进入if的条件中,执行函数变成了Number.call( "a", 0, ["a", "b", "c"])

至此应该就可以理解上面的那个简化是怎么来的了

然后再简化

1
2
3
4
5
[
Number(0, ["a", "b", "c"]), // this = 'a'
Number(1, ["a", "b", "c"]), // this = 'b'
Number(2, ["a", "b", "c"]) // this = 'c'
]

最后

[0, 1, 2]

fn.call.call.call.call.call.call()

有人看到call.call就这么复杂,那如果是很多个call,意味着什么?

意味着还是fn.call.call()

1
2
Function.call === Function.call.call        // true
Function.call === Function.call.call.call // true

因为Function.prototype.call,这是原型上的方法,点多少个都是它自己,而call只认调用他的那个对象到底是什么,如果是普通对象,那说明只有一个call,如果是call,那就按照call.call的方式去处理

参考链接