何为闭包?
闭包(Closure)是一个封闭的作用域,它可以访问外部作用域的变量。
说起来比较抽象,实际上闭包就是一个函数,函数内部可以访问外部的变量,比如下面这个例子:
function sayHello() {
const greet = 'hello world';
function helloFunc() {
console.log(greet);
}
helloFunc();
}
sayHello(); // hello world
helloFunc函数内虽然没有定义greet变量,但是它的外层函数sayHello函数里定义了greet变量,所以最后成功输出hello world,这个例子里面helloFunc就是一个闭包
,寻找变量的过程是按照作用域链
来寻找的。
闭包的作用
我们知道javascript外部作用域无法访问内部作用的值。
function func() {
var name = 'jack';
}
console.log(name); // undefined
但是函数内部却可以访问外部变量。
var name = 'jack';
function func() {
console.log(name);
}
func(); // jack
所以函数内部可以修改外部状态,我们可以让函数拥有状态,比如迭代器
或生成器
。
var add = (function() {
var counter = 0;
return function() {
counter++;
return counter;
};
})();
add(); // 1
add(); // 2
add(); // 3
这个例子我们使用了一个立即执行函数表达式
(IIFE)创建了一个匿名函数也就是一个闭包,函数返回的函数会引用这个匿名函数的局部变量counter,所以我们每次调用add函数输出的值都不一样,add函数因为闭包有了状态。
这里使用立即执行函数主要是为了直接返回最终函数,也可以返回个普通函数像下面这样:
var genAddFunc = function() {
var counter = 0;
return function() {
counter++;
return counter;
}
}
var add = genAddFunc();
add(); // 1
add(); // 2
add(); // 3
闭包的应用
比如我们需要对多个li绑定点击事件如下:
- item 1
- item 2
- item 3
const itemList = document.getElementById('itemList');
const items = itemList.getElementsByTagName('li');
for (var i = 0; i < items.length; i++) {
items[i].onclick = function() {
console.log(`item ${i} clicked`);
}
}
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked
我们发现虽然li的点击事件都已经创建了闭包记住了i的值,但是都是输出3而不是0,1,2这是为什么呢?
原因是for循环中大括号包含该的部分并不是一个封闭的作用域,通过下面代码可以验证:
for (var i = 0; i < 3; i++) {}
console.log(i); // 3
console.log(window.i); // 3
当for循环结束后变量i并没有被回收,实际上我们是创建了一个全局变量i,3个item的点击事件函数绑定的是全局变量i的值,所以最后都输出的是item 3 clicked,通过下面代码可以验证:
for (var i = 0; i < items.length; i++) {
items[i].onclick = function() {
console.log(`item ${i} clicked`);
}
}
i = 99;
items[0].click(); // item 99 clicked
items[1].click(); // item 99 clicked
items[2].click(); // item 99 clicked
当我们修改了i的值发现3个点击事件的结果也都变成了item 99 clicked,说明它们输出的i绑定的都是最新i的值而不是当时循环时i的值。
于是我们尝试一下使用一个局部变量看看能否保存i的值,这样我们就可以输出正确的i的值:
for (var i = 0; i < items.length; i++) {
items[i].onclick = function() {
var j = i;
console.log(`item ${j} clicked`);
}
}
i = 99;
items[0].click(); // item 99 clicked
items[1].click(); // item 99 clicked
items[2].click(); // item 99 clicked
使用局部变量j保存i还是没有成功,我们修改了i的值后j的值也发生了变化,说明js在执行过程中j还是引用i的值。
解决方法1:函数传参
for (var i = 0; i < items.length; i++) {
items[i].onclick = function() {
console.log(`item ${j} clicked`);
} (i)
}
i = 99;
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked
通过传递参数我们得到了正确的结果,修改了i的值也没有影响点击事件的结果。
解决方法2: 使用闭包
for (var i = 0; i < items.length; i++) {
items[i].onclick = (function() {
var j = i;
return function() {
console.log(`item ${j} clicked`);
}
})();
}
i = 99;
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked
在闭包中我们用j来保存i的值,这样我们就能记住当时循环时i的值了,和之前的单独一个function区别是闭包可产生独立的作用域这样j就是i的值的拷贝而不是i的引用,我是这么理解的。
解决方法2: 使用let
使用let应该是最优雅的解决办法了,let是ES6的关键字它能够产生封闭的作用域:
for (let i = 0; i < items.length; i++) {
items[i].onclick = function() {
console.log(`item ${i} clicked`);
}
}
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked