变量作用域
在讲闭包之前我们先来简单介绍一下JavaScript中的变量作用域。
在ES6之前变量(用var关键字声明的变量)根据作用域的不同分为两种:全局变量和局部变量
在ES6中还新增了块级作用于变量(用let声明的变量),用let声明的变量也分为全局变量和局部变量,只不过是用let声明的局部变量是块级作用域的,作用域的范围比更加精细了。比如我们在一对大括号{}里用let声明的变量则作用域范围仅限于当前的大括号内部。
全局变量:一般情况下只要是在函数外声明变量都是全局变量
局部变量:在函数内部声明的变量为局部变量,只能在函数内部使用,如果使用let声明的变量则作用域范围更小,比如在函数内部的if语句块中声明,则作用域范围仅限if语句块。
用var声明的变量:
- 函数内部可以使用全局变量
- 函数外部不可以使用局部变量
- 语句块外部不能使用语句块内用let声明的变量
- 当函数或语句块执行完毕,本作用域内的局部变量会销毁
闭包
闭包(closure)是指有权访问另一个函数作用域中变量的函数。
通过前面讲的变量作用域我们知道,一般情况下,在一个函数的内部是没办法访问另一个函数内部定义的变量的,不管是var定义还是let定义,都是无法访问的。
function fun1(){
var name = 'alvin';
}
function fun2(){
console.log(name);
}
fun2();//报错ReferenceError:name is not defined
如上述代码所示,在fun2中访问fun1中的变量name,当我们调用fun2时会抛出错误提示:变量未定义。那么如果我铁了心就要在fun2中使用fun1中的name变量呢,就没有别的办法了吗?答案肯定是有的,那就是接下来我们所要介绍的闭包了。我们把上面的代码来稍微改造一下:
function fun1(){
var name = 'alvin';
function fun2(){
console.log(name);
}
fun2();
}
fun1();//alvin
上面这段代码就形成了一个闭包。我们来解析一下:上面说过闭包是指有权访问另一个函数作用域中变量的函数。在上面的代码中,fun2(函数)作用域中访问了fun1(函数)作用域(另一个作用域)中的变量name。刚好符合了闭包的定义,所以这里就产生了闭包。
上面这段代码可能会有人产生疑问:fun2本来就是fun1的内部,所以能访问fun1中的变量也是理所应当的。那如果我们想在fun1的外面访问fun1里面的变量能不能实现呢?下面我们来继续改造一下上面的代码:
function fun1(){
var name = 'alvin';
function fun2(){
console.log(name);
}
return fun2;//这里不直接调用而是返回
}
var fn = fun1();
fn();//alvin
上面代码中我们在fun1中不直接调用里面的函数fun2,而是将fun2作为返回值返回。然后在外面调用fun1并用变量fn来接收fun1的返回值。我们知道fun1的返回值也是一个函数(fun2),所以变量fn也就是一个函数了,这就相当于我们在全局作用域(fun1的外部)中定义了一个函数fn,而当我们去调用fn函数时结果打印输出了fun1中的变量值“alvin”。这样就实现了在函数fun1的外部访问函数内部的变量了。
闭包的作用
通过上面的代码,我们实现了在函数外部访问函数内部的局部变量。由此可知闭包的主要作用:延伸了变量的作用范围。
前面我们提到函数内部的变量在函数外部是无法访问的,而且在函数执行完毕后变量就会自动销毁。而通过闭包即使函数已经调用执行完毕其内部的变量也不会销毁,同时在函数外部也可以访问到函数内部的变量。所以这也再次印证了闭包的作用是:延伸了变量的作用范围。
闭包的案例
接下来我们用一个例子来演示一下闭包的使用
场景:在页面上有四个div,每个div内部都有一段文字描述,现在想要在页面加载完成5秒后,打印输出所有div中的内容。
分析:因为是打印所有的div内容,所以首先需要获取到所有的div元素,然后再用for循环逐个打印。
因为是在5秒后才打印,所以这里需要在for循环中使用setTimeout函数进行倒计时。
问题:那么问题就来了,我们都知道for是同步一次性执行完成的,而setTimeout是异步函数,也就是说如果在for循环中使用setTimeout函数,并且setTimeout函数内部需要用到for循环中的计步器,那么就会引发一个问题,即当setTimeout函数真正开始执行的时候,它内部拿到的计步器将永远都会是for循环结束后的最后一个。看下面代码:
<html>
<head>
<title>闭包DEMO</title>
</head>
<body>
<div>javascript</div>
<div>VUE</div>
<div>HTML</div>
<div>CSS</div>
<div>python</div>
<script>
function printDivContent(){
var divs = document.querySelectorAll('div');
for(var i = 0; i < divs.length; i++){
setTimeout(function(){
console.log(divs[i].innerHTML);
}, 5000);
}
}
printDivContent();//5秒后会抛出错误:Uncaught TypeError:Cannot read property 'innerHTML' of undefined
</script>
</body>
</html>
我们来看上面代码,5秒后没有得到我们所期望的结果,而是抛出了错误:Uncaught TypeError:Cannot read property 'innerHTML' of undefined。这是因为setTimeout内部的函数开始执行的时候,for循环已经结束所以我们得到的i的值已经变成了5,而divs的最大索引是4,所以实际上divs[5]是一个undefined而undefined并没有任何属性,所以就报出了上述错误。
下面我们将代码改为闭包的形式,再来看一下:
<html>
<head>
<title>闭包DEMO</title>
</head>
<body>
<div>javascript</div>
<div>VUE</div>
<div>HTML</div>
<div>CSS</div>
<div>python</div>
<script>
function printDivContent(){
var divs = document.querySelectorAll('div');
for(var i = 0; i < divs.length; i++){
//定义一个自调用函数
(function(index){
setTimeout(function(){
console.log(divs[index].innerHTML);
}, 5000);
})(i);//将i作为参数传递给自调用函数
}
}
printDivContent();
//5秒后打印输出:javascript VUE HTML CSS python
</script>
</body>
</html>
我们看到代码改造后已经输出了我们期望的结果。这里我们定义了一个自调用函数并将for循环的计步器作为参数传递给自调用函数,这样在自调用函数内部就能拿到for循环每次循环的索引值而不是只有最后一个值了。
而在自调用函数内部还有一个匿名函数使用了自调用函数的参数index,这样就形成了一个闭包。我们说过闭包的作用就是延伸了变量的作用范围,从而也就实现了我们期望的效果。
闭包缺点
普通函数中,当函数执行完毕其内部的变量会随之销毁,而闭包中,即使函数已经执行,它内部的变量也不会销毁,而是要等到使用到这个变量的函数执行完成后才会销毁,这样就会造成一定的性能损失。