coverPiccoverPic

细节解析 JavaScript 中 bind 函数的模拟实现

大家的阅读是我发帖的动力,本文首发于我的博客:deerblog.gu-nami.com/,欢迎大家来玩,转载请注明出处

💢前言

bind是一个改变函数this指针指向的一个常用函数,经常用在涉及this指针的代码中。来看 MDN 的文档

Function实例的bind()方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其this关键字设置为给定的值,同时,还可以传入一系列指定的参数,这些参数会插入到调用新函数时传入的参数的前面。

最近搞出了一个很难注意得到的 bug,bind函数返回的函数中,原函数的属性消失了,导致了一个工具函数的失效。

ts
  1. function Message (/*...*/) {/*...*/}
  2. Message.success = function (/*...*/) { return Message('success', /*...*/) }
  3. // ...
  4. xxx.Message = Message.bind(/*...*/)
  5. // ...
  6. xxx.Message.success(/*...*/)
  7. // Uncaught TypeError: xxx.success is not a function

解决方法自然是Object.keys()遍历一下原函数的属性,添加到新的函数上面。

来看看文档怎么说的:

绑定函数还会继承目标函数的原型链。然而,它不会继承目标函数的其他自有属性(例如,如果目标函数是一个类,则不会继承其静态属性)。

所以上面Message的属性就消失了。

后来去翻看Funtion.__proto__.bind的文档发现了一些以前从未注意到的内容,感觉挺有意思…

bind的功能主要有两点,一个是修改函数的this指针。

例如在老版本的 React Class 组件中使用回调函数:

ts
  1. export default class TestButton extends Component {
  2. testClickHandler () {
  3. console.log(this.state)
  4. }
  5. render () {
  6. return (
  7. <Button onClick={this.testClickHandler.bind(this)}>test</Button>
  8. )
  9. }
  10. }

setTimeout等回调中访问当前函数的this指针(当然现在我们都可以用箭头函数实现):

ts
  1. function test () {
  2. var that = this
  3. setTimeout(function () {
  4. console.log(that.test)
  5. })
  6. }

bind还可以暂存参数:

ts
  1. const func = (a, b) => a + b
  2. const funcBound = func.bind(null, 1)
  3. funcBound(2) // 3
  4. func(1, 2) // 3

我们还可以用这个特性做到函数柯里化,在复杂的场景中(好像业务开发几乎用不到的样子)使得函数调用更加灵活:

ts
  1. const curry = (func: Function, ...args: any[]) => {
  2. let resArgsCount = func.length - args.length
  3. let tempFunc = func.bind(null, ...args)
  4. const ans = (...childArgs: any[]) => {
  5. resArgsCount -= args.length
  6. return resArgsCount > 0
  7. ? ((tempFunc = tempFunc.bind(null, ...childArgs)), ans)
  8. : tempFunc(...childArgs)
  9. }
  10. return ans
  11. }
  12. const test = (a, b, c) => a + b + c
  13. const testCurry = curry(test, 1)
  14. testCurry(2)
  15. testCurry(3)
  16. // 6

在 ES6 尚未普及的年代,我们并不能直接使用bind这个新特性,这就需要 polyfill,因此产生了很多相关的技巧(现在即使要兼容 IE 也可以直接通过 Bable 兼容),在 JS 中模拟实现bind经典面试题了属于是…

结合文档,这篇博客将在 JS 中实现一下bind的功能。

🍵修改 this 指针和记录入参

众所周知,bind可以修改this的指向,并且记录入参:

ts
  1. function testFunc (a, b) {
  2. return [a + b, this]
  3. }
  4. testFunc(1, 2)
  5. // [3, Window]
  6. testFunc.bind({}, 1)(2)
  7. //  [3, {…}]

下面就在 JS 中手写一下:

ts
  1. Function.prototype.deerBind = function(ctx, ...args) {
  2. ctx = ctx || window
  3. const self = this
  4. return function (...argsNext) {
  5. return self.apply(ctx, [...args, ...argsNext])
  6. }
  7. }
  8. testFunc(1, 2)
  9. // [3, Window]
  10. testFunc.deerBind({}, 1)(2)
  11. //  [3, {…}]

☕作为构造函数

绑定函数自动适用于与 new 运算符一起使用,以用于构造目标函数创建的新实例。当使用绑定函数是用来构造一个值时,提供的this会被忽略。

在 JS 中,当你new一个对象时:

  1. 创建一个新对象;
  2. 构造函数this指向这个新对象;
  3. 执行构造函数中的代码;
  4. 返回新对象。

