闭包的运用
闭包(Closure)经常会被认为是 JavaScript 的高级机能,但了解闭包是精通语言的必要之事。
思考以下的函数︰
2 | var name = "Mozilla"; |
3 | function displayName() { |
init() 函数建立了称为 name 的局域变量,然后定义了称为 displayName() 的函数。displayName() 是内部的函数 – 他是在 init() 内部定义的,而且只在函数本体内部才可使用。displayName() 没有他自己的局域变量,但会重复使用在外部函数里所宣告的 name 变量。
本例只会做一点事 – 试试执行代码看会发生什么。这是词汇作用域的范例︰在 JavaScript 中,变量的作用域是由他自己在原始码中的位置所定义的,且内部的函数能够存取宣告于外部作用域的变量。
现在思考下例︰
02 | var name = "Mozilla"; |
03 | function displayName() { |
09 | var myFunc = makeFunc(); |
如果你执行这个代码,将会发生和前一个 init() 例子完全相同的效果︰字符串 "Mozilla" 将会被显示在 JavaScript 的警告方框中。其中的不同点 – 以及有趣的一点 – 是内部的 displayName() 函数会在执行之前从外部的函数所返回。
代码的运作看起来也许很不直觉。通常说,在函数内部的局域变量只存在于函数执行的期间。一旦 makeFunc() 执行完毕,预期不再需要 name 变量是很合理的。由于代码仍旧以预期般的运作,很明显情况并不如此。
对于这个难题的解答是 myFunc 已经变成闭包了。闭包是一种特殊的对象,其中结合了两样东西︰函数,和函数所建立的环境。环境由任意的局域变量所组成,这些变量是由在闭包建立的时间点上存在于作用域里的所有变量。既然如此,myFunc 就是结合了 displayName 函数和闭包建立之后就存在的 "Mozilla" 字符串这两者的闭包。
这里还有更为有趣的范例 – makeAdder 函数︰
01 | function makeAdder(x) { |
07 | var add5 = makeAdder(5); |
08 | var add10 = makeAdder(10); |
在这个范例中,我们已经定义了函数 makeAdder(x),可接受单一参数 x,并返回新的函数。返回的函数会接受单一参数 y,并返回 x 和 y 的合。
就本质而言,makeAdder 是函数的制造机 – 他会建立可以把指定的值和他们的参数相加的函数。在上例中,我们使用了我们的函数制造机来建立两个新的函数 – 一个给他自己的参数加上 5,另一个则加上 10。
add5 和 add10 两个都是闭包。他们共享相同的函数本体的定义,但保存了不同的环境变量。在 add5 的环境中,x是 5。至于互有关连的 add10,x 是 10。
实用的闭包
该是抛开理论的时候了 – 但是闭包真的有用吗?让我们思考闭包潜在的用处。闭包让你把一些数据(环境)和可操作数据的函数联系在一起。这一点明显和面向对象程序设式并行不悖,对象可让我们把一些数据(对象的属性)和一个以上的方法联系在一起。
因此,如果通常你会在某个地方使用附有单一方法的对象,你可以在这些地方使用闭包。
视情况你可能会想这样做,这在 Web 上尤其常见。我们写在 Web 上的 JavaScript 代码多半是以事件为基础 – 我们定义了一些行为,然后把这些行为和由使用者所触发的事件(如 click 或 keypress)连系在一起。我们的代码通常被连系为 Callback︰在响应事件时,所执行的单一函数。
这里有个实际的例子︰假如我们希望在页面上加入可以调整页面文字的按钮。以像素为单位,指定 body 元素的 font-size 是一个方法,然后以 em 为单位,设定在页面上(如页眉)的其他元素的大小︰
body {
font-family: Helvetica, Aria, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}[/code]
我们的交互式文字大小按钮可以改变 body 元素的 font-size 属性,拜相对单位之赐,接着对其他的元素做调整。
JavaScript 代码︰
1 | function makeSizer(size) { |
3 | document.body.style.fontSize = size + 'px'; |
7 | var size12 = makeSizer(12); |
8 | var size14 = makeSizer(14); |
9 | var size16 = makeSizer(16); |
现在 size12、size14 和 size16 这些函数可分别调整 body 文字的大小为 12、14 和 16 像素。我们可以把代码和按钮(本例中使用的是连结)连系在一起,如下︰
1 | function setupButtons() { |
2 | document.getElementById('size-12').onclick = size12; |
3 | document.getElementById('size-14').onclick = size14; |
4 | document.getElementById('size-16').onclick = size16; |
1 | <a href="#" id="size-12">12</a> |
2 | <a href="#" id="size-14">14</a> |
3 | <a href="#" id="size-16">16</a> |
使用闭包模拟私有的方法
像 Java 这类语言可以把方法宣告为私有的,意思是这些方法只能被同一类别的其他方法所呼叫。
JavaScript 并不提供做这些事的原生方式,但可以使用闭包来模拟私有方法。私有方法不只是对限制代码的存取这方面有用︰同时也是管理你的全局命名空间的强大方式,把非必要的方法堆在公开的界面里。
这里是如何使用闭包来定义可以存取私有函数和变量的公开函数︰
01 | var Counter = (function() { |
02 | var privateCounter = 0; |
03 | function changeBy(val) { |
04 | privateCounter += val; |
07 | increment: function() { |
10 | decrement: function() { |
14 | return privateCounter; |
19 | alert(Counter.value()); |
22 | alert(Counter.value()); |
24 | alert(Counter.value()); |
在此完成了很多事。在上一个范例中,每一个闭包都有他自己的环境;此处我们建立了由三个函数所共享的单一环境︰Counter.increment、Counter.decrement、Counter.value。
共享的环境是建立在无名函数的本体内,无名函数一经定义就会开始执行。环境内含两个私有项︰称作privateCounter 的变量,以及称作 changeBy 的函数。这两个私有项都不能在无名函数外部被直接存取。相对的,必须由三个公开的函数来存取这些私有项,这三个函数是从无名函数的封装器所返回的。
这三个公开的函数共享闭包的同一个环境。感谢 JavaScript 的词汇作用域,这三个函数都能存取 privateCounter变量和 changeBy 函数。
按照这个方式来运用闭包,可以得到通常是附加在面向对象程序设计里的数据隐藏和封装的好处。
在循环中建立闭包︰常见的错误
在 JavaScript 1.7 引入 let 关键词以前,闭包常见的问题出现在当闭包是在循环内部建立的时候。思考以下的例子︰
1 | <p id="help">这里会显示有用的提示</p> |
2 | <p>E-mail: <input type="text" id="email" name="email"></p> |
3 | <p>姓名: <input type="text" id="name" name="name"></p> |
4 | <p>年龄: <input type="text" id="age" name="age"></p> |
01 | function showHelp(help) { |
02 | document.getElementById('help').innerHTML = help; |
07 | {'id': 'email', 'help': '你的 e-mail 地址'}, |
08 | {'id': 'name', 'help': '你的完整姓名'}, |
09 | {'id': 'age', 'help': '你的年龄(你必须大于 16 岁)'} |
12 | for (var i = 0; i < helpText.length; i++) { |
13 | var item = helpText[i]; |
14 | document.getElementById(item.id).onfocus = function() { |
20 | <code>helpText</code> 数组定义了三个有用的提示,每一个都和文件中的输入字段的 ID 连系在一起。循环会在这些定义里巡回一圈,给每一个显示相关连的说明的方法使用 onfocus 事件。 |
22 | 如果你试着执行这个代码,你会发现他并不如预期般的运作。不管你把焦点放在哪一个字段上,都会显示关于你的年龄的讯息。 |
24 | 这其中的原因是代入给 onfocus 的函数是闭包;这些闭包是由函数的定义和从 <code>setupHelp</code> 函数的作用域所捕捉到的环境所组成的。这三个闭包已经建立了,但每一个都共享同一个环境。每次执行 onfocus 的 Callback 的时候,循环执行的是他自己的闭包,以及指向 <code>helpText</code> 列表中的最后一项的变量 item(由三个闭包所共享)。 |
26 | 本例的解决方法是使用更多的闭包︰特别是使用稍早已描述过的函数制造机︰ |
27 | <pre class="brush: jscript;">function showHelp(help) { |
28 | document.getElementById('help').innerHTML = help; |
31 | function makeHelpCallback(help) { |
39 | {'id': 'email', 'help': '你的 e-mail 地址'}, |
40 | {'id': 'name', 'help': '你的完整姓名'}, |
41 | {'id': 'age', 'help': '你的年龄(你必须大于 16 岁)'} |
44 | for (var i = 0; i < helpText.length; i++) { |
45 | var item = helpText[i]; |
46 | document.getElementById(item.id).onfocus = makeHelpCallback(item.help); |
49 | <p>这次就如预期般运作。而不是所有的 Callback 都共享单一的环境,<code>makeHelpCallback</code> 给每一个 <code>help</code> 建立新的环境,此处的 <code>help</code> 参照了相对应的 <code>helpText</code> 数组的字符串。</p> |
50 | <p>如果你使用 JavaScript 1.7 以上的版本,你可以使用 <code>let</code> 关键词建立具有区块层级作用域的变量来解决这个问题︰</p> |
51 | <pre class="brush: jscript;">for (var i = 0; i < helpText.length; i++) { |
52 | let item = helpText[i]; |
53 | document.getElementById(item.id).onfocus = function() { |
57 | <p><code>let</code> 关键词使 item 变量改用具有区块层级的作用域来建立,导致 for 循环每一次反复都能建立新的参考。意思是每一个闭包都会捕捉到个别的变量,解决因为共享同一环境所引起的问题。</p> |
效能的考虑
如果并没有特定的任务需要用到闭包,且闭包对 Script 的效能会有负面的影响,因此在其他函数的内部里建立不必要的函数是很不智的。
例如,当建立新的对象或类别时,通常应该要把方法和对象的原型连系在一起,而不是在对象的建构子中定义。这其中的理由是,每当呼叫建构子的时候,就要把方法代入(也就是每一个对象正在建立的时候)。
思考以下不切实际的例子︰
01 | function MyObject(name, message) { |
02 | this.name = String(name); |
03 | this.message = String(message); |
04 | this.getName = function() { |
08 | this.getMessage = function() { |
上面的代码并未从闭包的行为中取得好处,应该改用重整过的形式︰
01 | function MyObject(name, message) { |
02 | this.name = String(name); |
03 | this.message = String(message); |
09 | getMessage: function() { |
或者是︰
01 | function MyObject(name, message) { |
02 | this.name = String(name); |
03 | this.message = String(message); |
05 | MyObject.prototype.getName = function() { |
08 | MyObject.prototype.getMessage = function() { |