一文手撕call、apply、bind

前言

在面试中,this的指向是经常被考到的,然而call、apply、bind可以改变this的指向,所以有时候面试中可能会被要求我们自己手写call、apply、bind方法。今天我就来试着手撕这一道考题。

call、apply、bind用法

ini 复制代码
fn.call(thisArg,arg1,arg2,arg3,...)
fn.apply(thisArg,[arg1,agr2,...])
newFn=fn.bind(thisArg,arg1,arg2,arg3,...)

call、apply、bind对比

不同点:

  1. 三者参数不相同,call,bind接收一个参数列表,而apply接收一个数组参数
  2. call,apply会立即获得执行结果,而bind会返回一个已经指定this指向和参数的新函数

相同点:

三者都可以改变this的指向,而且都位于function的原型上,所有函数都可以通过原型链找到并调用。

实现myCall、myApply、myBind

myCall

首先我们先把自制myCall要挂载到func的原型上,可以让所有的函数调用。

javascript 复制代码
Function.prototype.myCall=function(thisArg,...args){
    console.log(this)
    console.log(thisArg)
}
fn.myCall(obj,arg1,arg2)

在myCall中的this指向fn函数,thisArg指向目标对象obj。我们想要让fn运行时候fn里面的this指向目标对象thisArg,即让fn成为obj这个对象的方法来运行。

我们可以通过给thisArg增加一个方法(f),把this(fn)传给它,然后运行这个方法(f),再把参数传进去,最终返回这个方法(f)让外界。

javascript 复制代码
Function.prototype.myCall=function(thisArg,...args){
    thisArg.f=this
    let res=thisArg.fn(...args)
    return res
}

此时call的方法就基本上完成了,但是还是存在着点问题。

  • 如果有多个函数同时调用这个方法(myCall),并且目标对象相同,那么目标对象的f属性可能会被覆盖。
  • 目标对象上会永远存在着f属性。

那么该如何解决这两个问题呢?我们可以使用Symbol来创建独一无二的值确保对象的唯一属性名,然后再返回res之前用delete删除该属性就解决了这两个问题。

javascript 复制代码
Function.prototype.myCall=function(thisArg,...args){
    const prop=Symbol()
    thisArg[prop]=this
    let res=thisArg[prop](...args)
    delete thisArg[prop]
    return res
}

最后我们再规范一下传入的参数,如果当传入的对象(thisArg)是undefined或者null,我们就要自动替换成全局对象。
最终版myCall

javascript 复制代码
Function.prototype.myCall=function(thisArg,..args){
    thisArg=thisArg||window
    const prop=Symbol()
    thisArg[prop]=this
    let res=thisArg[prop](...args)
    delete thisArg[prop]
    return res
}

myApply

由于call和apply的差别不大,仅传入参数有差异,所有我们可以沿用myCall的思路。

ini 复制代码
Function.prototype.myApply=function(thisArg,args){
    thisArg=thisArg||window
    args=args||[]
    const prop=Symbol()
    thisArg[prop]=this
    let res=thisArg[prop](...args)
    delete thisArg[prop]
    return res
}

myBind

myBind函数的实现是比较复杂的,因为bind函数它可以创建一个新的绑定this指向的函数并返回,而且这个返回的函数还可以继续传参,也还可以用new创建实例,要考虑的方面很多,那么我们来逐步实现myBind。

我们已经实现了myCall和myApply,我们可以选择一个来绑定this,再给它封装一层函返回,这样就实现了给个简易版的myBind。

javascript 复制代码
Function.prototype.myBind=function(thisArg,...args){
    const self=this
    return function(){
        return self.apply(thisArg,args)
    }
}

这个简易版的myBind有几处地方需要注意。

首先,我们apply的形参是数组形式,所以我们传入的是args而非...args。在return前,为什么需要self来保存this呢?是因为我们要用闭包把this(fn)保存起来,让myBind方法返回的函数在运行时的this值可以正确地指向fn。

javascript 复制代码
//如果不定义self
Function.prototype.myBind=function(thisArg,...args){
    return function(){
        return this.apply(thisArg,args)
    }
}
const f=fn.myBind(obj)
const f=function(){
    return this.apply(thisArg,args)
}
f()
//当我们调用f函数时候,会发现this的指向并非我们所期待的fn

由于bind返回的函数是可以传递参数的,在上个版本myBind中还未解决绑定返回的函数可以传参问题。

javascript 复制代码
Function.prototype.myBind=function(thisArg,...args){
    const self=this
    return function(...innerArgs){
        const finalArgs=[...args,...innerArgs]
        return self.apply(thisArg,finalArgs)
    }
}

由于bind创建的新的绑定指向的函数还可以通过new来创建实例,所以我们要考虑如何让我们的myBind可以实现new。

javascript 复制代码
//在我们的myBind函数里面
...
function(...innerArgs){
    const finalArgs=[...args,...innerArgs]
    return fn.apply(thisArg,finnalArg)
}
//用new来创建f的实例
const newFn=new f()

在new的过程中会执行构造函数的代码,因此此处绑定函数f中的代码会执行。而fn.apply(thisArg,finalArgs)里的thisArg会仍然有效,但是这就不符合bind的方法了。

我们面临着如何解决new创建绑定函数的实例时,要让先前传入的thisArg失效的问题。

对于绑定函数f来说,执行时的this的不确定的。如果我们直接执行f,那么绑定函数的this应该执行全局对象window。如果我们用new来创建f的实例,那么f中的this应该指向新创建的实例。

基于上述情况,我们决定修改myBind返回的绑定函数,在函数内部对this进行判断,从而区分是否使用了new运算符。

最终版myBind

javascript 复制代码
Function.prototype.myBind=function(thisArg,...args){
    const self=this
    const bound=function(...innerArgs){
        const finalArgs=[...args,...innerArgs]
        const isNew=this instanceof bound
        if(isNew){
            return new self(...finalArgs)
        }
        return self.apply(thisArgs,finalArgs)
    }
    return bound
}

Ending

以上内容就是手撕call、apply、bind三个绑定this函数的实现了。

相关推荐
七灵微1 小时前
【后端】单点登录
服务器·前端
xzkyd outpaper3 小时前
从面试角度回答Android中ContentProvider启动原理
android·面试·计算机八股
持久的棒棒君5 小时前
npm安装electron下载太慢,导致报错
前端·electron·npm
yours_Gabriel6 小时前
【java面试】微服务篇
java·微服务·中间件·面试·kafka·rabbitmq
crary,记忆7 小时前
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
前端·webpack·angular·angular.js
漂流瓶jz8 小时前
让数据"流动"起来!Node.js实现流式渲染/流式传输与背后的HTTP原理
前端·javascript·node.js
SamHou08 小时前
手把手 CSS 盒子模型——从零开始的奶奶级 Web 开发教程2
前端·css·web
我不吃饼干8 小时前
从 Vue3 源码中了解你所不知道的 never
前端·typescript
开航母的李大8 小时前
【中间件】Web服务、消息队列、缓存与微服务治理:Nginx、Kafka、Redis、Nacos 详解
前端·redis·nginx·缓存·微服务·kafka