手写 bind

939 阅读3分钟

bind 是在 ES5 加入的语法,具体如何使用 MDN 都有。但有一点需要额外注意一下:bind 会返回一个新的函数。也就是说:它会修改 this 的指向,并返回新的函数体。

接下来,我来讲一下手写 bind 的思路。

首先我们要明白 bind 的用途:它是为了改变默认 this 的,如:

var obj = { a: 1 }

function originFn(a) {this.a = a}

var newFn = originFn.bind(obj)
newFn(3)

console.log(obj.a) // 3

也就是说,此时 newFn 的代码实际上是下面这样的

function xxx(newA) {
   obj.a = newA // 当前 this 是 obj
}

了解了如何使用,我们才能往下写,我们来为函数的原型添加一个 myBind 方法:

(Function.prototype.myBind = function(otherThis) {
  // 如果 this 的类型不是函数,就返回
  if (typeof this !== 'function') {
    return
  }
  // 获取原始函数除去第一项后的参数,也就是除去 this 的剩余参数
  var bindArgs = Array.prototype.slice.call(arguments, 1) 
  var bindArgsLength = bindArgs.length  // 这句话很重要,代码后有解释

  var fBind = this // 要绑定到哪里,暂存 this
  var fNP = function(){}  // 中间量,以便生成的新函数和原来函数毫无瓜葛
  var fBound = function() {
    bindArgs.length = bindArgsLength // 必须重置 bindArgs 的长度,下面有解释
    bindArgs.push.apply(bindArgs, arguments) // 组合两次的数据

    return fBind.apply(otherThis, bindArgs) // 用 apply 方法为新的返回值绑定用户指定的 this
  }

  if (this.prototype) { // Function.prototype 没有 prototype
    fNP.prototype = this.prototype
  }

  fBound.prototype = new fNP() 
  
  return fBound
})()

需要说明一下重置 bindArgs 那一句话:

因为 bindArgs 是和 fBound 同一级别的,当执行 myBind 这个函数之后,fBound 和 bindArgs 都只有一份,但是可能多次调用 myBind 返回的函数并传参,如果不重置,我们两次调用,那 bindArgs 的长度就变为 2 了,但我们希望两次调用不能被影响,只能受父亲传参的影响。由于每次添加都是 push 到后面,所有每次只要恢复成以前的长度即可。

好了,我们已经大致完成了,我们已经可以在函数种使用了。

但是还有一个缺陷:不支持 new,真正的 bind 方法在使用 new 之后的 this 还是指向 new 新生成的对象。

举例来说,我们希望得到下面的效果:

var obj = { a: 1 }
function originFn(a) {this.a = a}
var newFn = originFn.myBind(obj)
var brandNewObj = new newFn('你好')
console.log(obj.a) // 1 
console.log(brandNewObj.a) // 你好

而我们现在的代码是如何的呢? 由于我们没做判断,我们返回的函数实际还是:

// 伪代码
function brandNewObj(newA) {
   obj.a = newA 
}

这样的话,最终的结果是:brandNewObj.a 未定义,而 obj.a 是你好。

如何解决?只需在执行 apply 的时候绑定更换绑定的 this 就好了。

解决的关键是 this 到底指向什么,如果使用 new 操作符,那个时候的 this 就是指向新的对象

(Function.prototype.myBind = function(otherThis) {
  // 当前的 this 是 originFn,如何 this 的类型不是函数,就返回
  if (typeof this !== 'function') {
    return
  }

  var bindArgs = Array.prototype.slice.call(arguments, 1) // 获取除去第一项后的参数
  var bindArgsLength = bindArgs.length 

  var fBind = this // 当前的调用 bind的 函数
  var fNP = function(){}  // 中间量,以便生成的新函数和原来函数毫无瓜葛

  if (this.prototype) { // Function.prototype 没有 prototype
    fNP.prototype = this.prototype
  } // 放在前面便于理解

  var fBound = function() {
    bindArgs.length = bindArgsLength // 必须重置 bindArgs 的长度
    bindArgs.push.apply(bindArgs, arguments) // 组合两次的数据
      
      // 如果使用 new 操作符,此时 this 指向的是新生成的对象,那么 fNP.prototype.isPrototypeOf(this) === true
      // brancNewObj -> newFn -> FNP
      return fBind.apply( fNP.prototype.isPrototypeOf(this) ? this: otherThis, bindArgs) 
  }

  fBound.prototype = new fNP() 
  
  return fBound
})()

写完了,昨天花了一个下午的时间在理解这个问题,我认为理解起来还是有点难度的,希望帮助到你。