当一个函数被作为构造函数new的时候,它的this指向该函数的实例。这里我们修改一下deerBind函数的返回:

ts
  1. Function.prototype.deerBind = function(ctx, ...args) {
  2. ctx = ctx || window
  3. const self = this
  4. const funcBound = function (...argsNext) {
  5. return self.apply((this instanceof funcBound ? this : ctx), [...args, ...argsNext])
  6. }
  7. funcBound.prototype = self.prototype
  8. return funcBound
  9. }
  10. function testNew (str) { this.test = 'test ' + str }
  11. new (testNew.bind({}, 'shikinoko nokonoko koshitanntann'))
  12. // testNew {test: 'test shikinoko nokonoko koshitanntann'}
  13. new (testNew.deerBind({}, 'shikinoko nokonoko koshitanntann'))
  14. // testNew {test: 'test shikinoko nokonoko koshitanntann'}

另外,bind返回的函数的实例和原函数是指向同一个原型的,这里也满足了:

ts
  1. const ins1 = new (testNew.deerBind({}, 'test')), ins2 = new testNew('test')
  2. ins1.__proto__
  3. // {constructor: ƒ}
  4. ins1.__proto__ === ins2.__proto__
  5. // true

🧉处理箭头函数的情况

注意到,箭头函数没有实例,也不能newthis来自亲代作用域,用作构造函数会引起错误:

ts
  1. new (() => {})
  2. // Uncaught TypeError: (intermediate value) is not a constructor
  3. new ((() => {}).bind())
  4. // Uncaught TypeError: (intermediate value).bind(...) is not a constructor
  5. const testArrowFunc = (a, b) => {
  6. return [a + b, this]
  7. }
  8. testArrowFunc(1, 2)
  9. // [3, Window]
  10. testArrowFunc.bind({}, 1)(2)
  11. // [3, Window]

再修改一下这里的实现:

ts
  1. Function.prototype.deerBind = function(ctx, ...args) {
  2. ctx = ctx || window
  3. const self = this
  4. let funcBound
  5. if (self.prototype) {
  6. funcBound = function (...argsNext) {
  7. return self.apply((this instanceof funcBound ? this : ctx), [...args, ...argsNext])
  8. }
  9. funcBound.prototype = self.prototype
  10. } else {
  11. funcBound = (...argsNext) => {
  12. return self.apply(ctx, [...args, ...argsNext])
  13. }
  14. }
  15. return funcBound
  16. }
  17. testArrowFunc.deerBind({}, 1)(2)
  18. // [3, Window]
  19. new ((() => {}).deerBind())
  20. // Uncaught TypeError: (intermediate value).deerBind(...) is not a constructor

🍯处理类构造器的情况

你可能会发现,bind可以在类的构造器上面使用,但是我们上面自己写的似乎存在一点小错误:

ts
  1. class base { constructor (a, b) { this.test = a + b } }
  2. new base(1, 2)
  3. // base {test: 3}
  4. new (base.bind({}, 1))(2)
  5. // base {test: 3}
  6. const bind = base.deerBind({}, 1)
  7. new bind(2)
  8. // Uncaught TypeError: Class constructor base cannot be invoked without 'new'

这里通过prototype上面的constructor来检查一个函数是否是构造器:

ts
  1. Function.prototype.deerBind = function(ctx, ...args) {
  2. ctx = ctx || window
  3. const self = this
  4. let funcBound
  5. if (self.prototype) {
  6. funcBound = function (...argsNext) {
  7. return !self.prototype.constructor
  8. ? self.apply((this instanceof funcBound ? this : ctx), [...args, ...argsNext])
  9. : new self(...args, ...argsNext)
  10. }
  11. funcBound.prototype = self.prototype
  12. } else {
  13. funcBound = (...argsNext) => {
  14. return self.apply(ctx, [...args, ...argsNext])
  15. }
  16. }
  17. return funcBound
  18. }
  19. const bind = base.deerBind({}, 1)
  20. new bind(2)
  21. // base {test: 3}

我们实现的deerBind也可以用于构造函数了。

🍹结语

到这里,bind大体就是实现完成了,这里具体涉及了bind函数改变this指针,记录参数以及作为构造函数的功能的实现。去看了一下著名 JS polyfill 库 core-js 的实现,感觉思路大概差不多的样子,它作为 polyfill 也考虑了兼容性的问题,感觉好厉害的样子。

参考:

  1. https://juejin.cn/post/6844903733013250056
  2. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
0 条评论未登录用户
Ctrl or + Enter 评论
🌸 Run