原文:Understanding JavaScript Bind ()
前言
稍微写多点儿 JS 的人应该都见过var self = this
这种写法,它是为了解决在不同函数调用时,this
所指向的上下文参数变化的问题,你可以通过MDN this这篇文章先了解一下 JS 中的this
。下文提供了将函数与其想要的this
绑定的方法,以下是翻译正文。
理解 JS 中的 Bind()
当你初学 JavaScript 时你可能并不关心函数绑定的问题,但是当你需要一个在其他函数中保持this
内容的解决方案时,你可能并没有意识到你真正需要的就是Function.prototype.bind()
函数。
第一次遇到这个问题时,当你切换上下文时可能会将this
赋值给一个可以引用的变量。大多数人会选择self
,_this
或context
作为变量名,这种方法是可用的,并不会出错,但是还有一种更好、更优雅的方式。
Jake Archibald发推讨论过捕获this
的问题:
Ohhhh I would do anything for scope, but I won’t do that = this — Jake Archibald (@jaffathecake) February 20, 2013
当Sindre Sorhus讨论这个问题时,答案已经很显然了:
@benhowdle $this for jQuery, for plain JS i don’t, use .bind() — Sindre Sorhus (@sindresorhus) February 22, 2013
但是我却忽略了好几个月。
我们想要解决什么问题?
在下面的代码中,将上下文对象赋值给一个变量是情有可原的:
1 | var myObj = { |
如果我们在上面的代码中直接调用this.specialFunction()
,那么就会看到如下的错误信息:
1 | Uncaught TypeError: Object [object global] has no method 'specialFunction' |
我们需要持有myObj
对象的上下文用于回调函数的调用,调用that.specialFunction()
函数使我们持有上下文并且正确的执行我们的函数。但是使用Function.prototype.bind()
是更优雅的方法。
我们写个例子:
1 | render: function () { |
上例中做了什么?
.bing()
只是创建了一个新的函数,当它被调用时会将this
关键字设置为之前提供的值。这样我们就可以向.bind()
函数传入期望的上下文参数即this
(在这里就是myObj
),然后当回调函数执行时,this
就引用了myObj
对象。
如果你想看看Function.prototype.bind()
这个函数内部是如何运行的,可以看下面这个简单的例子:
1 | Function.prototype.bind = function (scope) { |
下面是个简单的用例:
1 | var foo = { |
我们创建了一个新的函数,其执行时会将this
设置为foo
对象,而不是像例子中直接调用bar()
时,this
默认指向的全局对象。
浏览器支持
浏览器 | 支持的版本 |
---|---|
Chrome | 7 |
Firefox(Gecko) | 4.2(2) |
Internet Explore0 | 9 |
Opera | 11.60 |
Safari | 5.1.4 |
如上所示,在 Internet Explorer 8 及以下版本中并不支持Function.prototype.bind()
函数,所以你需要一个备用方案。
幸好,MDN提供了一个可靠的备选方案,用于没有在本地实现.bind()
方法的浏览器:
1 | if (!Function.prototype.bind) { |
用法范例
我发现当学习东西时,不仅要透彻的学习它的概念,还要将它用在实践中。幸好,下面的几个例子可以用于你的代码中或者解决你的问题。
点击事件处理
通常用于记录点击事件(或者点击后执行一个动作),这时就需要将信息保存在一个对象中,如下:
1 | var logger = { |
我们可能会像下面这样添加点击事件处理,在其中调用logger
对象的updateCount()
方法:
1 | document.querySelector('button').addEventListener('click', function(){ |
但是为使得updateCount()
函数中的this
关键字表示正确的值,我们需要创建一个并不必须的匿名函数。
可以像下面这样优化:
1 | document.querySelector('button').addEventListener('click', logger.updateCount.bind(logger)); |
我们可以使用方便的.bind()
函数来创建一个新的函数,然后将作用域设置的绑定到logger
对象。
SetTimeout
如果你尝试过模板引擎(例如Handlebars)或者某种 MV* 框架(如 Backbone.js),你可能会碰到这样的问题:当你渲染模板时,在调用了渲染方法后,想要立即获取新的 DOM 节点可能就会出错。
假设我们再初始化一个 jQuery 插件:
1 | var myView = { |
你可能会发现这段代码可以正常运行,但并不总是这样。问题就在这里,它产生了一种竞争的情况:有时候渲染先执行完成,有时候插件的初始化先执行完成。
有些人可能不知道,我们可以通过使用setTimeout()
函数来解决问题。
像下面这样简单的修改下代码,使得我们可以在 DOM 节点展示完成后立马安全的初始化我们的 jQuery 插件:
1 | // |
然而,我们会看到找不到.afterRender()
函数的报错信息。
是时候祭出我们的.bind()
方法了:
1 | // |
现在,我们的afterRender()
函数就会执行在正确的上下文环境中了。
整理通过 querySelectorAll 绑定的事件
自从添加了诸如querySelector
,querySelectorAll
和classList
等有用的方法后,DOM API 提升了很多。
然而,到目前为止并没有一个原生的方法来对一个NodeList
中的所有节点添加事件,因此我们还需要使用Array.prototype
中的forEach
方法来循环添加:
1 | Array.prototype.forEach.call(document.querySelectorAll('.klasses'), function(el){ |
我们可以通过使用.bind()
方法来优化一下:
1 | var unboundForEach = Array.prototype.forEach, |
现在我们有一个整齐的方法来循环我们的 DOM 节点了。
结论
如你所见,JS 的bind()
函数可以巧妙的用于各种用途或者代码的整理。期望你能在需要时将.bind()
添加进你的代码中来驾驭this
值转换的能力。