313 lines
17 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<article id="wikiArticle">
<p></p><p></p>
<p class="summary">闭包是函数和声明该函数的词法环境的组合。</p>
<h3 id="词法作用域"><strong>词法作用域</strong></h3>
<p>考虑如下情况:</p>
<div style="width: auto; overflow: hidden;">
<pre><code class="language-javascript">function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
</code></pre>
</div>
<p><code>init()</code> 创建了一个局部变量 <code>name</code> 和一个名为 <code>displayName()</code> 的函数。<code>displayName()</code> 是定义在 <code>init()</code> 里的内部函数,仅在该函数体内可用。<code>displayName()</code> 内没有自己的局部变量,然而它可以访问到外部函数的变量,所以 <code>displayName()</code> 可以使用父函数 <code>init()</code> 中声明的变量 <code>name</code> 。但是,如果有同名变量 <code>name</code> 在 <code>displayName()</code> 中被定义,则会使用 <code>displayName()</code> 中定义的 <code>name</code></p>
<p></p><p><iframe frameborder="0" height="200" src="https://jsfiddle.net/xAFs9/3/embedded/js,result/" width="100%"></iframe></p><p></p>
<p><a class="external" href="http://jsfiddle.net/xAFs9/3/" rel="noopener" title="http://jsfiddle.net/xAFs9/">运行</a>代码可以发现 <code>displayName()</code> 内的 <code>alert()</code> 语句成功的显示了在其父函数中声明的 <code>name</code> 变量的值。这个<em>词法作用域</em>的例子介绍了引擎是如何解析函数嵌套中的变量的。词法作用域中使用的域,是变量在代码中声明的位置所决定的。嵌套的函数可以访问在其外部声明的变量。</p>
<h3 id="闭包"><strong>闭包</strong></h3>
<p>现在来考虑如下例子 </p>
<pre><code class="language-javascript">function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();</code></pre>
<p>运行这段代码和之前的 <code>init()</code> 示例的效果完全一样。其中的不同 — 也是有意思的地方 — 在于内部函数 <code>displayName()</code> 在执行前,被外部函数返回。</p>
<p>第一眼看上去,也许不能直观的看出这段代码能够正常运行。在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 <code>makeFunc()</code> 执行完毕,我们会认为 <code>name</code> 变量将不能被访问。然而,因为代码运行得没问题,所以很显然在 JavaScript 中并不是这样的。</p>
<p>这个谜题的答案是JavaScript中的函数会形成<em>闭包</em>。 闭包是由函数以及创建该函数的词法环境组合而成。<u><strong>这个环境包含了这个闭包创建时所能访问的所有局部变量</strong></u>。在我们的例子中,<code>myFunc</code> 是执行 <code>makeFunc</code> 时创建的 <code>displayName</code> 函数实例的引用,而 <code>displayName</code> 实例仍可访问其词法作用域中的变量,即可以访问到 <code>name</code> 。由此,当 <code>myFunc</code> 被调用时,<code>name</code> 仍可被访问,其值 <code>Mozilla</code> 就被传递到<code>alert</code>中。</p>
<p>下面是一个更有意思的示例 — <code>makeAdder</code> 函数:</p>
<pre><code class="language-javascript">function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
</code></pre>
<p>在这个示例中,我们定义了 <code>makeAdder(x)</code> 函数,它接受一个参数 <code>x</code> ,并返回一个新的函数。返回的函数接受一个参数 <code>y</code>,并返回<code>x+y</code>的值。</p>
<p>从本质上讲,<code>makeAdder</code> 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。</p>
<p><code>add5</code><code>add10</code> 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 <code>add5</code> 的环境中,<code>x</code> 为 5。而在 <code>add10</code> 中,<code>x</code> 则为 10。</p>
<h3 id="Practical_closures" name="Practical_closures">实用的闭包</h3>
<p>闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。</p>
<p> </p>
<p> </p>
<p>因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。</p>
<p>在 Web 中你想要这样做的情况特别常见。大部分我们所写的 JavaScript 代码都是基于事件的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。</p>
<p>假如,我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定 <code>body</code> 元素的 <code>font-size</code>,然后通过相对的 <code>em</code> 单位设置页面中其它元素(例如<code>header</code>)的字号:</p>
<pre><code class="language-css">body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
</code></pre>
<p>我们的文本尺寸调整按钮可以修改 <code>body</code> 元素的 <code>font-size</code> 属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。</p>
<p>以下是 JavaScript</p>
<pre><code class="language-javascript">function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
</code></pre>
<p><code>size12</code><code>size14</code><code>size16</code> 三个函数将分别把 <code>body</code> 文本调整为 121416 像素。我们可以将它们分别添加到按钮的点击事件上。如下所示:</p>
<pre><code class="language-javascript">document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
</code></pre>
<pre><code class="language-html">&lt;a href="#" id="size-12"&gt;12&lt;/a&gt;
&lt;a href="#" id="size-14"&gt;14&lt;/a&gt;
&lt;a href="#" id="size-16"&gt;16&lt;/a&gt;
</code></pre>
<p></p><p><iframe frameborder="0" height="200" src="https://jsfiddle.net/cubr4hs0/embedded/" width="100%"></iframe></p><p></p>
<h3 id="Emulating_private_methods_with_closures" name="Emulating_private_methods_with_closures">用闭包模拟私有方法</h3>
<p>编程语言中,比如 Java是支持将方法声明为私有的即它们只能被同一个类中的其它方法所调用。</p>
<p> </p>
<p> </p>
<p>而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。</p>
<p>下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 <a class="external" href="http://www.google.com/search?q=javascript+module+pattern" rel="noopener" title="http://www.google.com/search?q=javascript+module+pattern">模块模式module pattern</a></p>
<pre><code class="language-javascript">var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
</code></pre>
<p>在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:<code>Counter.increment</code><code>Counter.decrement</code><code>Counter.value</code></p>
<p>该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 <code>privateCounter</code> 的变量和名为 <code>changeBy</code> 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。</p>
<p>这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 <code>privateCounter</code> 变量和 <code>changeBy</code> 函数。</p>
<div class="note">
<p>你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量<code>counter</code>。我们可以把这个函数储存在另外一个变量<code>makeCounter</code>中,并用他来创建多个计数器。</p>
</div>
<pre><code class="language-javascript">var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
</code></pre>
<p>请注意两个计数器 <code>counter1</code> 和 <code>counter2</code> 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 <code>privateCounter</code> 。</p>
<p>每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。</p>
<div class="note">
<p>以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。</p>
</div>
<h3 id="Creating_closures_in_loops_A_common_mistake" name="Creating_closures_in_loops_A_common_mistake">在循环中创建闭包:一个常见错误</h3>
<p>在 ECMAScript 2015 引入 <a href="/en-US/docs/JavaScript/Reference/Statements/let" title="let"><code>let</code> 关键字</a> 之前,在循环中有一个常见的闭包创建问题。参考下面的示例:</p>
<pre><code class="language-html">&lt;p id="help"&gt;Helpful notes will appear here&lt;/p&gt;
&lt;p&gt;E-mail: &lt;input type="text" id="email" name="email"&gt;&lt;/p&gt;
&lt;p&gt;Name: &lt;input type="text" id="name" name="name"&gt;&lt;/p&gt;
&lt;p&gt;Age: &lt;input type="text" id="age" name="age"&gt;&lt;/p&gt;
</code></pre>
<pre><code class="language-javascript">function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i &lt; helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
</code></pre>
<p></p><p><iframe frameborder="0" height="200" src="https://jsfiddle.net/51brm6ps/embedded/" width="100%"></iframe></p><p></p>
<p>数组 <code>helpText</code> 中定义了三个有用的提示信息,每一个都关联于对应的文档中的<code>input</code> 的 ID。通过循环这三项定义依次为相应<code>input</code>添加了一个 <code>onfocus</code>  事件处理函数,以便显示帮助信息。</p>
<p>运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个<code>input</code>上,显示的都是关于年龄的信息。</p>
<p>原因是赋值给 <code>onfocus</code> 的是闭包。这些闭包是由他们的函数定义和在 <code>setupHelp</code> 作用域中捕获的环境所组成的。这三个闭包在循环中被创建但他们共享了同一个词法作用域在这个作用域中存在一个变量item。当onfocus的回调执行时<code>item.help</code>的值被决定。由于循环在事件触发之前早已执行完毕,变量对象<code>item</code>(被三个闭包所共享)已经指向了<code>helpText</code>的最后一项。</p>
<p>解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:</p>
<pre><code class="language-javascript">function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i &lt; helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();
</code></pre>
<p></p><p><iframe frameborder="0" height="300" src="https://jsfiddle.net/v7gjv/12185/embedded/" width="100%"></iframe></p><p></p>
<p>这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, <code>makeHelpCallback</code> 函数为每一个回调创建一个新的词法环境。在这些环境中,<code>help</code> 指向 <code>helpText</code> 数组中对应的字符串。</p>
<p>另一种方法使用了匿名闭包:</p>
<pre><code class="language-javascript">function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i &lt; helpText.length; i++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // 马上把当前循环项的item与事件回调相关联起来
}
}
setupHelp();</code></pre>
<p>避免使用过多的闭包可以用let关键词</p>
<pre><code class="language-javascript">function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i &lt; helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();</code></pre>
<p>这个例子使用<code>let</code>而不是<code>var</code>,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。</p>
<h3 id="Performance_considerations" name="Performance_considerations">性能考量</h3>
<p>如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。</p>
<p>例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。</p>
<p>考虑以下示例:</p>
<pre><code class="language-javascript">function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
</code></pre>
<p>在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:</p>
<pre><code class="language-javascript">function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
</code></pre>
<p>但我们不建议重新定义原型。可改成如下例子:</p>
<pre><code class="language-javascript">function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
</code></pre>
<p>在前面的两个示例中,继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法。参见 <a href="/zh-CN/docs/JavaScript/Guide/Details_of_the_Object_Model" title="en-US/docs/JavaScript/Guide/Details of the Object Model">对象模型的细节</a> 一章可以了解更为详细的信息。</p>
</article>