一、何为闭包
定义
所谓闭包,其实是一个带有执行环境的函数。这个执行环境,就是指 「词法作用域」。那什么叫「词法作用域」 呢?简单说,词法作用域就是代码的书写位置。
闭包,可以在词法作用域的外部执行。这是闭包的特点。还有其他的说法:如,闭包是函数内部定义的函数。如,闭包是有权访问另一个函数内部变量的函数。其实这些都是关于闭包的不完全描述。
《你不知道的JavaScript》 一书中关于闭包的理解:即,可以记住并访问所在词法作用域的函数。再简单点理解就是,在定义函数的作用域之外,能够访问这个作用域内的变量。这样的函数,相当于自己随时带着一个执行环境。这个函数以及它的执行环境,就称之为闭包。下面画图说明。
整个红框中的变量,以及 sayHello
函数,形成了闭包。包起来的是变量 me
。函数 printTxt
把 sayHello
作为执行结果返回了。之后把返回结果赋值给 print
。此时,执行 sayHello
函数,当前作用域、整个作用域链中,变量 me
是不存在的。这么操作,应该是找不到 me
,并且报错才对。但因为这个闭包的存在, sayHello
函数成功找到变量并执行了。
请注意:js中,作用域链访问权限是由内向外的。也就是说,所有父级作用域对子级可见,但是子级对父级不可见。
到了这里,也就能理解啥 闭包是有权访问另一个函数内部变量的函数了。因为上面的例子表明,就是这么回事。 print
访问到了printTxt
内部的变量。这句没错,但不够严谨。为啥不严谨,看下面的例子:
function printTxt() {
const me = 'Tom'
function sayHello() {
console.log(`This is ${me}!`)
}
return sayHello
}
printTxt()
这里没有产生闭包。 执行 print
能访问到 me
,是因为作用域链的存在。当执行 sayHello
时,首先在自己的作用域内进行了查找,没有找到。但是向上查找,在父级作用域中找到了变量。这种情况,也能够这样描述:sayHello
有权限访问 printTxt
中的变量。但这不是闭包,所以不够严谨。
例子
闭包,其实在代码中随处可见。只是我们没有注意。
以下这些,都是闭包:
- IIFE(Immediately Invoked Function Expression)立即执行函数表达式
- 定时器
- 事件监听器
- …… 只要使用了回调函数,都会产生闭包。 ```javascript // 无闭包 for (var i = 0; i < 5; i++) { setTimeout(function () { console.log(i) // 4, 4, 4, 4, 4 }, 0) }
// 有闭包 for (var i = 0; i < 5; i++) { (function (k) { setTimeout(function () { console.log(k) // 0, 1, 2, 3, 4 }, 0) })(i) }
如果我们想依次打印 1~5,用第 1 种方式是不行的。因为在定时器执行的时候,访问到的变量 `i` ,是唯一的,只有一个。所以会重复打印。在 第 2 种方式中,因为IIFE,每次循环都会产生新的作用域,封存了一个变量 k,k保留的对 i 的引用。循环结束,并不会立即释放这些变量。一共有5份。 `console.log` 时,会依次次读取每个 IIFE 中的k。
<a name="PG1Ii"></a>
## 二、闭包的用处
总体来说,常见的用处,大概有以下:
1. 块级作用域(ES6的 let命令,很好的解决这个需求),像上面的 `for` 循环那样
2. 封装独立模块,避免局部变量、全局变量冲突
3. 私有变量,私有方法。
<a name="EJ1pP"></a>
## 三、闭包的缺点
过度使用闭包,会造成大量的内存地址不能回收,导致内存泄露。
<a name="esmCZ"></a>
## 四、立即调用函数表达式
IIFE,即 Immediately invoked Function Expression。一个匿名的函数声明,声明之后立即调用。有两种写法,不论哪种,都是表达式后面,紧跟一个 `()`。
```javascript
(function() {
// 一种写法
})()
(function() {
// 一种写法
}())
常见的作用:
- 闭包
- 模块化