简洁的代码,详细的解释,每个前端er都要会的手写bind方法。

1,784 阅读5分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

又是一年的金三银四,疯狂的手写剧情再度上演,接下来就让我们一起来实现下JavaScript里的bind函数。

一、思路梳理

在实现手写bind之前让我们先来梳理下bind函数的使用场景。通常我们在使用bind的时候,需要两个对象(所有的一切都是对象🙈):

  1. 想要执行的function(我们假设叫foo)。
  2. 希望能够调用foo的对象(我们假设叫obj)。

那我们在实现bind函数的时候主要突破点就是能够获取到foo以及obj。

还有一点bind和apply或者call方法不同的地方。bind会返回一个函数,再次执行返回的函数,才能是我们目标函数的真正的执行结果。所以对于目标函数foo原本需要的参数的传递方式就有三种情况:

  1. 在调用bind函数时传递
  2. 在调用bind返回的函数时传递
  3. 在调用bind时传递一部分,调用bind返回的函数时传递一部分。

这是在实现bind函数时另一个需要注意的地方。

那么接下来我们就带着这样的一个思路来实现bind方法吧。

二、bind方法的实现

function jBind(thisArg,...args) {
  return (...argus) => {
    // 传进来的是null和undefined时将this指向为window
    thisArg = thisArg ?? window

    // 确保传进来的是Object
    thisArg = Object(thisArg)
    
    // 生成一个唯一的key值,防止和传进来的对象的key重复
    const key = Symbol()
    thisArg[key] = this

    // 获得函数的执行结果
    const result = thisArg[key](...[...args, ...argus])

    // 删除手动添加的属性,使thisArg回到最初的样子
    delete obj[key]

    // 返回函数的执行结果
    return result
  }
}

Function.prototype.jBind = jBind

三、测试结果展示

function foo(x, y, z) {
  console.log(x + y + z, this);
}

const obj = {
  name: 'ZJoker',
}

foo.jBind()()
foo.jBind(null,1,2)(3)
foo.jBind(undefined,1,2,3)
foo.jBind(123,1,2,3)()
foo.jBind('123',1,2,3)()
foo.jBind(false,1,2,3)()
foo.jBind([],1,2,3)()
foo.jBind({},1,2,3)()

console.log('---------我是分割线----------');

foo.bind()()
foo.bind(null,1,2)(3)
foo.bind(undefined,1,2,3)()
foo.bind(123,1,2,3)()
foo.bind('123',1,2,3)()
foo.bind(false,1,2,3)()
foo.bind([],1,2,3)()
foo.bind({},1,2,3)()

在这里插入图片描述

四、实现的详解

4.1 获取需要调用目标function的对象

   // 传进来的是null和undefined时将this指向为window
   thisArg = thisArg ?? window

   // 确保传进来的是Object
   thisArg = Object(thisArg)

在思路梳理里我们提到的需要调用函数foo(目标function)的对象obj(目标对象)其实就是bind方法的第一个参数,所以我们这里用形参thisArg来获取obj。

但是,对于使用bind方法时是否传参或者传的第一个参数的类型是什么我们没有办法控制,所以我们要手动的处理下传过来的参数。

4.2 获取被调用的目标function

  // 生成一个唯一的key值,防止和传进来的对象的key重复
  const key = Symbol()
  thisArg[key] = this

通常情况下我们在使用bind方法的时候,都是xxx.bind()()的方式,所以bind方法的执行是被函数foo唤起的。也就是说bind方法内的this指向的就是我们的目标function foo。

至此我们就已经完全的获取到目标function foo以及目标对象obj。不过这里有一种边界情况需要我们去处理。

为了能够让obj(形参thisArg)调用我们的foo,也就是将foo的this绑定到obj(bind方法的实际意义,改变函数的this指向)。我们需要以obj.foo()的形式执行目标函数。所以我们给obj新增一个方法就是我们的foo。但是key如何起名,是一个值得考虑的问题,我们要避免和obj原有的方法重名,所以这里使用了Symbol来保证key值的唯一。

4.3 目标function参数的获取

return (...argus) => {

   ... ...
   
   // 获得函数的执行结果
   const result = thisArg[key](...[...args, ...argus])

   // 删除手动添加的属性,使thisArg回到最初的样子
   delete obj[key]

   // 返回函数的执行结果
   return result
 }

在思路梳理我们提到foo可能会有自己的参数,并且可以有三种传递方式。所以在bind方法中,除了第一个参数目标对象以外,还可能或有剩余的其他参数来作为foo的参数。

这里我们利用ES6中的剩余参数(Rest Parameters)的概念接收bind方法中除了第一个参数以外的参数,即jBind函数中的形参...args。即下面一行代码中的...args

function jBind(thisArg,...args) 

同时为了满足能够在执行bind方法返回的函数时也可以传递foo的参数,所以我们在返回的函数里也有形参来接收参数。

这里同样利用ES6中的剩余参数(Rest Parameters)的概念来获得foo的另一部分参数(...argus),即下面一行代码中的...argus

 return (...argus) => {

最后我们利用ES6中的展开语法(Spread syntax)的概念来将所有接收到的参数传递给foo并执行,即下面一行代码中的...[...args, ...argus]

thisArg[key](...[...args, ...argus])

最后我们把手动添加到obj的方法foo删除,使thisArg回到最初的样子,并将obj.foo()的执行结果返回。

结语

其实不难发现,在bind函数的实现过程中主要是利用了闭包

以上就是手写bind的实现过程,一个出自js小白的学习记录,不对的地方欢迎指正。

---------------我是分割线---------------

简洁的代码,详细的解释,每个前端er都要会的手写bind方法。

简洁的代码,详细的解释,每个前端er都要会的手写apply方法。

简洁的代码,详细的解释,每个前端er都要会的手写call方法