今天有人问了下有关javascript的闭包的问题,自己也没有看相关的文档,只是模糊的回答了下。回答完之后感觉那样对自己不好,一定要弄清javascript的闭包。正好在火狐开发者社区看到一篇有关闭包的文章https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
一、什么是闭包?
闭包是指函数有自主独立的变量,也就是说定义闭包中的函数可以记忆它创建时候的“环境”。
二、 语法的作用域
看看下面的函数
<script type="text/javascript"> function init() { var name ="dongtian"; function displayName(){ alert(name); } } init(); </script>
在函数init()里面定义一个变量名称为name的临时变量,然后定义一个名称为displayName 的内部函数。此内部函数仅仅只有在本init方法内可用,其他都没有办法调用,你看displayName函数内并没有自己的局部变量,然而它可以访问到外包函数的变量-可以使用父函数使用的变量。
我们更改上面的代码
<script type="text/javascript"> function init() { var name ="dongtian"; function displayName(){ alert(name); } //仅仅在init方法体内调用 displayName(); } init(); </script>
这是作用域的一个例子,在javascript 中,变量的作用域是嵌套函数可以访问外部的变量。
三、闭包
我们先看下面的一个例子
<script type="text/javascript"> function init() { var name ="dongtian"; function displayName(){ alert(name); } //仅仅在init方法体内调用 return displayName; } var display = new init(); display(); </script>
运行代码与上面的结果一样,这段代码看起来比较别扭,但是运行正常,我们都知道一个函数的作用域仅仅在运行期可用,当init方法运行完之后我们会认为name变量已经失效不可用了,但是虽然运行没有问题,实际上是对应name变量不是那样不可用已经失效了。
因为这个init已经变成闭包了,是闭包的一个特殊对象,它有两部分构成:函数以及创建函数的环境,环境由闭包创建时候在作用域的任何局部变量组成,-上面我们可以说 init 是个闭包,由displayName函数和闭包创建时候的name="dongtian"组成。
下面看一个加法器的一个闭包的写法:
<script type="text/javascript"> function subAdd(x) { return function(y) { return x +y; } } var add1 = subAdd(2); var add2 = subAdd(5); alert(add1(2)); alert(add2(3)); </script>
这个例子我们定义了subAdd()函数,带有一个参数x,并且返回一个新的函数,返回的函数中带有一个参数y并返回 x和y的和。从本质上讲 subAdd()函数是个工厂函数,创建它并且要指定值并且包含一个求和的函数。在上面的例子中我们通过此函数工厂创建了两个函数一个将参数2求和一个将参数5求和,从这我们可以看出此两个函数都是闭包,它们共享函数的定义并且他们处在不同的环境中。
运行结果是 弹出 4 和 8
四、为何使用闭包
理论就是这些了 — 可是闭包确实有用吗?让我们看看闭包的实践意义。闭包允许将函数与其所操作的某些数据(环境)关连起来。这显然类似于面向对象编程。在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。
因而,一般说来,可以使用只有一个方法的对象的地方,都可以使用闭包。
在 Web 中,您可能想这样做的情形非常普遍。大部分我们所写的 Web JavaScript 代码都是事件驱动的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常添加为回调:响应事件而执行的函数。
以下是一个实际的示例:假设我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定body
元素的 font-size
,然后通过相对的 em 单位设置页面中其它元素(例如页眉)的字号:
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; }
我们的交互式的文本尺寸按钮可以修改 body
元素的 font-size
属性,而由于我们使用相对的单位,页面中的其它元素也会相应地调整。
以下是 JavaScript:
function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);
size12
,size14
和 size16
为将 body
文本相应地调整为 12,14,16 像素的函数。我们可以将它们分别添加到按钮上(这里是链接)。如下所示:
document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
五、用闭包模拟私有方法
诸如 Java 在内的一些语言支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。
对此,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 */
这里有很多细节。在以往的示例中,每个闭包都有它自己的环境;而这次我们只创建了一个环境,为三个函数所共享:Counter.increment,
Counter.decrement
和 Counter.value
。
该共享环境创建于一个匿名函数体内,该函数一经定义立刻执行。环境中包含两个私有项:名为privateCounter
的变量和名为 changeBy
的函数。 这两项都无法在匿名函数外部直接访问。必须通过匿名包装器返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法范围的作用域,它们都可以访问privateCounter
变量和 changeBy
函数。
您应该注意到了,我们定义了一个匿名函数用于创建计数器,然后直接调用该函数,并将返回值赋给Counter
变量。也可以将这个函数保存到另一个变量中,以便创建多个计数器。
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 */
请注意两个计数器是如何维护它们各自的独立性的。每次调用 makeCounter()
函数期间,其环境是不同的。每次调用中, privateCounter 中含有不同的实例。
这种形式的闭包提供了许多通常由面向对象编程U所享有的益处,尤其是数据隐藏和封装。
六、在循环中创建闭包---- 一种常见的错误
在 JavaScript 1.7 引入 let
关键字 之前,闭包的一个常见的问题发生于在循环中创建闭包。参考下面的示例:
<p id="help">Helpful notes will appear here</p> <p>E-mail: <input type="text" id="email" name="email"></p> <p>Name: <input type="text" id="name" name="name"></p> <p>Age: <input type="text" id="age" name="age"></p>
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 < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
数组 helpText
中定义了三个有用的提示信息,每一个都关联于对应的文档中的输入域的 ID。通过循环这三项定义,依次为每一个输入域添加了一个 onfocus
事件处理函数,以便显示帮助信息。
运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个输入域上,显示的都是关于年龄的消息。
该问题的原因在于赋给 onfocus
是闭包(setupHelp)中的匿名函数而不是闭包对象;在闭包(setupHelp)中一共创建了三个匿名函数,但是它们都共享同一个环境(item)。在 onfocus
的回调被执行时,循环早已经完成,且此时 item
变量(由所有三个闭包所共享)已经指向了 helpText
列表中的最后一项。
解决这个问题的一种方案是使onfocus指向一个新的闭包对象。
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 < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); } } setupHelp();
这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, makeHelpCallback
函数为每一个回调创建一个新的环境。在这些环境中,help
指向 helpText
数组中对应的字符串。
七、性能
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次(也就是说,为每一个对象的创建)。
考虑以下虽然不切实际但却说明问题的示例:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
上面的代码并未利用到闭包的益处,因此,应该修改为如下常规形式:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype = { getName: function() { return this.name; }, getMessage: function() { return this.message; } };
或者改成
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; };
在前面的两个示例中,继承的原型可以为所有对象共享,且不必在每一次创建对象时定义方法。
八、总结
记住闭包的作用一般有两种,所有局部变量永驻内存和外包无法访问到局部变量。