【 js手写系列】实现自己的bind

1,387 阅读5分钟

努力让学习成为一种习惯,自信来源于充分的准备

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

前言

vue中,我们可以在methods里面访问vue实例:

export default {
    methods: {
        test() {
          this.XX
        },
    }
}

这里有个疑问就是:test方法里面的this理论上应该是methods引用的对象。为什么会是vue实例呢?

显然vue内部做了处理,显示绑定了thisvue实例。我们可以看下 vue源码

image.png

image.png

因此我们可以在method方法内部使用vue实例上的属性、方法

无论是日常使用的第三方库还是日常业务开发。bind的出场率都非常高,接下来让我们实现bind

解读

function bar (a, b, c) {
  console.log(this.value);
  console.log(a);
  console.log(b);
  console.log(c);
}
const foo = bar.bind({value: 'value'}, 1)
foo(2,3) // 'value'、1、2、3

在实现之前,我们先从规范结合现象来分析下bind具体有哪些行为表现

bind规范如下(具体规范详情可以查看Function.prototype.bind

image.png

其中4-10步主要设置函数相关信息(设置长度、函数名称等),不是本文的重点,不展开讨论

底部的注意点和 callapply一样,如果是箭头函数或者之前已经绑定过则会失效

整体流程大同小异,具体相似的细节不再描述,可以参考实现自己的call、apply

我们重点关注第3步,可以发现bind新创建了一个函数,并在最后将其作为返回值返回,我们用代码验证下

function bar() {}
const bar2 = bar.bind()
console.log(bar === bar2); // false

我们继续深入看看第三步到底做了什么

image.png

简单翻译下步骤:

  1. 创建一个基本对象,并设置其 prototype属性为绑定函数(targetFunction)的prototype
  2. 设置内部属性[[call]]、[[Construct]]
  3. 设置内部属性[[BoundTargetFunction]]为绑定的函数
  4. 设置内部属性[[BoundThis]]为传入的this
  5. 设置内部属性[[BoundArguments]]为传入的arguments
  6. 返回该对象

这里可能有些晦涩难懂,我们通过一段代码打印直观的看下👀

function foo() {}
const _foo = foo.bind({a:1}, 1,2)
console.dir(foo)
console.dir(_foo)
console.log(Object.getPrototypeOf(foo) === Object.getPrototypeOf(_foo)) // true

image.png

image.png

通过打印_foo。再回头看规范,我们可以很清晰的了解了bind做了哪些事情

  1. _foo函数的名称在原基础上加了bound前缀,并且同步设置了length
  2. 设置_foo函数的[[prototype]],指向绑定函数foo的[[prototype]]
  3. 设置[[BoundThis]]为传入的对象
  4. 设置[[BoundArgs]]为传入的参数

到这里 bind的核心步骤相信大家已经有个清晰的概念了。可能还会有个疑惑。第二步设置[[prototype]]指向的目的是什么。别着急,下来会讲到。我们一步一步来实现bind

实现

第一步

首先bind相比于apply的最大直观区别是:bind不会立刻执行,它只是绑定this,并返回一个新的函数,将新函数的执行权交给用户,我们可以很自然的想到返回apply函数就可以了

Function.prototype.bind2 = function (context) {
  var _this = this
  return function () {
    return _this.apply(context)
  }
}

function foo() {
  console.log(this.a);
}
const _foo = foo.bind({a:1})
_foo() // 1

第二步

bind函数的另一个特点是可以传入参数,作为最后调用后的参数。我们可以利用闭包的特性将最初传入的参数缓存下来

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

function foo(...args) {
  console.log(this.a);
  console.log(args);
}
const _foo = foo.bind({a:1}, 1,2,3)
_foo(4,5) // 1,1,2,3,4,5

第三步

接下来是bind最难也是容易忽视的一个点(与callapply最大的不同),bind绑定的函数可以作为构造函数这也是之前提到的[[prototype]]赋值的核心原因

我们来看个例子

const obj = {a:1}

function foo(num1, num2) {
  this.num1 = num1
  this.num2 = num2
}

foo.prototype.test = function() {
  console.log(this.num1);
  console.log(this.num2);
}

const _foo = foo.bind(obj, 1)
const i = new _foo(2)
i.test() // 1,2
console.log(obj); // {a:1}

this绑定的优先级中:new绑定 > 显式绑定,具体可以参考深入理解this机制

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数

我们可以拆分成两个步骤实现

  1. _foo函数被作为构造函数调用,并且实例可以调用绑定函数原型链上的方法
Function.prototype.bind2 = function (context) {
  var args = Array.prototype.slice.call(arguments, 1)
  var _this = this
  function fBind () {
    var bindArgs = args.concat(Array.prototype.slice.call(arguments))
    return _this.apply(context, bindArgs)
  }
  // 让fBind的原型指向绑定函数的原型
  fBind.prototype = this.prototype
  return fBind
}
  1. 如果是作为构造函数调用,原本传入bind中的上下文对象obj会被忽略,参数仍被使用,这里的关键在于判断内部函数func是作为构造函数还是普通函数调用。从而决定传给apply的this指向
Function.prototype.bind2 = function (context) {
  var args = Array.prototype.slice.call(arguments, 1)
  var _this = this
  function fBind () {
    var bindArgs = args.concat(Array.prototype.slice.call(arguments))
    return _this.apply(this instanceof fBind ? this : context, bindArgs)
  }
  fBind.prototype = this.prototype
  return fBind
}

到这里就是bind的最终核心实现了,最后有两个细节需要调整下:

  1. bind传入的不是函数时,应该给出报错
  2. fBind.prototype = this.prototype 两者是同一个引用,如果修改了fBindprototype那么原绑定函数的prototype也会被修改
Function.prototype.bind2 = function (context) {
    if (typeof this !== "function") {
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var args = Array.prototype.slice.call(arguments, 1)
    var _this = this
    if (this.prototype) {
      fNOP.prototype = this.prototype
    }
    fBInd.prototype = new fNOP()
    
    function fNOP() {}
    function fBind () {
      var bindArgs = args.concat(Array.prototype.slice.call(arguments))
      return _this.apply(this instanceof fBind ? this : context, bindArgs)
    }

    return fBind
  }

最终版终于完成!!🎉🎉🎉

最后

到这里,就是本篇文章的全部内容了

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

如果你有疑问或者出入,评论区告诉我,我们一起讨论

参考文章

JavaScript深入之bind的模拟实现

MDN Bind