简单易懂讲闭包

刚看到了 Rust 语言中利用闭包实现匿名函数,想着博客许久没更新了,那就写一篇帮助大家理解吧。闭包是 JavaScript 的一个特点,搜索引擎搜索闭包基本都会用 JavaScript 做例子,同时闭包也是 JavaScript 初学者较难理解的一个部分。

前置知识

作用域规则

JavaScript
1
2
3
4
5
6
7
const variable = "variable";
function readVar() {
var innerVariable = "innerVariable";
return variable;
}

console.log(innerVariable); //undefined

这段代码说明的是 JavaScript 的作用域链规则,readVar函数执行时没有在自身作用域内找到variable变量,于是向上层作用域寻找,并使用它。简单来说就是子作用域能使用父作用域的变量,而父作用域不能使用子作用域的变量。

讲清楚什么是闭包

一句话

闭包保存的是函数和函数运行的必要环境(使用到的变量等),它和作用域类似,但闭包只是包含一部分作用域,它是函数和函数使用的外部变量的集合

产生

闭包在函数创建时就会产生,只要使用函数就存在闭包。

利用闭包

按照上面所述,只要有函数就有闭包,但是我们在使用的时候感受不到闭包的存在。要怎么利用闭包呢?

这个例子已经被用烂了,但是用它来讲闭包也再合适不过了。

JavaScript
1
2
3
4
5
6
7
8
const countUp = (() => {
let count = 0;
return () => {
return ++count;
};
})();
// 每调用一次返回一个比上一次大1的值
countUp();

第 4 行使用了一个父作用域的变量count,可以改变count的值,第 3 行将一个匿名函数返回到countUp外部,相当于创建了一个快照并保存到立即执行函数外部,里边包含了函数内部环境和该函数使用的外部变量count,这样通过闭包就能用countUp函数控制count值。你可以看到,利用闭包可以在函数外部控制函数内部的值。所以,利用闭包的核心将函数和环境保存到外部变量

我们都知道,当函数执行完毕,执行上下文离开作用域(这里指函数作用域),作用域会被立即销毁,按照常理来说代码不可能运行起来。但是因为返回了一个闭包,这之后闭包为countUp函数提供环境,这就是为什么上面的例子中立即执行函数执行完之后,通过countUp访问到的count是之前的状态。

所以,在使用闭包的时候你需要清楚的一点是,闭包会造成内存泄漏,如果你返回了一个函数作为闭包,只要返回的函数存在,这个闭包就一直存在,会占用内存。

说点其他的

这里我们利用《Understanding ECMAScript 6》块级绑定中的一个例子

典型错误

JavaScript
1
2
3
4
5
6
7
8
9
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function () {
console.log(i);
});
}
funcs.forEach(function (func) {
func(); // 输出 10 共10次
});

在第 3 行,console.log()使用了变量i,它的闭包中除了本身,还应该保存有变量i,这个闭包被保存到了funcs[i],但你要注意到,变量i是用var声明的,它在函数作用域中,i存在的作用域只有 window 一个,在循环中虽然产生了 10 个闭包,但他们都保存着同一个i,当循环执行完,i = 10,所以最后从闭包中取到的i也全都是 10。

解决思路

上面提到了,因为变量i存在的作用域只有一个,而一个作用域中只能有一个i,要使i值不一样,就需要创建不同的作用域,解决方法有两个。

方法一:使用块级作用域

只需要把var声明改为let声明即可

JavaScript
1
2
3
4
5
6
7
8
9
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs.push(function () {
console.log(i);
});
}
funcs.forEach(function (func) {
func(); // 输出 0,1,2 ... 9
});

在 for 循环初始化的时候使用进行 let 声明,这时变量的作用域是局部块作用域,每进入一次循环都会重新创建一个i变量。所以每次循环产生的闭包中的i都来自不同的作用域,而且每个作用域中的i都是随循环变化的,最终达成目的输出 0~9。

这时候不妨回去检验一下(已经头晕请跳过)
我们将i变量和funcs变量放在同级作用域下,代码如下:

JavaScript
1
2
3
4
5
6
7
8
9
10
var funcs = [];
let i;
for (i = 0; i < 10; i++) {
funcs.push(function () {
console.log(i);
});
}
funcs.forEach(function (func) {
func(); // 输出 10 共10次
});

我不是故意把你弄晕的,这样就能再次证明导致 bug 的根本原因是 10 个闭包使用同一个作用域的变量。

方法二:创建函数作用域

这种方法的思路是创建函数内部的i变量,根据作用域链规则,当函数内部存在i变量时,就会屏蔽掉上层的i变量,通过函数返回闭包,这样闭包内的i变量就来自于函数,而不是函数上层的i变量。每个函数都会生成独一无二的作用域,根据上面的结论,同样能解决问题。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(
(function (value) {
return function () {
console.log(value);
};
})(i)
);
}
funcs.forEach(function (func) {
func(); // 输出 0,1,2 ... 9
});

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×