任何事情,我们都要了解为什么,再去了解是什么

前言

为什么要了解闭包呢,首先,闭包是JavaScript学习的一个难点,也是一个特色;其次,我们在日常的使用中,偶尔会因为闭包导致一些错误。

假设一个使用场景,我们现在需要编写一个登录信息的界面,然后有个基本的要求,在用户聚焦到输入框输入内容的时候,在旁边给个提示信息,比如,当用户在输入邮箱的时候,提示“这里输入你的邮箱”。

1
2
3
4
5
6
<p>登录信息</p>
<p id="help"></p>
<p>邮箱: <input type="text" id="email" name="email"></p>
<p>姓名: <input type="text" id="name" name="name"></p>
<p>年龄: <input type="text" id="age" name="age"></p>
<!--省略其他包括按钮的代码-->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': '请输入你的邮箱'},
{'id': 'name', 'help': '请输入你的名字'},
{'id': 'age', 'help': '请输入你的年龄,必须大于16周岁'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function(){
showHelp(item.help);
};
}
}
setupHelp();

在代码中,我将每个输入框的聚焦监听都绑定了一个函数。但最终的实现效果却和预期的不一样,当我点击任何一个输入框时,只会提示“请输入你的年龄,必须大于16周岁”。这里其实不难理解,绑定函数的过程,函数体是不会执行的,而是当输入框被聚焦的时候,才会去执行函数的内容。也就是说,当点击任意输入框时,才会执行函数function(){showHelp(item.help);};里的内容。

然后我们再来看下,函数体里使用的变量item,属于循环体内部,而item所涉及到的变量helpText和i同样属于父函数setupHelp。而在函数show被使用的时候,父函数setupHelp里的变量i已经经过了循环变成了2,所以item就固定为helpText[2]。

总结下来就是,我们所使用的函数里的变量发生了改变;那首先的直观想法就是,将变量绑定到函数内部。这里我们可以使用立即执行函数(function(x){})(i)

1
2
3
4
5
6
7
8
9
//这里只显示for函数的内容
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = (function(selfItem){
return function(){
showHelp(selfItem.help);
}
})(item);
}

我想称这种写法为流氓写法,将父函数的变量强行占位己有,其实说白了就是在循环发生的时候,函数就执行了,使用的变量不再是循环体里的变量。

当然,也可以将立即执行函数给提取出来,最后的实现是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': '请输入你的邮箱'},
{'id': 'name', 'help': '请输入你的名字'},
{'id': 'age', 'help': '请输入你的年龄,必须大于16周'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();

个人觉得可以这么理解,在循环体进行的时候,每次都会去执行makeHelpCallback函数,而变量help只属于该函数,所以它不再由item决定。

好了,上面的场景就是我们在日常编程中可能会遇到的问题。而为什么会出现上面的问题呢,主要是因为我们想在父函数里写子函数,而在JS的语法里,默认的,在子函数里,可以使用父函数的变量,而在子函数使用的时候,父函数已经执行完毕,而子函数使用的父函数的变量是执行完毕后的变量。

好了,噔噔蹬蹬……引入闭包的概念了,我们把能够读取其他函数内部变量的函数,称为闭包。 当然这是阮一峰老师的理解,通俗易懂。

其实归根结底就是两个重点,一个是变量的作用域,以及函数的执行。接下来,我们借助这两点来更好的理解闭包。

变量的作用域

变量的作用域无非就是两种:全局变量和局部变量。而Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

例子1:在函数getNum执行的时候,我们可以获取到全局变量num。

1
2
3
4
5
var num = 7;
function getNum(){
return num;
}
getNum();//7

在这里有一点需要注意,当在子函数内部定义变量时,要使用var类型,否则变量将会被默认为全局变量。如:

1
2
3
4
5
6
7
8
9
10
function p(){
function p1(){
n = 7;
console.log(n);//7
}
p1();
console.log(n);//7
}
p();
console.log(n);//7

例子2:在函数外部获取到函数内部的变量。

1
2
3
4
5
6
7
8
9
10
11
var p = function(){
var num = 7;
var getNum = function(){
return num;
}
return {
getNum:getNum
};
}
var p1 = p();
p1.getNum();//7

这里,我们在函数外部想获取到函数内部的变量,就在函数p里使用了函数getNum,所以,这里的函数getNum就是闭包,而这也是闭包的用途之一,用来让外部读取函数的内部变量。

函数的执行

例子3,这里我们分析一段简单的JS代码,并将其上下文执行顺序写在了注释中。

1
2
3
4
5
6
7
8
9
let a = 3               //1.声明变量a,赋值3
function addTwo(x) { //2.声明函数addTwo,赋值,但不执行
//4.在addTwo中声明变量x,赋值a,全局上下文搜索a,赋值3
let ret = x + 2 //5.在addTwo中声明变量let,赋值x+2,addTwo上下文搜索x,找到3,赋值5
return ret //6.返回值5,addTwo执行结束,消除变量x,ret
}
let b = addTwo(a) //3.声明变量b,赋值addTwo,全局上下文搜索,找到了addTwo,执行函数addTwo
//7.函数addTwo执行返回5,赋值5
console.log(b) //8.打印5

例子4,接下里再来看一个使用到闭包的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createCounter() {          //1.声明函数createCounter
let counter = 0 //3.声明变量counter,赋值0
const myFunction = function() { //4.声明函数myFunction,再将函数作用域内声明的变量打成闭包,及函数的内容,一起赋值给函数myFunction
counter = counter + 1 //8.寻找变量counter,先在本地上下文搜索,找不到则在闭包里搜索,找到变量counter,将0加1,并赋值1,再次保存到闭包中。
//12.寻找变量counter,先在本地上下文搜索,找不到则在闭包里搜索,找到变量counter,将1加1,并赋值2,再次保存到闭包中。
return counter //9.返回变量counter的值1
//13.返回变量counter的值2
}
return myFunction //5.返回函数myFunction
}
const increment = createCounter() //2.声明变量increment,全局上下文搜索,找到函数createCounter并执行
//6.返回值给变量increment赋值,其中包括一个函数的定义,及一个闭包。
const c1 = increment() //7.声明变量c1,全局上下文搜索,找到函数increment并执行
//10.返回值1,赋值给c1
const c2 = increment() //11.声明变量c2,全局上下文搜索,找到函数increment并执行
//14.返回值2,赋值给c2
const c3 = increment() //15.同理返回值3,赋值给c3
console.log('example increment', c1, c2, c3)

从上面的例子可以看出,父函数在定义子函数的时候,会将子函数作用域内的变量声明一起打包在子函数中,如果子函数被赋值给变量A,则A实际上除了包含函数的内容外,还携带了一个包含函数作用域内的变量声明的背包,这个背包实际上就是闭包

结论

  • 本文参照了一些文章,主要对JS的闭包(Closure)进行了一些个人的解读。
  • 在本文介绍“函数的执行”时,将闭包模拟成了父函数托付给子函数的包含父函数声明的变量的背包,个人认为这个比喻是最为恰当的。当然,这个例子诠释了“包”的概念,没有诠释“闭”的概念。我的想法是,“闭”更多的体现在,它是在子函数定义的时候创建的,并赋予定义的变量,但不属于全局。
  • 最后总结下闭包的利弊:利:让外部可以使用到函数内部的变量,或者像例子4一样用来做计数器;弊:闭包的变量是保存在内存中,如果使用过多会影响性能等。

参考