日期: 2020-06-22 17:52:20
本文实例讲述了JavaScript闭包原理与用法。分享给大家供大家参考,具体如下:
闭包(Closure)
闭包是一个函数和词法环境的组合,函数声明在这个词法环境中。
看下面的一个例子:
function init() { var name = 'GaoPian'; // name是局部变量 function displayName() { //displayName();是内部函数,一个闭包 alert(name); // 使用外部函数声明的变量 } displayName(); } init();
init()创建了一个局部变量name和一个函数displayName()。
函数displayName()是一个已经定义在init()内部的函数,并且只能在函数init()里面才能访问得到。
函数displayName()没有自己的局部变量,但由于内部函数可以访问外部函数变量,displayName()可以访问到声明在外部函数init()的变量name,如果局部变量还存在的话,displayName()也可以访问他们。
看下面一个例子
function makeFunc() { debugger var name = 'GaoPian'; function displayName() { alert(name); } return displayName; } var myFunc = makeFunc(); myFunc();
运行这段代码和之前init()的方法的效果是一样。
经过debugger一遍之后发现:
二者不同之处是,displayName()在执行之前,这个内部方法是从外部方法返回来的。
首先,代码还是会正确运行,在一些编程语言当中,一个函数内的局部变量只存在于该函数的执行期间,随后会被销毁,一旦makeFunc()函数执行完毕的话,变量名就不能够被获取,但是,由于代码仍然正常执行,这显然在JS里是不会这样的。这是因为函数在JS里是以闭包的形式出现的。
闭包是一个函数和词法作环境的组合,词法环境是函数被声明的那个作用域,这个执行环境包括了创建闭包时同一创建的任意变量,即创建的这个函数和这些变量处于同一个作用域当中。在这个例子当中,myFunc()是displayName()的函数实例,makeFunc创建的时候,displayName随之也创建了。displayName的实例可以获得词法作用域的引用,在这个词法作用域当中,存在变量name,对于这一点,当myFunc调用的话,变量name,仍然可以被调用,因此,变量'GaoPian'传递给了alert函数。
这里还有一个例子
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
在这个例子当中,我们定义了一个函数makeAdder(x),传递一个参数x,并且返回一个函数,这个返回函数接收一个参数y,并返回x和y的和。
实际上,makeAdder是一个工厂模式:它创建了一个函数,这个函数可以计算特定值的和。在上面这个例子当中,我们使用工厂模式来创建新的函数, 一个与5进行加法运算——add5,一个与10进行加法运算——add10。add5和add10都是闭包,他们共享相同的函数定义,但却存储着不同的词法环境,在add5的词法环境当中,x为5;在add10的词法环境当中,x变成了10。
闭包是很有用的,因为他让我们把一些数据(词法环境)和一些能够获取这些数据的函数联系起来,这有点和面向对象编程类似,在面向对象编程当中,对象让我们可以把一些数据(对象的属性)和一个或多个方法联系起来。
因此,你能够像对象的方法一样随时使用闭包。实际上,大多数的前端JS代码都是事件驱动性的:我们定义一些事件,当这个事件被用户所触发的时候(例如用户的点击事件和键盘事件),我们的事件通常会带上一个回调:即事件触发所执行的函数。举个栗子,假设我们希望在页面上添加一些按钮,这些按钮能够调整文字的大小,实现这个功能的方式是确定body的字体大小,然后再设置页面上其他元素(例如标题)的字体大小,我们使用em作为单位。
<style> body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; } </style>
我们设置的调节字体大小的按钮能够改变body的font-size,并且这个调节能够通过相对字体单位,反应到其他元素上,
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是三个分别把字体大小调整为12,14,16的函数,我们可以把他们绑定在按钮上。
<button id="size-12">12</button> <button id="size-14">14</button> <button id="size-16">16</button>
document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16;
通过闭包来封装私有方法:类似JAVA语言能够声明私有方法,意味着只能够在相同的类里面被调用,JS无法做到这一点,但却可以通过闭包来封装私有方法。私有方法不限制代码:他们提供了管理命名空间的一种强有力方式。
下面代码阐述了怎样使用闭包来定义公有函数,公有函数能够访问私有方法和属性。
var counter = (function () { debugger; 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());// 0 counter.increment(); counter.increment(); console.log(counter.value());// 2 counter.decrement(); console.log(counter.value()); // 1
在之前的例子当中,每个闭包具有他们自己的词法环境,而在这个例子中,我们创建了一个单独的词法环境,这个词法环境被3个函数所共享,这三个函数是counter.increment, counter.decrement和counter.value。
共享的词法环境是由匿名函数创建的,一定义就可以被执行,词法环境包含两项:变量privateCounter和函数changeBy,这些私有方法和属性不能够被外面访问到,然而,他们能够被返回的公共函数访问到。这三个公有函数就是闭包,共享相同的环境,JS的词法作用域的好处就是他们可以互相访问变量privateCounter和changeBy函数。
下面一个例子:
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(); alert(counter1.value()); /* Alerts 0 */ counter1.increment(); counter1.increment(); alert(counter1.value()); /* Alerts 2 */ counter1.decrement(); alert(counter1.value()); /* Alerts 1 */ alert(counter2.value()); /* Alerts 0 */
两个计数器counter1和counter2分别是互相独立的,每个闭包具有不同版本的privateCounter,每次计数器被调用,词法环境会改变变量的值,但是一个闭包里变量值的改变并不影响另一个闭包里的变量。
看下面一个例子:
<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 数组定义了三个有用的hint,每个分别与输入框的id相对应,每个方法与onfocus事件绑定起来。当你运行这段代码的时候,不会像预期的那样工作,不管你聚焦在哪个输入框,始终显示你的age信息。
原因在于,分配给onfocus事件的函数是闭包,他们由函数定义构成,从setupHelp函数的函数作用域获取。三个闭包由循环所创建,每个闭包具有同一个词法环境,环境中包含一个变量item.help,当onfocus的回调执行时,item.help的值也随之确定,循环已经执行完毕,item对象已经指向了helpText列表的最后一项。
解决这个问题的方法是使用更多的闭包,具体点就是提前使用一个封装好的函数:
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 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++) { (function () { var item = helpText[i]; document.getElementById(item.id).onfocus = function () { showHelp(item.help); } })(); // Immediate event listener attachment with the current value of item (preserved until iteration). } } setupHelp();
如果不想使用闭包,也可以使用ES6的let关键字:
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++) { let item = helpText[i]; document.getElementById(item.id).onfocus = function () { showHelp(item.help); } } } setupHelp();
这个例子使用let代替var,所以,每个闭包绑定了块级作用域,也就意味着不需要额外的闭包。
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。