New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

深入学习JavaScript之一:bind函数的实现 #1

Open
shhdgit opened this Issue Feb 28, 2017 · 4 comments

Comments

Projects
None yet
3 participants
@shhdgit
Owner

shhdgit commented Feb 28, 2017

这是我第一次认真组织blog,也是本系列的第一篇文章。既然这个系列是学习JavaScript,那么就从实现ES5的一些api开始吧,代码主要参考es5-shim,坚持下去的话下一步自然是实现ES6的api。写blog主要还是为了记录下自己的学习历程,不能总是看完文章、学完知识就丢到脑后,话不多说,坚持最重要。

前言

大家都知道在JavaScript中的bind是用于绑定函数this指向的。bind方法会创建一个新函数,bind的第一个参数将作为新函数运行时的this,其余的参数将会在新函数后续被调用时位于其他实参前被传入。那么很明显,用bind函数可以很轻松的实现函数的curry化,这里就不展开介绍了,我们一起来看一看引擎盖下面是什么吧。

目标

  • 返回新函数,调用新函数的返回值与调用旧函数的返回值相同
  • 新函数的this指向bind的第一个参数
  • 其余参数将在新函数后续被调用时位于其它实参前被传入
  • 新函数也能使用new操作符创建对象,构造器为原函数

思路

  • 首先我们的实现是一个猴子补丁,即给内置对象添加方法:
Function.prototype.bind = Function.prototype.bind || bind
  • 返回新函数,很简单,return一个包裹着旧函数调用的新函数即可:
function bind () {
  var target = this
  return function () {
    return target()
  }
}
  • 新函数的this要指向bind的第一个参数,也简单,用js的call或apply函数就可以达成目标:
function bind (that) {
  var target = this
  return function () {
    return target.call(that)
  }
}
  • 其余参数将在新函数后续被调用时在其它实参前被传入,类似实现curry化时通过闭包保存参数:
function bind (that) {
  var target = this
  var bindArgs = Array.prototype.slice.call(arguments, 1)
  return function () {
    var callArgs = Array.prototype.slice.call(arguments)
    return target.apply(that, bindArgs.concat(callArgs))
  }
}

这里要绕过来的点在于bind调用时获取的是bind函数其余的参数bindArgs,新函数被调用时则获取到传入新函数的参数callArgs。两组参数组合后传入原函数,即bind的其余参数将在新函数被调用时位于其它实参前被传入。

  • 最后要达成的目标是在使用new操作符时,旧函数能作为构造器被调用。
    那么我们知道new fn()操作执行时,一个新的对象会被创建,并且该对象继承自fn.prototype,然后fn会被执行,fn的this指向这个新对象(当然最后还有return的过程)。从new操作的顺序来看我们能通过 this instanceof fn 这段代码判断某次调用是否通过new调用,如果是通过new调用的,那么this就是fn的实例。直接调用函数时的this指向global。知道了这点之后我们该如何改造我们的代码以达成目标呢:
function bind (that) {
  var target = this
  var bindArgs = Array.prototype.slice.call(arguments, 1)

  function bound () {
    var callArgs = Array.prototype.slice.call(arguments)
    if (this instanceof bound) {
      return target.apply(this, callArgs.concat(bindArgs))
    } else {
      return target.apply(that, callArgs.concat(bindArgs))
    }
  }

  // 实现继承,让bound函数生成的实例通过原型链能追溯到target函数
  // 即 实例可以获取/调用target.prototype上的属性/方法
  var Empty = function () {}
  Empty.prototype = target.prototype
  bound.prototype = new Empty()  // 这里如果不加入Empty,直接bound.prototype = target.prototype的话
                                 // 改变bound.prototype则会影响到target.prototype,原型继承基本都会加入这么一个中间对象做屏障

  return bound
}

至此我们实现了一个功能完整够用的bind函数,当然如果是写一个库,那就要应对各种奇怪的情况,考虑的需要更加严谨全面。更严谨的代码可以参考es5-shim的实现。

@shhdgit shhdgit added the JavaScript label Feb 28, 2017

@zerosrat

This comment has been minimized.

Show comment
Hide comment
@zerosrat

zerosrat Dec 1, 2017

if (this instanceof fn) 应为 if (this instanceof Empty)?

zerosrat commented Dec 1, 2017

if (this instanceof fn) 应为 if (this instanceof Empty)?

@shhdgit

This comment has been minimized.

Show comment
Hide comment
@shhdgit

shhdgit Dec 4, 2017

Owner

@zerosrat 是的,非常感谢 : D

Owner

shhdgit commented Dec 4, 2017

@zerosrat 是的,非常感谢 : D

@Tvinsh

This comment has been minimized.

Show comment
Hide comment
@Tvinsh

Tvinsh Mar 6, 2018

callArgs.concat(callArgs) 应该是 callArgs.concat(bindArgs)

Tvinsh commented Mar 6, 2018

callArgs.concat(callArgs) 应该是 callArgs.concat(bindArgs)

@shhdgit

This comment has been minimized.

Show comment
Hide comment
@shhdgit

shhdgit Mar 6, 2018

Owner

@Tvinsh 已修改,非常感谢!

Owner

shhdgit commented Mar 6, 2018

@Tvinsh 已修改,非常感谢!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment