通过自己实现 bind 来理解 bind

分析 bind 的特点

MDN 的无废话版本介绍 bind

bind()方法创建一个新的函数, 当被调用时,将其 this 关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

实现最小可用版本

  1. 会创建一个新函数
  2. 在调用这个新函数时 this 关键字设置为提供的值(传入的参数)
var cat = {
  face: '🐱'
}
var dog = {
  face: '🐶'
}

function fn() {
  console.log(this.face)
}
Function.prototype.bind2 = function(context) {
    // 这里的 this 指向调用 bind2 的那个函数
    self = this
    return function() {
      return self.apply(context)
    }
  }
  // test
var bindCat = fn.bind2(cat)
bindCat(); // 🐱
bindCat(dog); // 🐱

是在进入 bind2 函数时用一个 self 的变量来存储当前 this 的引用(this 指向调用 bind2 的那个函数)。返回的函数在被调用时,在作用域链上来拿到这个 self 。在返回函数中的 return 是为了在绑定函数 fn 有返回值的时候,能顺利的通过 bindCat 拿到该返回值

发现问题,继续改进

上面的版本,我们会发现无法给 bind2 传入参数。再回头看看给 bind 传参时的特点:「在调用新函数时,在任何提供之前提供一个给定的参数序列」怎么来理解这句话呢?看一个例子就懂了呢

  face: '🐱'
}
var dog = {
  face: '🐶'
}

function fn(name, age) {
  this.name = name
  this.age = age
  console.log(this.face)
}

var bindCat = fn.bind(dog, 'Lucy')
bindCat(2)

我们在绑定时就给 bind 函数传入了一个参数'Lucy',在调用 bind 生成的新函数时又传了一个2。这就是 bind 传参的特点:参数'Lucy'会在调用 fn 时传递的任何实参之前先传给 fn。那就针对这个特点来实现第二个版本吧

Function.prototype.bind2 = function(context) {
  var args = Array.prototype.slice.call(arguments, 1)
  self = this
  return function() {
    var bindArgs = Array.prototype.slice.call(arguments)
    return self.apply(context, args.concat(bindArgs))
  }
}

我们通过来拿到 bind2 的所有参数的类数组对象 arguments,再用借用数组的方法去掉参数里的第一项后组成一个新数组 args。为什么去掉第一项呢? arguments[0] === dog它不需要被我们传入到 fn 中。然后再用同样的方法拿到 bind2 返回的函数传入的参数的数组bindArgs,并将它放到 args后面拼接成新数组来传给 apply

最终完善

bind 还有另一个特点,简单的来说就是它的优先级低于 new。也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。看个例子就能明白了

var cat = {face: '🐱'}
var dog = {face: '🐶'}
function fn(name,age){
  this.name = name
  this.age = age
  console.log(this.face)
}

var bindCat = fn.bind(cat,'Lucy')
var obj = new bindCat(2)
console.log(cat)
console.log(obj)

从控制台的输出分析,在 bindCat 被 new 调用时内部的 this 指向被改变成 obj 了。之前让 cat 绑定在 bindCat 上失效了。 来分析我们之前第 2 版本的代码会让它失效吗?

Function.prototype.bind2 = function(context) {
  // 这里的 this 依然是 fn
  self = this
  var args = Array.prototype.slice.call(arguments, 1)
  return function() {
    var bindArgs = Array.prototype.slice.call(arguments)
      // 🐛 而我们在这里调用 fn 时给它绑定 this 值还是 传进来的 cat。显然不是我们想要的
    console.log(self)
    return self.apply(context, args.concat(bindArgs))
  }
}

找出了问题所在我们再修改代码

Function.prototype.bind2 = function(context) {
  var args = Array.prototype.slice.call(arguments, 1)
  self = this
  var fBound = function() {
      var bindArgs = Array.prototype.slice.call(arguments)
        // 我们可以在这个 console.log(this) 来观察 this 的指向是这个函数构造出来的实例对象 obj
        // 所以 这里的 this 是就是我们想要的那个值
      return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs))
    }
    // 为了让实例 obj 能继承 fn 原型对象上的属性
  fBound.prototype = this.prototype
  return fBound
}
var cat = {face: '🐱'}
var dog = {face: '🐶'}
function fn(name,age){
  this.name = name
  this.age = age
console.log(this.face)
}
fn.prototype.friend = dog

var bindCat = fn.bind(cat,'Lucy')
var obj = new bindCat(2)

总结

是在理解《JavaScript深入之bind的模拟实现》之后再实现的过程给自己讲一遍。前两种版本的实现很容易理解,但到了第三个版本就需要画原型图来辅助自己思考了。

另外,在 MDN 给出的 pollfill 中,在处理返回函数 fBound 继承绑定函数 fn 是时用到一个技巧,琢磨了很多遍之后算是明白怎么一回事,但还是很难把它讲得很明白。

他的方法是:定义一个空函数,那这个空函数就有对应的原型对象。将空函数的原型对象指向需要被继承函数的原型对象。再通过 new 创建空函数的实例对象。再让指向空函数的实例对象指向继承函数的原型对象(等于把被继承的原型对象替换成空函数的实例对象)。从而达到继承的目的

在之后写「继承的各种写法」的时候再回头来理解一下吧

参考链接