一、call和apply介绍
call()
方法使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数。
apply()
方法调用一个具有给定this
值的函数,以及作为一个数组(或类似数组对象)提供的参数。
该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。
二、call的使用
call是Function
默认实现的函数,使用方式为:fn.call(context, arg0, arg1, arg2...)
。fn函数的上下文环境变成context,并且还会使用传入的参数。
var foo = { value: 1 };
function fn() {
console.log(this.value);
}
fn.call(foo); // value => 1
三、模拟call的实现
要模拟call的实现,首先明白call做了那些事情。
- 只有使用call时,fn的上下文环境才会改变
- fn的实际参数来自于call的第二个参数一直到最后一个参数就是
- call方法不传入上下文的时候,默认使用全局上下文
上面的3点大概就是非严格模式下call的行为。
先看第一点,如何改变fn的上下文。
改变fn的上下文,只需要三步:
- 打开冰箱(将fn挂在foo上,命名为_fn)
- 大象放进去(执行foo._fn)
- 关上冰箱(删除foo._fn)
写成代码就是如下
var foo = {
value: 1
};
function fn() {
console.log(this.value);
}
Function.propotype.myCall = function(that) {
that._fn = this;
that._fn();
delete that._fn;
}
第一步就轻松的完成了,开不开心~
考虑有参数传入的情况
首先得看收集参数,然后将参数一个一个传递给fn。毕竟参数的数量不清楚,所以使用arguments收集参数当然会好一点。问题就在于fn接收参数的时候是fn(context, arg0, arg1, arg2...),有什么方法可以将参数变成这种形式呢。
**注意:**参数不是数组,不是收集起来直接放进去就可以的
api翻了一下,能满足要求的就只有下面两种方法:
- 使用eval函数
- 使用Function的构造函数
使用eval的方法会比较简单,直接用字符串拼接一下参数,然后传入eval运行就好。但是这种方法官方不推荐,存在被恶意攻击的风险。这里采用Function的方案实现。
Function.prototype.myCall = function(that) {
var target = this;
var args = arguments, argArr = [];
var args = arguments, argNameArr = [], argArr = [];
for (var i = 0; i < args.length; i++) {
argNameArr.push('arguments[1][' + i + ']');
argArr.push(arguments[i]);
}
argNameArr.pop(); // 去除多的arguments名字
argArr.shift(); // 去除第一个参数 context
that.fn = target; // 给that添加函数 fn
var result = new Function('fn', 'return fn(' + argNameArr.join(',') + ')')(that.fn, argArr); // 立即调用执行方法
delete that.fn; // 移除函数 fn
return result;
}
考虑一下特殊情况,传入的上下文为空的时候,实现代码如下:
value = 111;
var foo = {
value: 1
}
function bar(p1, p2) {
console.log(this.value + ',' + p1 + ',' + p2);
}
Function.prototype.myCall = function (that) {
var target = this;
// 如果target不是function
if (typeof target !== 'function') {
throw new TypeError(target + ' is not type of Function');
}
// 如果that为空,将全局的this赋值给that
if (!that) {
that = function() {
return this;
}()
}
// 获取参数
var args = arguments, argNameArr = [], argArr = [];
for (var i = 0; i < args.length; i++) {
argNameArr.push('arguments[1][' + i + ']');
argArr.push(arguments[i]);
}
argNameArr.pop(); // 去除多的arguments名字
argArr.shift(); // 去除第一个参数 context
that.fn = target; // 给that添加函数 fn
var result = new Function('fn', 'return fn(' + argNameArr.join(',') + ')')(that.fn, argArr); // 立即调用执行方法
delete that.fn; // 移除函数 fn
return result;
}
bar.myCall(foo, 2, 3);
这样一来,一个call的模拟实现就完成了。
四、apply的用法
apply是Function
默认实现的函数,使用方式为:fn.apply(thisArg, [argsArray])
。实例代码如下:
var foo = { value: 1 };
function fn() {
console.log(this.value);
}
fn.apply(foo); // value => 1
五、模拟实现apply
apply和call的唯一区别在于接收的参数不同,那需要改的地方就只有对参数的处理就好了。
value = 111;
var foo = {
value: 1
}
function bar(p1, p2) {
console.log(this.value + ',' + p1 + ',' + p2);
}
Function.prototype.myApply = function (that) {
var target = this;
// 如果target不是function
if (typeof target !== 'function') {
throw new TypeError(target + ' is not type of Function');
}
// 如果that为空,将全局的this赋值给that
if (!that) {
that = function () {
return this;
}()
}
// apply第二个参数是数组
var args = arguments.length > 1 ? arguments[1] : [], argNameArr = [];
// 拼接参数名称 arguments[1][0]、arguments[1][1]...
for (var i = 0; i < args.length; i++) {
argNameArr.push('arguments[1][' + i + ']');
}
// args 不是数组类型
if (args instanceof Array) {
that.fn = target;
result = new Function('fn', 'return fn(' + argNameArr.join(',') + ')')(that.fn, args);
} else {
throw new ReferenceError('arguments 1 must be type of Array');
}
delete that.fn;
return result;
}
bar.myApply(null, [2, 3]);
六、总结
想要深入的了解call和apply还是得实现一下,不然有很多地方总是会想不清楚。比如说函数的上下文是怎么改变的。只是实现了这两个函数,收获如下:
- 如何获取全局的this
- Function的构造函数使用
- arguments参数的使用