js是面向对象编程的语言,类和实例是我们必须要掌握的东西,这里面最有代表性的关键字就是 this 。我们都知道事件绑定的时候,函数中 this 是被绑定的元素;非严格模式下回调函数(包括定时器)中的 this 一般是 window;普通函数执行,函数中 this 取决于函数执行主体;箭头函数中的 this 是继承自上下文的;构造函数中的 this 一般是实例;当然有些时候我们需要改变函数中的 this 指向,比如在 react 中给元素绑定事件的时候,我们需要保证被绑定函数中的 this 是当前组件,通常我们会选择在绑定函数外边包一个箭头函数或者使用 bind 处理一下(在 constructor 中统一使用 bind 处理一次性能会好一些),在封装一些组件的时候,我们有时候也需要对用户传进来的函数进行 this 处理。所以,我们有必要对 call、apply、bind 进行一个总结。
一、使用
call、apply、bind 都是 Function.prototype 上的方法,只要是函数都可以直接调用。
1、call
语法:fn.call(context,a,b,c...),fn 是要进行 this 改变的函数,call 函数第一个参数context 是新 this 指向,后边参数是要传给 fn 的参数(注意是分开传的)。
执行过程:(1)首先是 fn 找到 Function.prototype 上的 call 方法并执行。
(2)call 执行的时候,把 call 中 this(这里是 fn)中的 this 改成 call 中的第 一个参数 context。
(3)把 call 中 this(这里是 fn)执行,并且把 call 中第一个参数以后的参数传递给 call 中 this(这里是 fn)。
let f = function(){console.log(this,arguments);}
let obj = {a:1};
f.call(obj,1,2,3);上述代码执行结果:
多个 call 情况:面试中经常会问到两个 call 的执行情况,其实如果知道了一个 call 的执行过程,两个 call 的就很好分析了。
let fn1 = function(){console.log(1,this,arguments)};
let fn2 = function(){console.log(2,this,arguments)};
fn1.call.call(fn2,1,2,3);分析上述函数执行过程:
(1)首先 fn1.call 也是一个函数,所以它也能调用 Function.prototype 上的方法(也就是可以调用 call 方法),那么这个执行过程就是 fn1 找到 Function.prototype 上的 call 方法(fn1.call),然后 call 又找到 Function.prototype 上的 call 方法(fn1.call.call)并且把 call 方法执行。注意此时 call(右边的) 是通过 call(左边的) 函数调用的,所以call(右边的)中的 this 是 call(左边的)。
(2)call(右边的)方法执行的时候,把 call(右边的)中 this(左边的 call)中的 this 改成call(右边的) 的第一个参数 fn2。
(3)把 call(右边的) 中 this(左边的 call) 执行,并且把 call(右边的) 的第二个以后的参数(1,2,3)传递给 call 中 this(左边的 call)。
(4)左边的 call 执行的时候(此时左边 call 中的 this 已经被改成了 fn2),首先把左边 call中的 this(fn2)中的 this 改成 左边 call 的第一个参数(1)。
(5)把左边 call 中的 this(fn2) 执行,并且把左边 call 的第二个以后的参数(2,3)传递给左边 call 中的 this(fn2)。
所以最后的结果就是 fn2 执行,接受参数(2,3),fn2 中 this 是 1,而 fn1 只是帮助找了一下 call 方法并没有执行。
通过上述过程的分析我们也发现最多只有两个 call 会生效,有两个以上的 call 时前边的 call 也不会执行的。
2、apply
语法:fn.apply(context,[a,b,c...]),fn 是要进行 this 改变的函数,call 函数第一个参数context 是新 this 指向,第二个参数是要传给 fn 的参数(注意这里传的是数组,接收是分开接收的)。用法与 call 基本一致,只是第二个参数是一个数组。
执行过程:
(1)首先是 fn 找到 Function.prototype 上的 apply 方法并执行。
(2)apply 执行的时候,把 apply 中 this(这里是 fn)中的 this 改成 apply 中的第一个参数context。
(3)把 apply 中 this(这里是 fn)执行,并且把 apply 中第二个参数(数组)展开以后传递给 apply 中 this(这里是 fn)。
let f = function(){console.log(this,arguments);}
let obj = {a:1};
f.apply(obj,[1,2,3]);上述代码执行结果:
多个 apply 执行时候需要注意一下第二个参数类型,必须是数组,如果不是会报错。
比如这个会报错(执行第二个 apply 的时候传的是2而不是数组)
let fn1 = function(){console.log(1,this,arguments)};
let fn2 = function(){console.log(2,this,arguments)};
fn1.apply.apply(fn2,[1,2,3]);改成这样是可以的
let fn1 = function(){console.log(1,this,arguments)};
let fn2 = function(){console.log(2,this,arguments)};
fn1.apply.apply(fn2,[1,[2],3]);3、bind
语法:fn.bind(context,a,b,c...),fn 是要进行 this 改变的函数,bind 函数第一个参数context 是新 this 指向,第二个以后参数是要传给 fn 的参数(分开传的)。用法与 call 基本一致,只不过 bind 不是立即执行,而是执行后返回一个函数,我们可以在需要的时候手动执行它。
执行过程:
(1)首先是 fn 找到 Function.prototype 上的 bind 方法并执行。
(2)bind 执行的时候,返回一个函数A,函数A中把 bind 中 this(这里是 fn)中的 this 改成 bind 中的第一个参数 context。
(3)函数A中把 bind 中 this(这里是 fn)执行,并且把 bind 中第一个参数以后的参数及A中的参数传递给 bind 中 this(这里是 fn)。
(4)需要的时候把这个返回的函数A执行。
let f = function(){console.log(this,arguments);}
let obj = {a:1};
let A = f.bind(obj,1,2,3);
A(4,5);上述代码执行结果:
多个 bind 执行的时候跟 call 基本一致,只不过由于 bind 执行返回一个预处理函数,不会自动执行,需要我们手动执行一下,所以每执行一次 bind 后边加一个(),就和 call 差不多了,只要把参数对应好就行。
let fn1 = function(){console.log(1,this,arguments)};
let fn2 = function(){console.log(2,this,arguments)};
fn1.bind.bind(fn2,1,2,3)(4)(5);可以发现,多个 bind 执行的时候和 call 类似,最后的结果就是最后一个 bind 中的第一个参数(如果是函数的话)执行,并且把这个函数中的 this 改成最后一个 bind 中的第二个参数,只是传参的时候比 call 多传了一个 4 和 5。
二、简单实现(不考虑严格模式下的情况,不考虑对ES6的兼容)
1、call
fn.call(context,1)
在 call 中我们需要做的事就是拿到 fn 、拿到 context 及后边参数、把 fn 中 this 改成 context 后执行并把参数传给它,fn 在 call 中可以通过 this 获取,call 中参数也好获取,执行传参也都好办,关键就是修改 fn 中的this。我们要写的方法就是修改函数中 this 的,而方法中又需要修改 this,又没有现成的方法可以调用,怎么办?虽然没有现成的方法可以调用,但是我们可以间接的修改 this,回到最开始我们列出的几种 this 情况,可以间接修改 this 的有两种办法可以考虑。第一就是构造函数,我们可以把传进来的 context 整成一个实例,把 fn 放到这个实例所属类的 prototype 上,这样就可以通过实例.fn调用了,但是 context 是用户传进来的现成的对象或者只是一个数字字符串,对象还好,我们可以创建一个类,在这个类里边对 context 进行for in 循环,挨个把属性挂在 this 上,这样通过这个类创建出来的实例就相当于复制了一份 context 了,然后我们把 fn 也放到这个类上,那么这个新复制的实例就可以调 fn 了,但是数字和字符串怎么办?所以创建构造函数的方式是行不通的。后来还想使用Object.create搞一下,好像也不行。第二就是利用普通函数执行,函数中 this 取决于函数执行主体这一条,这就很好办了,这很容易就让我们联想到这个很熟悉的题
let obj = {
a:1,
fn:function(){console.log(this)}
}
obj.fn();
let f = obj.fn;
f();我们完全可以把 fn 挂到 context 上,这里又出现一个问题,如果 context 是 数字字符串甚至是 null undefined 怎么办?我们试试用 Object 转一下
貌似可以。。。到这里基本上可以实现改变 this 的功能了
Function.prototype.myCall = function (context, ...arg) {
context = Object(context);
context.fn = this;
context.fn(...arg);
}
let obj = { a: 1 };
let fn = function () { console.log(this) };
fn.myCall(obj);靠谱,但是 this 中多了一个 fn ,原 call 是没有的,那就把它删掉吧
Function.prototype.myCall = function (context, ...arg) {
context = Object(context);
context.fn = this;
context.fn(...arg);
delete context.fn;
}
let obj = {a:1};
let fn = function(){console.log(this)};
fn.myCall(obj);删掉了,这个是显示问题。但是当我们第一个参数传 null 或者 undefined 的时候,由于 Object(null) 或者 Object(undefined)是一个空对象,所以结果也是一个空对象,而实际上应该是 window,所以我们应该单独处理一下。
Function.prototype.myCall = function(context,...arg){
context = (context === null || context === undefined) ? window : Object(context);
context.fn = this;
context.fn(...arg);
delete context.fn;
}
let obj = {a:1};
let fn = function () { console.log(this) };
fn.myCall(undefined);处理后传 undefined 和 null 时 this 是 window 了。这里可以简写一下
Function.prototype.myCall = function ( context,...arg ){
context = Object(context) || window;
context.fn = this;
context.fn(...arg);
delete context.fn;
}
let obj = { a:1 };
let fn = function () { console.log(this) };
fn.myCall(undefined);this 改变是可以了,但是还有个问题,如果 fn 有返回值,我们上边写法是接收不到的,还得改一下
Function.prototype.myCall = function (context,...arg) {
context = Object(context) || window;
context.fn = this;
let returnV = context.fn(...arg);
delete context.fn;
return returnV;
}
let obj = { a:1 };
let fn = function () { console.log(this);rturn 78; }
let returnV = fn.myCall(obj);
console.log(returnV);以上基本实现了对 call 函数的一个模仿,当然跟实际函数还有一些差别。除了把 fn 挂到 context 上,我们还可以把它放到 context 的原型链上。比如context.__proto__.fn = this或者干脆放到基类原型上Object.prototype.fn = this,总之能让 context 调到就可以,但是记得删除。
2、apply
apply 实现和 call 类似,我们也可以基于 call 实现
Function.prototype.myCall = function(context,...arg){
if(typeof this !== 'function'){
throw new Error('不是一个函数!');
}
context = Object(context) || window;
context.fn = this;
let returnV = context.fn(...arg);
delete context.fn;
return returnV;
}
Function.prototype.myApply = function(context,arg){
return this.myCall(context,...arg);
}
let obj = {a:1};
let fn = function (){console.log(this,arguments);return 78;}
let returnV = fn.myApply(obj,[1,2,3]);
console.log(returnV);3、bind(暂时不考虑bind执行后被 new 的情况)
我们也可以基于 call 实现
Function.prototype.myCall = function(context,...arg){
if(typeof this !== 'function'){
throw new Error('不是一个函数!');
}
context = Object(context) || window;
context.fn = this;
let returnV = context.fn(...arg);
delete context.fn;
return returnV;
}
Function.prototype.myBind = function(context,...arg){
return (...bindArg)=>{
return this.myCall(context,...arg,...bindArg);
}
}
let obj = {a:1};
let fn = function(){console.log(this,arguments);return 78;};
let returnV = fn.myBind(obj,1,2,3)(4);
console.log(returnV);