最近对原生js的学习学到了call,apply,bind,为了加深理解,于是决定手写call,apply,bind,现将手写代码与大家分享一下。
什么是call,apply,bind
call:
call()方法使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。apply:
apply()方法调用一个具有给定this值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。bind:
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
这是MDN对call,apply,bind的描述,简单来说,call,apply,bind都可以将一个函数执行时的this指向变更为传入的第一个参数,区别在于call,apply会立即执行该函数,而bind会返回一个新函数,call,bind传入多个参数以逗号隔开,apply第一个参数为给定的this,第二个参数为剩余参数的集合,可以是数组,也可以是类数组,如图所示:
我们知道了call,apply,bind之后,就可以开始手写了
call
完整代码大致如下,我们一步一步来解释
Function.prototype.myCall = function(ctx, ...args) {
ctx = (ctx !== null && ctx !== undefined) ? Object(ctx) : globalThis;
const key = Symbol('fn');
Object.defineProperty(ctx, key, { value: this });
const result = ctx[key](...args);
delete ctx[key];
return result;
}
执行效果如图所示:
第一步:myCall应该写在哪?
所有函数都能够调用call方法,那么myCall也要支持所有函数的调用,所以我们可以将myCall写在Function的原型对象上面,也就是Function.propotype
Function.prototype.myCall = function() {}
第二步:入参
ctx为传入的新的this,使用...将剩余的参数集合在args中,此时args是一个数组
Function.prototype.myCall = function(ctx, ...args) {}
第三步:如何调用func呢?
因为func直接调用myCall,所以myCall中的this指向func,直接调用this等同于直接调用func
Function.prototype.myCall = function(ctx, ...args) {
console.log(this)
this()
}
function func() {
console.log(this)
}
func.myCall({});
可以看到,直接调用
this并没有将func的this指向ctx,所以如何更改this指向呢?
第四步:如何更改this指向
想要将func中的this指向ctx就需要在ctx中调用func
Function.prototype.myCall = function(ctx, ...args) {
ctx.fn = this;
ctx.fn(...args);
delete ctx.fn
}
function func(a, b) {
console.log(this, a, b)
}
func.myCall({}, 1, 2);
在
ctx中添加一个新属性,值为this,再调用该属性,最后删除,到这里,基本的call的功能已经实现了,接下来就是对细节上的优化,和异常处理了
第五步:处理ctx的异常
在实际的开发工作中,调用call方法可能不传参数,或者传递的参数为基本数据类型,那么对于null或者undefined,我们要将它包装成全局对象,即globalThis,对于基本数据类型,我们需要显式地将它包装成一个对象
Function.prototype.myCall = function(ctx, ...args) {
ctx = (ctx !== null && ctx !== undefined) ? Object(ctx) : globalThis;
ctx.fn = this;
ctx.fn(...args);
delete ctx.fn
}
第六步:处理临时新加的属性
在ctx中添加值为this的新属性,该属性可能会与ctx原有属性重名,为了预防这种情况,我们使用Symbol来给这个属性命名
Function.prototype.myCall = function(ctx, ...args) {
ctx = (ctx !== null && ctx !== undefined) ? Object(ctx) : globalThis;
const key = Symbol('fn');
ctx[key] = this;
ctx[key](...args);
delete ctx[key];
}
但是这样会产生一个新的问题,就是在原函数执行时的this中能够获取到这个Symbol,也就是说会显式的改变ctx,如图所示:
所以我们可以通过属性设置的方式来设置新的临时属性,通过defineProperty设置
Function.prototype.myCall = function(ctx, ...args) {
ctx = (ctx !== null && ctx !== undefined) ? Object(ctx) : globalThis;
const key = Symbol('fn');
Object.defineProperty(ctx, key, { value: this });
ctx[key](...args);
delete ctx[key];
}
第七步:处理原函数的返回值
最后将原函数的返回值返回就ok了
Function.prototype.myCall = function(ctx, ...args) {
ctx = (ctx !== null && ctx !== undefined) ? Object(ctx) : globalThis;
const key = Symbol('fn');
Object.defineProperty(ctx, key, { value: this });
const result = ctx[key](...args);
delete ctx[key];
return result;
}
到这里,call的手写就已经结束了,怎么样,是不是很简单呢,最后大家可以自行测试一遍
apply
apply和call的差距就在于入参方式上,所以可以在call的基础上处理入参以实现手写apply,由于apply第二个参数是一个数组或者类数组,所以我们可以使用Array.from()对第二个参数进行处理
Function.prototype.myApply = function(ctx, args) {
ctx = (ctx !== null && ctx !== undefined) ? Object(ctx) : globalThis;
const key = Symbol('fn');
Object.defineProperty(ctx, key, { value: this });
const result = ctx[key](...Array.from(args));
delete ctx[key];
return result;
}
bind
bind函数入参与call一致,返回一个新函数,这里第一个return的function()是返回出去的函数,第二个return是原函数执行时的返回值。
Function.prototype.myBind = function(ctx, ...args) {
const that = this;
return function() {
return that.apply(ctx, [...args, ...arguments]);
}
};
由于bind可以入参,新函数也可以入参,所以将bind的入参和新函数的入参拼接起来一起传给apply,如图所示:
我这边使用
apply来实现bind的手写,感兴趣的小伙伴也可以尝试不使用apply。
这样我们就实现了对call,apply,bind的手写
前端小渣渣的学习总结,如有错误,欢迎指正!!