浅谈JS闭包

变量的作用域

了解JS的人都知道,在ES6之前JavaScript中只有函数作用域和全局作用域,而没有块级作用域(try…catch是一个例外)。该怎么理解这句话呢?我们先来看一个例子:

1
2
3
4
for(var i=0; i<5; i++){
console.log(`i=${i}`)
}
console.log(i) // 4

当运行完一个for循环后,i=4。由于JS中不存在块级作用域,所以这里在for循环中申明的变量i是一个全局变量,因此可以在外部访问到。
现在我们来看下一个例子:

1
2
3
4
5
6
7
8
9
function init(){
var name = 'limoer';
function sayHello(){
console.log(`hello ${name}`)
}
sayHello();
}
init(); // hello limoer
console.log(name); // undefined

这里我们定义了一个函数,函数中申明了一个局部变量name,并且在函数内部定义了一个内部函数sayHello,这个函数只能在函数init内使用,然而sayHello并没有自己的局部变量,但是其可以访问到函数外部的变量,即其父级函数的name变量

通过上面的两个例子可以清楚的知道,变量的作用域完全是由它在源代码中的位置决定的,并且嵌套的函数也可以访问其外层作用域中的变量。

闭包

闭包和变量的作用域息息相关。现在我们来修改上面的这个例子

1
2
3
4
5
6
7
8
9
function init(){
var name = 'limoer';
function sayHello(){
console.log(`hello ${name}`)
}
return sayHello
}
var sayHelloFunc = init();
sayHello(); // hello limoer

注意修改的地方,我们这次是直接返回这个内部函数,然后在外部执行这个函数。
但是,通常来说,当函数一旦运行完成,其局部变量就不可用了,在这里是当执行了var sayHelloFunc = init();后name应该不可用了。但是实际运行情况是成功访问到了name这个属性。

原因是因为这里sayHelloFunc已经成为了一个闭包。它由两部分组成,返回的函数本身以及创建该函数的环境。
而所谓的环境是由闭包在创建时其作用域内的变量组成的。对于上面的这个例子,这里的变量就是指的name

再看一个闭包的例子

1
2
3
4
5
6
7
8
9
function addSome(num){
return function(y){
console.log(num + y)
}
}
var add10 = addSome(10);
var add1 = addSome(1);
add10(1); // 11
add1(10); // 11

对于上面的这个例子,addSome()做为一个函数工厂产生了两个闭包,它们共享了函数的定义,但是却又保存了不同的环境。

闭包的应用

通过上面的描述,知道闭包其实就是将函数和其作用环境相互关联起来,达到保存变量的目的。

把上面的例子稍微改一下,我们可以把它用到实践中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<button id="toBlue">切换背景为蓝色</button>
<button id="toYello">切换背景为黄色</button>
<button id="toGreen">切换背景成绿色</button>
<script type="text/javascript">
function changeBgColorTo(type){
return function(){
document.body.style.backgroundColor = type;
}
};
var toBlue = changeBgColorTo('blue');
var toYellow = changeBgColorTo('yellow');
var toGreen = changeBgColorTo('green');
document.getElementsById('toBlue').addEventListener('click', toBlue);
document.getElementsById('toYellow').addEventListener('click', toYello);
document.getElementsById('toGreen').addEventListener('click', toGreen);
</script>
</body>
</html>

上面的例子展示了如何使用闭包来定义公共函数,来减少代码的冗余。

一个常见的错误,使用闭包来解决

直接贴代码吧:
html:

1
2
3
4
<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>

js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();

上面的代码是我们实际开发过程中非常常见的错误。我们使用循环来给每一个输入框绑定一个事件,从而实现当聚焦到不同的输入框是产生不同的输出。
但是,上面的代码显然不能完成这样的工作,因为当循环完成后,此时item已经指向了helpText的最后一项,而给onfocus绑定的是一个匿名函数,当聚焦到某一个输入框时,执行showHelp(item.help)而item早已是helpText中的最后一项了,所以造成了错误.

知道错误后,我们就知道改怎样修改了。我们需要保存运行时的环境,返回一个闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(help) {
return function(){
showHelp(help)
}
})(item.help);
}
}

好了,简单的对于闭包的介绍就到这里了!
想更系统的学习JS点击这里

分享到 评论