今天有人问了下有关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; };
在前面的两个示例中,继承的原型可以为所有对象共享,且不必在每一次创建对象时定义方法。
八、总结
记住闭包的作用一般有两种,所有局部变量永驻内存和外包无法访问到局部变量。
相关推荐
总之,JavaScript内核系列的资料全面覆盖了JavaScript语言的各个方面,无论是初学者还是有经验的开发者,都能从中获益,提升自己的编程能力。深入学习并实践这些知识点,将使你在JavaScript的世界里游刃有余。
本书是关于JavaScript闭包的深度解析与应用实践的权威之作,深入探讨了闭包在JavaScript中的各种应用场景和技术细节。 **作者简介:** Michael Bolin是一位资深的软件工程师,拥有多年的编程经验,专注于...
"JavaScript手册全套必备"集合了从基础到高级的各种资源,旨在为开发者提供全面的学习材料。以下是一些重要的JavaScript知识点,结合描述中的内容进行展开: 1. **JavaScript 入门**: JavaScript 的基础学习包括...
3. **作用域和闭包**:解析了JavaScript中的变量作用域规则以及闭包的概念,这是解决许多复杂问题的基础。 4. **异步编程**:详述了事件循环、回调函数、Promise、async/await等异步处理方式,帮助开发者应对...
这份名为“JavaScript必看全面总结.zip”的压缩包文件包含了一份深入的JavaScript学习笔记,旨在帮助开发者全面掌握这一动态类型的脚本语言。以下是根据标题和描述提炼出的JavaScript关键知识点: 1. **基础语法**...
JavaScript全面分析——中文版是为想要快速理解和掌握JavaScript编程语言的学者精心编写的教程。JavaScript是一种广泛应用于Web开发的脚本语言,它在浏览器端运行,为网页添加交互性,使得用户界面更加生动活泼。本...
总的来说,《JavaScript语言精粹》涵盖了JavaScript的各个方面,从基础到高级,从语法到实战,全面解析了这个强大且广泛应用的语言。无论是新手还是老手,都能从中受益匪浅,提升自己的JavaScript编程技能。
根据提供的标题“JAVASCRIPT完全解析”及描述“JAVASCRIPT完全解析,不错的PDF”,我们可以推断出这份文档是一本关于JavaScript编程语言的详细教程。JavaScript是一种广泛使用的脚本语言,在前端开发、后端开发乃至...
深入理解JavaScript系列(3):全面解析Module模式 深入理解JavaScript系列(4):立即调用的函数表达式 深入理解JavaScript系列(5):强大的原型和原型链 深入理解JavaScript系列(6):S O L I D五大原则之...
深入理解JavaScript系列(3):全面解析Module模式 深入理解JavaScript系列(4):立即调用的函数表达式 深入理解JavaScript系列(5):强大的原型和原型链 深入理解JavaScript系列(6):S.O.L.I.D五大原则之...
Rhino的核心特性在于其对ECMAScript规范的全面支持,这使得JavaScript代码能够在Java环境中无缝执行。1.7.2版本发布于2009年,虽然相对较旧,但仍然包含了许多关键的JavaScript语言特性,如函数、对象、数组、正则...
2. **作用域和闭包**:讲解了如何管理变量的作用域,以及闭包如何为JavaScript提供强大的特性,例如模块化和记忆化。 3. **原型和继承**:JavaScript的面向对象编程特性,如原型链、构造函数和实例化。 4. **异步...
《JavaScript权威指南》是JavaScript编程领域的一本经典之作,由David Flanagan撰写,第六版的中文扫描版在压缩包中提供了全面的JavaScript语言知识。这本书深入浅出地讲解了JavaScript的核心概念,包括语法、类型、...
JavaScript全教程是一个旨在引导初学者全面理解JavaScript编程语言的学习资源。JavaScript,通常简称为JS,是一种广泛用于网页和网络应用的脚本语言。它在浏览器环境中运行,为动态交互、网页动画、数据处理以及Web...
JavaScript Module模式是编程实践中一种非常重要的设计模式,它主要用于实现JavaScript代码的模块化和封装,以提高代码的可维护性和可重用性。模块模式的关键在于利用匿名函数和闭包来创建私有作用域,从而保护内部...
《JAVASCRIPT语言精髓与编程实践》是周爱民撰写的一本深入解析JavaScript编程的著作,这本书旨在帮助读者理解JavaScript的核心概念,并将其应用于实际的编程实践中。JavaScript,作为全球最广泛使用的脚本语言,是...
"javascript经典源代码最全"这个压缩包文件显然包含了大量JavaScript的源代码示例,旨在帮助开发者理解和学习JavaScript的核心概念、语法以及常见应用场景。下面我们将深入探讨JavaScript的关键知识点,并结合...
《JavaScript权威指南》是一本备受推崇的编程教材,专注于JavaScript语言的深度解析。这本书由David Flanagan撰写,为读者提供了全面而深入的JavaScript知识。Epub格式的电子书便于在各种设备上阅读,而源码文件则...