0. 相关概念 📜

开门见山,首先我们先看看 bind 函数做了什么:

MDN:bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

由此我们可以首先得出 bind 函数的两个特点:

  1. 返回一个函数,并指定其实际调用者 this
  2. 绑定时可以传入参数。

了解了 bind 函数的相关逻辑后,我们可以尝试着手写实现,首先应关注的是 指定 this 这个功能,对此我们可以通过 apply 函数为其指定。于是我们有了如下的代码 👇

1. 手写实现 ✍

① 初版代码:指定 this

Function.prototype.myBind = function (context) {  // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 bar  const self = this  return function () {    // 利用 apply 指定传入的 context 作为 this    // 同时返回出去,因为原函数 *可能有返回值*    return self.apply(context)  }}// 使用let foo = {  value: 1}function bar() {  console.log(this.value)}function test() {  return this.value // 可能有返回值}// 调用 myBind,传入的 context 为 foo,并且返回一个函数let bindFoo1 = bar.myBind(foo)let bindFoo2 = test.myBind(foo)bindFoo1() // 1console.log('bindFoo2:', bindFoo2()) // 存在返回值的情况

初步代码初步实现了 this 的绑定,但考虑不周:因为原生 bind 函数是可以传参的,并且可以分两步走,即绑定时可以传参,后续调用时也可以传参。 即如下代码:

let foo = {  value: 1}function bar(name, age) {  console.log(this.value)  console.log(name)  console.log(age)}// 绑定时传参let bindFoo = bar.bind(foo, 'John')// 调用时传参bindFoo('13')

对此,我们可以为传入的参数做一次合并,这就要使用到 Arguments 对象了,它可以轻松地取得我们调用函数时传入的参数。

② 二版代码:实现传参

Function.prototype.myBind = function (context) {  // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 bar  const self = this  // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)  // 这里产生了闭包~  const args = Array.prototype.slice.call(arguments, 1)  return function () {    // 这个时候的 arguments 是指 myBind 返回的函数传入的参数,我全都要!    const bindArgs = [...arguments] // 可以采用 ES2015 的语法将其转为数组    // 合并    return self.apply(context, args.concat(bindArgs))  }}

第二版代码已经初步实现了 bind 函数,但仍有欠缺,因为原生 bind 函数还有一个用法:作为构造函数使用的绑定函数。对此,简单来说就是:一个调用了 bind 之后返回的函数,也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器。这样做的话,原本绑定传入的 context 参数会被忽略,但传入的参数依然生效。如下例:

let value = 2 // 用于后续验证let foo = {  value: 1}function bar(name, age) {  this.habit = 'eating'  console.log(this.value)  console.log(name)  console.log(age)}let bindFoo = bar.bind(foo, 'John')let obj = new bindFoo('18')// undefined  // 原有的 this 失效// John// 18console.log(obj.habit)// eating// 而使用我们自编的代码:let myBindFoo = bar.myBind(foo)let myObj = new myBindFoo('John', '18')// 1 // 原有的 this 未失效// John// 18// 并且你可以尝试控制台输出:myObj 是空对象,而 habit 被挂到了 foo 对象上

由上述代码可见,使用原生 bind 生成绑定函数后,通过 new 操作符调用该函数时,this.value 是一个 undefined,既未输出 foo 对象内的 value,也未输出 window 对象的 value。实际上此时 this 指向的就是自身 obj,该现象符合正常调用 new 操作符时的表现。

而采用 myBind 生成绑定函数后,后续的操作都运用在了 foo 对象上。具体原因就像是使用 new 操作符执行该绑定函数时内部的 this 值未更改。简单来说,就是有 newnew 都一样!

那么问题出在哪?

其实就出在我们返回的是一个 self.apply(context) 函数,当其作为构造函数执行时,其 this 还是会被 apply 更改为 context,并非为新实例。

问题描述完毕,这个缺点我们可以通过增加一个调用者判定进行完善。

③ 三版代码:完善逻辑

Function.prototype.myBind = function (context) {  // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 bar  const self = this  const args = Array.prototype.slice.call(arguments, 1)  // fBound 指的是绑定函数,指的是基于 self 之上包装了一层的函数  const fBound = function () {    const bindArgs = [...arguments]    // 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例    // 当作为普通函数时,this 一般指向 window,此时判定结果为 false,将绑定函数的 this 指向 context 即可    // this instanceof fBound 的 this 就是绑定函数的调用者    return self.apply(      this instanceof fBound ? this : context,      args.concat(bindArgs)    )  }  return fBound}// 使用let foo = {  value: 1}function bar(name) {  this.habit = 'eating'  console.log(this.value)  console.log(name)}bar.prototype.testFn = function () {  console.log('yes!')}let bindFoo = bar.myBind(foo)let obj = new bindFoo('John')// undefined// Johnconsole.log(obj.habit) // eatingconsole.log(obj instanceof bindFoo) // trueconsole.log(obj instanceof bar) // falseobj.testFn() // obj.testFn is not a function// ??????????又有坑!

通过对绑定函数的调用者进行判定,我们成功地将 new 操作符调用绑定函数行为恢复正常,但也不算太正常,毕竟生成的新实例居然用不了 bar.prototype 上的方法!

bind 函数在本意上,只是对原函数做一层包装,为其指明原函数的调用者,而不应该破坏其原有的逻辑。

在该版实现中,我们本意应该是令新实例由 bar 构造函数构造而成,但实例却无法使用原型对象上的方法。

针对此缺点,我们进行如下改进:

④ 四版代码:修复继承关系

该版代码的改进思路在于,将返回的绑定函数的原型对象的 __proto__ 属性,修改为原函数的原型对象。便可满足原有的继承关系。

Function.prototype.myBind = function (context) {  const self = this  const args = Array.prototype.slice.call(arguments, 1)  const fBound = function () {    const bindArgs = [...arguments]    return self.apply(      this instanceof fBound ? this : context,      args.concat(bindArgs)    )  }  // 采用 Object.create 将绑定函数的 prototype.__proto__ 修改为 self.prototype  // 因为绑定函数在原函数之上做了一层封装,这样更改才可以正确实现继承。  fBound.prototype = Object.create(self.prototype)  return fBound}// 使用let foo = {  value: 1}function bar(name) {  this.habit = 'eating'  console.log(this.value)  console.log(name)}bar.prototype.testFn = function () {  console.log('yes!')}let bindFoo = bar.myBind(foo)let obj = new bindFoo('John')// undefined// Johnconsole.log(obj.habit) // eatingobj.testFn() // yes!console.log(obj instanceof bindFoo) // trueconsole.log(obj instanceof bar) // true

对此,我们已经基本实现了 bind 函数,不过仍然存在瑕疵,即关于 bind 函数的调用者的问题。

调用 bind 函数的必须是一个函数!基于此前提,我们可以通过必要的参数校验完善出最终的代码。

⑤ 最终代码:添加校验

Function.prototype.myBind = function (context) {  if (typeof this !== 'function') {    throw new Error(`${this}.bind is not a function`)  }  const self = this  const args = Array.prototype.slice.call(arguments, 1)  const fBound = function () {    const bindArgs = [...arguments]    return self.apply(      this instanceof fBound ? this : context,      args.concat(bindArgs)    )  }  if (self.prototype) {    fBound.prototype = Object.create(self.prototype)  }  return fBound}

最后部分对 self.prototype 进行了判断,因为有一种函数是没有 prototype 属性的,即 Function.prototype 。详见为什么 Function.prototype 可以直接执行?

完结撒花!💐

参考资料 📜

MDN: bind

《JavaScript 高级程序设计第 4 版》

cd ..

Copyright © 2022 · Jason Young

Made with ❤️