去年写了篇文章,call、apply和bind的区别,但是随着市场对前端工程师的要求越来越高,我们也要与时俱进,需要深入的了解一下这三个方法的原理。
大家看这篇文章之前如果已经了解如何使用call、apply和bind了,那么请继续往下看,如果不是很清楚,建议先读一下这篇文章call、apply和bind的区别。
首先我们来给call下个定义:
call方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
举个例子:
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
从上面代码的执行结果,我们可以看到,call首先改变了this的指向,使函数的this指向了foo,然后使bar函数执行了。
总结如下:1、call改变函数this指向,2、调用函数
思考一下:我们如何实现上面的效果呢?代码改造如下:
//将bar函数挂载到foo对象上,使其成为foo的方法,用foo.bar来调用
var foo = {
value:1,
bar:function(){
console.log(this.value)
}
}
foo.bar() //1
仔细观察一下我们做了什么,将bar函数挂载到foo对象上,使其成为foo的方法,用foo.bar来调用。
再来看上面代码的执行结果:打印1,有没有什么启发呢?为了模拟call方法,我们可不可以这样做呢:
a、将函数设为某个对象的属性(或者方法)
b、通过该对象的属性调用该函数
c、将该对象上的这个属性(或者方法)
代码如下:
Function.prototype.myCall = function(context) {
context = context || window
//将函数挂载到对象的fn属性上
context.fn = this
//处理传入的参数
const args = [...arguments].slice(1)
//通过对象的属性调用该方法
const result = context.fn(...args)
//删除该属性
delete context.fn
return result
}
我们看一下上面的代码:
1、首先我们对参数context做了兼容处理,不传值,context默认值为window。
2、然后我们将函数挂载到context上面,context.fn = this;
3、处理参数,将传入myCall的参数截取,去除第一位,然后转为数组;
4、调用context.fn,此时fn的this指向context;
5、删除对象上的属性 delete context.fn
5、将结果返回。
以此类推,我们顺便实现一下apply,唯一不同的是参数的处理,代码如下:
Function.prototype.myApply = function(context) {
context = context || window
context.fn = this
let result
// myApply的参数形式为(obj,[arg1,arg2,arg3]);
// 所以myApply的第二个参数为[arg1,arg2,arg3]
// 这里我们用扩展运算符来处理一下参数的传入方式
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
以上便是call和apply的模拟实现,唯一不同的是对参数的处理方式。
接着再来思考一下bind的实现,在模拟bind的实现之前,先看一下bind的使用案例:
var obj = {a:1};
function bar(){
console.log(this.a);
}
bar.bind(obj)();
我们看到,bind函数虽然也能改变bar函数的this,但是改变后,函数并不会执行,只是返回一个新的函数,想执行就得继续调用,仔细观察第五行代码的写法。
根据上面的使用案例,我们先实现一个简单版本的bind:
Function.prototype.myBind = function(ctx) {
return () => { // 要用箭头函数,否则 this 指向错误
return this.call(ctx)
}
}
var obj = {a:1};
function bar(){
console.log(this.a);
}
bar.myBind(obj)();
但是这样比较简陋,函数的参数一多就不能处理了,如下面这种情况:
bar.bind(obj, 2)(2)
// or
bar.bind(obj)(2, 2)
为了兼容bind调用时满足参数传递的不同方式,代码修改如下:
Function.prototype.myBind = function(ctx, ...argv1) {
return (...argv2) => {
return this.call(ctx, ...argv1, ...argv2)
}
}
//测试代码
var obj = {a:1};
function bar(b,c){
console.log(this.a+b+c);
}
bar.myBind(obj)(20,30);
bar.myBind(obj,20,30)();
仔细观察上面的代码,基本上能实现按照不同方式进行传参了,当然此处myBind和原生的bind相比还是有些不足,这些就留给大家去完善了,如果你对本文有什么其他想法或者建议,欢迎留言。
资料引用:
https://segmentfault.com/a/1190000018428876
https://segmentfault.com/a/1190000015438195#articleHeader3
https://github.com/mqyqingfeng/Blog/issues/11
https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5bdd0d8e6fb9a04a044073fe