佐罗菌的小屋

谈谈 Function.prototype.bind() 的 polyfill

December 1, 2017 • ☕️ 3 min read

为啥写这篇博客

以前一直是忽略了 bind 函数是怎么实现的,以致于看见下面这段代码时不知所措。

function largest(arr) {
  return arr.map(Function.apply.bind(Math.max, null))
}

largest([[1,34],[456,2,3,44]) // [34, 456]

于是我便开始看 bind 的实现,其中也涉及了很多概念如下,是非常值得重温和学习的

  • 作用域链
  • 闭包
  • 柯里化
  • new 操作做了什么

基础

从一段代码开始

var obj = {
  a: 1,
  print(x, y) {
    console.log(this.a, x + y)
  }
}

var test = {
  a: 2
}

obj.print(1, 2) // 1 3

obj.print.apply(test, [1, 2]) // 2 3
obj.print.call(test, 1, 2) // 2 3
obj.print.bind(test)(1, 2) // 2 3
obj.print.bind(test, 1)(2) // 2 3
obj.print.bind(test, 1, 2)() // 2 3

目标

  1. 返回一个由指定的 this 值(第一个参数)和初始化参数的新函数(是原函数的拷贝)
  2. 返回的新函数可以继续指定参数
  3. bind 函数不可通过 new 调用
  4. 新的绑定函数也能使用 new 操作符创建对象,这样做时指定的 this 无效

从零开始实现

下面代码实现了最简单的 bind 函数,返回新函数绑定了传入的 this 上下文以及给新函数传入了默认参数 arg

Function.prototype.bind = Function.prototype.bind || function(context) {
  var target = this
  var args = Array.prototype.slice.call(arguments, 1)
  return () => target.apply(context, args)
}

然后我们要求新函数也可以传入参数,这时候就不能用箭头函数了

Function.prototype.bind = Function.prototype.bind || function(context) {
  var target = this
  var args = Array.prototype.slice.call(arguments, 1)

  return function() {
    var newArgs = Array.prototype.slice.call(arguments)    target.apply(context, newArgs.concat(args))
  }
}

new fn.bind() 是非法的,非法调用的话要抛出异常

Function.prototype.bind = Function.prototype.bind || function(context) {
  if (typeof this !== 'function') {    throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable')  }
  var target = this
  var args = Array.prototype.slice.call(arguments, 1)

  return function() {
    var newArgs = Array.prototype.slice.call(arguments)
    target.apply(context, newArgs.concat(args))
  }
}

再来实现返回函数支持 new 操作。当代码 new Foo(...) 执行时:

  1. 一个新对象被创建。它继承自 Foo.prototype
  2. 调用构造函数 Foo,并将 this 绑定到新创建的对象。
Function.prototype.bind = Function.prototype.bind || function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable')
  }

  var target = this
  var args = Array.prototype.slice.call(arguments, 1)

  var fn = function() {}  fn.prototype = target.prototype  fnBound.prototype = new fn()  // or use `Object.create()`
  // fnBound.prototype = Object.create(target.prototype)

  return function fnBound() {
    var newArgs = Array.prototype.slice.call(arguments)
    var finalArgs = newArgs.concat(args)
    if (this instanceof fn) {      target.apply(this, finalArgs)    } else {      target.apply(context, finalArgs)
    }
  }
}

到了这里,代码其实和 MDN 里的 polyfill 差不多了,代码见链接

已经完美?

当我们认为这份代码已经完美时,这时应该看看 es5-shim 对 bind 的实现。搭配这篇博客(《从一道面试题的进阶,到“我可能看了假源码”(2)》)一起看效果更佳哦~


参考