手写call,apply,bind

134 阅读4分钟

最近对原生js的学习学到了callapplybind,为了加深理解,于是决定手写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,第二个参数为剩余参数的集合,可以是数组,也可以是类数组,如图所示:

截屏2022-11-23 16.46.55.png 我们知道了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;
}

执行效果如图所示:

截屏2022-11-23 17.33.38.png

第一步: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({});

image.png 可以看到,直接调用this并没有将functhis指向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);

image.pngctx中添加一个新属性,值为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,如图所示:

image.png 所以我们可以通过属性设置的方式来设置新的临时属性,通过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];
}

image.png

第七步:处理原函数的返回值

最后将原函数的返回值返回就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

applycall的差距就在于入参方式上,所以可以在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一致,返回一个新函数,这里第一个returnfunction()是返回出去的函数,第二个return是原函数执行时的返回值。

Function.prototype.myBind = function(ctx, ...args) {
    const that = this;
    return function() {
        return that.apply(ctx, [...args, ...arguments]);
    }
};

由于bind可以入参,新函数也可以入参,所以将bind的入参和新函数的入参拼接起来一起传给apply,如图所示:

image.png 我这边使用apply来实现bind的手写,感兴趣的小伙伴也可以尝试不使用apply

这样我们就实现了对callapplybind的手写

前端小渣渣的学习总结,如有错误,欢迎指正!!