一文手撕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函数的实现了。

相关推荐
@大迁世界5 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路14 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug17 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213819 分钟前
React面向组件编程
开发语言·前端·javascript
学历真的很重要20 分钟前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
持续升级打怪中41 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星1 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端