JavaScript闭包原理与用法学习笔记
本文实例讲述了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程序设计有所帮助。
以上是 JavaScript闭包原理与用法学习笔记 的全部内容, 来源链接: utcz.com/z/339113.html