首先介绍下call和apply两个方法,这两个方法都是挂载在函数的原型上的,所以所有的函数都可以调用这两个方法。
注意:call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。
例子:
function foo(b = 0) {
console.log(this.a + b);
}
const obj1 = {
a: 1
};
const obj2 = {
a: 2
};
foo.call(obj1, 1); // 2
foo.call(obj2, 2); // 4
foo.apply(obj1, [1]); // 2
foo.apply(obj2, [2]); // 4
对于this不熟悉的同学可以先异步:理解Javascript的this。总结起来一句话:Javascript函数的this指向调用方,谁调用this就指向谁,如果没人谁调用这个函数,严格模式下指向undefined,非严格模式指向window。
所以本质上call和apply就是用来更改被调用函数的this值的。如上,call和apply只有参数的不同,模拟实现了call,那么apply就只是参数处理上的区别。也就是说,call和apply干了两件事:
###更改this
现在模拟实现call和apply的问题转移到另一个问题上,即如何去更改一个函数的this值,很简单:
function foo(b = 0) {
console.log(this.a + b);
}
const obj1 = {
a: 1,
foo: foo
};
const obj2 = {
a: 2,
foo: foo
};
obj1.foo(1);
obj2.foo(2);
也就是说我们把这个方法赋值给对象,然后对象调用这个函数就可以了。改变一个函数的this步骤很简单,首先将这个函数赋值给this要指向的对象,然后对象调用这个函数,执行完从对象上删除掉这个函数就好了。步骤如下:
obj.foo = foo;
obj.foo();
delete obj.foo;
有了思路我们实现第一版call方法:
Function.prototype.call2 = function(context) {
context = context || {};
context[this.name] = this;
context[this.name]();
delete context[this.name];
}
this.name是函数声明的名称,但其实是没必要一定对应函数名称的,我们随便用一个key都可以:
Function.prototype.call2 = function(context) {
context = context || {};
context.func = this;
context.func();
delete context.func;
}
使用新的call调用上面的函数:
foo.call2(obj1); // 1
foo.call2(obj2); // 2
OK,this的问题解决了,接下来就是传参的问题:
函数中的参数保存在一个类数组对象arguments中。因此我们可以从arguments里面去拿从传到call2里面的参数:
Function.prototype.call2 = function(context) {
context = context || {};
var params = [];
for (var i = 1; i < arguments.length; i++) {
params[i - 1] = arguments[i];
}
context.func = this;
context.func();
delete context.func;
}
此时问题来了,如何把参数params传递到func中呢?比较容易想到的办法是利用ES6的扩展运算符:
Function.prototype.call2 = function(context) {
context = context || {};
var params = [];
for (var i = 1; i < arguments.length; i++) {
params[i - 1] = arguments[i];
}
context.func = this;
context.func(...params);
delete context.func;
}
看下我们的例子:
foo.call2(obj1, 1); // 2
foo.call2(obj2, 2); // 4
还有一个实现,是利用不常用的eval函数,即我们把参数拼接成一个字符串,传给eval函数去执行,
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。
看下我们的第二版实现:
Function.prototype.call2 = function(context) {
context = context || {};
var params = [];
for (var i = 1; i < arguments.length; i++) {
params[i - 1] = arguments[i];
}
// 注意,此处的this是指的被调用的函数
context.func = this;
eval('context.func(' + params.join(",") + ')');
delete context.func;
}
call和apply还有另外两个重要的特性,可以正常返回函数执行结果,接受null或undefined为参数的时候将this指向window,然后我们来实现下这两个特性,然后加上必要的判断提示,这是我们的第三版实现:
Function.prototype.call2 = function(context) {
context = context || window;
var params = [];
// 此处将i初始化为1,是为了跳过context参数
for (var i = 1; i < arguments.length; i++) {
params[i - 1] = arguments[i];
}
// 注意,此处的this是指的被调用的函数
context.func = this;
var res = eval('context.func(' + params.join(",") + ')');
delete context.func;
return res;
}
然后我们调用测试下:
foo.call2(obj1, 1); // 2
foo.call(2, 1); // NaN
foo.call2(2, 1); // context.func is not a function
如上我们发现将对象改成数字2后原始call返回了NaN,我们的call2却报错了,说明一个问题,我们直接context = context || window是有问题的。内部还有一个类型判断,解决这个问题后,我们的第四版实现如下:
Function.prototype.call2 = function(context) {
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context) || context;
}
var params = [];
// 此处将i初始化为1,是为了跳过context参数
for (var i = 1; i < arguments.length; i++) {
params[i - 1] = arguments[i];
}
// 注意,此处的this是指的被调用的函数
context.func = this;
var res = eval('context.func(' + params.join(",") + ')');
delete context.func;
return res;
}
这就是我们的最终代码,这个代码可以从ES3一直兼容到ES6,此时:
foo.call(2, 1); // NaN
foo.call2(2, 1); // NaN
apply和call只是参数上的区别,将call2改写就好了:
Function.prototype.apply2 = function(context, arr) {
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context) || context;
}
// 注意,此处的this是指的被调用的函数
context.func = this;
arr = arr || [];
var res = eval('context.func(' + arr.join(",") + ')');
delete context.func;
return res;
}
以上就是我们最终的实现,目前还有一个问题就是context.func的问题,这样一来我们传进来的context就不能使用func字符串作为方法名了。
我们实现过程都解决了以下问题:
call和apply可以用来重新定义函数的执行环境,也就是this的指向; call 和 apply 都是为了改变某个函数运行时的 context 即上下文而存在的 换句话说,就是为了改变函数体内部 this 的指向。
js中有三个改变this指针的方法,分别是 apply,call,bind。很多人只知道能改变的this,但是具体的适用场景不是太清楚。我也是遇到坑后不断的实践发现了区别。call ,apply方法:
call 方法是Function.prototype原型上天生自带的方法,所有的函数都可以调用的。它改变了call方法(Function.prototype原型上的call)的this指向。
call定义:调用一个对象的一个方法,以另一个对象替换当前对象,传递多个参数,apply定义:应用某一对象的一个方法,用另一个对象替换当前对象,apply传递多个参数的时候第二个参数需要传递一个数组
ECMAScript 规范给所有函数都定义了 call 与 apply 两个方法,它们的应用非常广泛,它们的作用也是一模一样,只是传参的形式有区别而已。
这是 JS 原生方法原理探究系列的第一篇文章。本文会介绍如何实现 call、apply 和 bind 方法。关于这几个方法的具体用法,MDN 以及网上的文章已经描述得很清楚,这里不再赘述。
本文将全面的,详细解析call方法的实现原理,并手写出自己的call方法,相信看完本文的小伙伴都能从中有所收获。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!