实现一个bind方法是一件非常复杂的事情

201 阅读3分钟

在项目和框架中,最常用的三种改变this指向的方法有call bind apply ,其中 callapply是最早在标准中实现了的。都是通过调用了函数的内置方法 [[call]] 来实现 this 指向的改变。在仿写callapply的时候,主要是通过用对象属性调用的方式来实现,只需要判断传入的参数是否符合标准即可。而 bind 方法复杂的地方是它的返回了一个函数,而没有调用它。JavaScript的函数既可以常规调用,又可以通过 new 调用。除此之外,函数还具有非常特殊的几个属性,比如 namelength。在这些特性中有一部分是我们在创建的时候程序帮我们处理好的,还有一部分需要我们自己绑定。

因为[[call]]方法无法外部调用,而本身call和apply的实现就是调用了 [[call]] 这个内部方法,所以方法中涉及到修改this指向的几乎都是通过调用 call和apply方法实现的

完整的代码片段全部是来自es5-shim

  1. 先简单获取一些基本的属性
Function.prototype._bind = function() {
  var target = this
  // 要求target必须是一个可调用对象,在这里就简单的判断成一个函数
  if(typeof target !== 'function'){
    throw TypeError('is not a function')
  }
  
  var [thisArg, ...args] = arguments
}

这一步的关键是容易忽略this类型的判断

  1. 处理 binder 函数的调用
Function.prototype._bind = function() {
  // ...
  
  var bound
  var binder = function() {
      if (this instanceof bound) {
          var result = Function.prototype.apply.call(target, thisArg, [
              ...args, ...arguments
          ])
          
          if (Object(result) === result)
              return result

          return this
      }else {
          return Function.prototype.apply.call(target, thisArg, [
              ...args, ...arguments
          ])
      }
  }
  
  // ...
}

binder函数最终是执行修改this值的函数,值得注意的是如何使用 applycall 来完成调用

  1. 绑定返回函数 bound 的自身的属性
Function.prototype._bind = function() {
  // ...
  
  var boundLength = 0
  var boundArgs = []

  boundLength = Math.max(0, target.length - args.length)

  for(var i = 0; i < boundLength; i++) {
      boundArgs.push('$' + i)
  }

  bound = Function('binder', `return function (${boundArgs.join(',')}) {
    return binder(arguments)
  }`)(binder)
  
  bound.name = `bound ${target.name}`
  
  // ...
}

以上这段代码我已经写过很多遍了,但是每一次写到这还是会停下来思考这些代码都做了什么,这里也是 bind 方法比较难理解的和想到的地方。获取 bound length 的地方是贴着标准实现的,这个可以参考后面的传送门,难想到的是如何设置参数。

bind 除了具有函数自身的特性之外,还可以分步的接受参数,这就是为什么大家都喜欢用 bind 来实现柯里化

    var foo = function (a, b) { console.log(a, b) }
    
    var bindFoo = foo.bind(null, 'a')
    bindFoo.length // 1
    
    bindFoo('b') // a, b

可以看到在使用bind的时候,如果如参的个数小于形参的个数,那么少的部分就会作为生成的 bindFoo 的形参。

而在JavaScript中有能力设置形参的方式就是 Function。所以通过自调用的方式将 binder 函数传入 bound 函数内部,同时设置了 bound 函数的形参

  1. 完成 bound 函数的原型链绑定
Function.prototype._bind = function() {
  // ...
  var Noop = function() {}
  
  var proto = target.getPropertyOf()
  if (proto){
    Noop.setPrototypeOf(proto)
    bound.setPrototypeOf(new Noop)
    Noop.setPrototypeOf(null)
  }
  
  return bound
}

最后就是设置了 bound 的原型,就结束了。

其实这是一个非常好的题,因为题目本身涉及的内容非常的广泛,而且功能的完善是分成了多个步骤,可能考验应试者对于函数特性的理解程度。

参考资料

github.com/es-shims/es…

tc39.es/ecma262/#se…