这篇文章是2018-06-26号创建的,那个时候我接触到了闭包,然后写下了自己的理解。2019年的时候我编辑过一次。因为自己对闭包有了新的理解。。

现在我看了《你不知道的JavaScript》这本书的上卷后,删除了之前编辑的内容。准备重新分享我对闭包的理解。

词法作用域

词法作用域,又叫静态作用域。

说道词法作用域就要提一下编译的三个阶段:

  • 词法分析
  • 语法分析
  • 代码生成

而词法作用域就是在词法分析阶段确定的,换句话说就是你在写代码时将变量放在哪个代码块里来决定。

看这样一段代码:

1
2
3
4
5
6
var a = 100
function foo(){
var a = 20;
console.log(a)
}
foo() // 20

稍微有点JavaScript基础的同学都知道,输出为什么是20。

作用域发生嵌套时,引擎会从当前作用域往外层逐层查找,就和前端的click事件冒泡一样。

什么是闭包

我印象里的关于闭包的定义有很多:

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。 —— MDN WEB Docs
  • 闭包就是能够读取其他函数内部变量的函数。 —— 阮一峰的网络日志
1
2
3
4
5
6
7
8
9
function foo(){
var a = 2
function bar(){
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2
  • 这段代码中,bar() 仍然持有对foo函数的作用域的引用,这个引用就叫做闭包。 —— 《你不知道的JavaScript》

这三种说法都没有问题,正常的词法作用域查找规则都是闭包的一部分,所以说,闭包无处不在。

闭包的应用

IIFE

IIFE 全称是 立即调用函数表达式(immediately-invoked function expression)

1
2
3
4
var a = 2
(function IIFE(){

})()

首先IIFE确实创建了闭包,但是a是通过普通的词法作用域查找而非闭包被发现的。

所以,这里我也迷糊了。我暂时认知为,它创建了闭包,但是没有使用闭包!

循环

在块作用域没出来之前,当循环里面有异步操作时,如何保证循环变量的值对应的是每一次循环时的值?

1
2
3
4
5
for(var i=1; i<=5; i++){
setTimeout(function timer(){
console.log(i)
},i*1000)
}

大部分同学一眼都能看出来,这段代码会以每秒一次的频率输出五个 6

那么如何按照我们的预期,每个一秒依次输出1~5呢?

1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer () {
console.log(i)
}, i * 1000)
})(i)
}

我们封闭settimeout的上级作用域,使i的查找的不再是var i ,而是 (function(i){})(i) 这个IIFE传递进来的,每一次循环的i值,会被保存下来,不会随着循环而改变。

可以参考以前搬运的一片文章:一道有意思的面试题

回调函数

回想一下,根据上面对闭包的定义,JavaScript上所有使用了回调函数的地方都使用了闭包!

模块

node.js的模块加载参照的是CommonJS规范,每个 .js 能独立一个环境只是因为 node 帮你在外层包了一个自执行函数

1
2
3
(function (exports, require, module, __filename, __dirname) {
t = 111;
})();

这样的话。a.js引用b.js,并且使用其暴露出去变量和方法不就是在使用闭包吗?

最后,我也不知道我又没有表达清楚,但我对闭包确实有了更深的理解。

参考链接: