# 深入理解 JavaScript 之闭包

## 概念

> 闭包就是指有权访问另一个函数作用域中的变量的函数

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>点击li标签弹出对应数字</title>
  </head>
  <body>
    <ul>
      <li>0</li>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
    <script>
      var list = document.getElementsByTagName('li');
      for(var i = 0;i < list.length;i++){
       list[i].onclick = function(){
           alert(i);
       }
    </script>
  </body>
</html>
```

如上题，最为常见的一个例子，这里解释由这道题引出的 js 知识点，如上我们知道在浏览器运行无论点击哪个 li 标签都是弹出 3,首先来理解为什么会弹出 3。

程序通过 for 循环给每个 li 标签绑定了事件，然后通过点击 li 标签触发方法，即执行 alert(i)。js 中有个作用域链查找机制，首先会在 onclick 返回的函数作用域查找 i 变量的值，找不到则往上一层找 i，上一层即是 window 全局作用域，即找到全局变量 i，即 for 循环定义的 i。

注意 for 循环的 i 并不是私有变量，而是全局变量。

> js 中所有的事件绑定都是异步编程（当前这件事件没有彻底完成，不再等待，继续执行下面的任务）

当绑定 onclick 事件后，不需要等待执行，继续执行下一个循环任务，所以当我们点击执行方法的时候，循环早已结束,即是最后 i=3。故程序执行后全局变量 i 被循环执行后赋值为最终的 3，所以当点击的时候，外层循环已经结束，页面加载完成预示着 js 代码都已经执行完成，即执行 alert(i)时，由于 i 不是私有变量，便会找到上一级 window 作用域全局的 i，所以无论点击哪个 li 标签都是弹出 3

## 为什么要用闭包

那么，解决这个问题的缘由，在于 i 每次在页面加载完就赋值为 3，alert(i)的时候总是找到全局变量 i。在 ES5 传统语法中，能形成作用域的只有全局和函数，现在每次 i 找的都是全局，那么要保住 i 的值只能在全局和 onclick 返回函数的作用域中间再加一个小的私有作用域，即是大的作用域外再加一个小的作用域，这样 i 往上一层作用域查找时，就会获取小作用域的 i 的值，而不会去获取全局变量的 i 值。

这个思路解决问题就需要引入闭包，在这个理解上闭包是指函数变量可以保存在函数作用域内，因此看起来是函数将变量“包裹”了起来。于是，代码改成：

```javascript
for (var i = 0; i < list.length; i++) {
  list[i].onclick = (function (n) {
    //形参n
    //=>让自执行函数执行,把执行的返回值(return)赋值给onclick
    //（此处onclick绑定的是返回的小函数，点击的时候执行的是小函数）,
    // 自执行函数在给事件赋值的时候就已经执行了
    // 自执行函数形成一个私有作用域
    var i = n
    return function () {
      alert(i)
    }
  })(i) //传入实参i
}
```

循环三次，形成三个不销毁的私有作用域（自执行函数执行），而每一个不销毁的栈内存中都存储了一个私有变量 i，而这个值分别是每一次执行传递进来的全局 i 的值（也就是：第一个不销毁的作用域存储的是 0，第二个是 1，第三个是 2，第四个是 3）；当点击的时候，执行返回的小函数，遇到变量 i，向它自己的上级作用域查找。这样就达到了我们需要的效果，这种闭包实现，也可以有另一种写法。

```javascript
/*原理同法二都是形成三个不销毁的私有作用域,分别存储需要的索引值*/
for (var i = 0; i < list.length; i++) {
  ;(function (n) {
    list[n].onclick = function () {
      alert(n)
    }
  })(i)
}
```

对于初始的代码，如果说为什么不能实现，那原因就可归纳为：

> 1.执行方法,形成一个私有的栈内存,遇到变量 i,i 不是私有变量,向上一级作用域查找（上级作用域 window） 2.所有的事件绑定都是异步编程,绑定事件后,不需要等待执行,继续执行下一个循环任务,所以当我们点击执行方法的时候,循环早已结束（让全局的 i 等于循环最后的结果 3）

## ES6 语法的解决方式

在 ES6 中，解决这种问题只需要一个 let 变量，ES6 中才有块级作用域（类似于私有作用域）的概念

```javascript
for (let i = 0; i < list.length; i++) {
  list[i].onclick = function () {
    alert(i)
  }
}
```

## 闭包对内存的影响

从上面可知，每次 for 都会形成一个私有作用域，每个都里面保存的变量 i 的值，程序运行后这些作用域并不会被销毁，所以由于闭包会携带包含它的函数的作用域，所以会比其他函数占用更多内容，过度使用闭包会导致内存占用过多。

在真实项目中为了保证 JS 的性能（堆栈内存的性能优化），应该尽可能的减少闭包的使用（不销毁的堆栈内存是耗性能的）

## 堆内存和栈内存的释放

这里又要提到一个知识点，js 中存储方式的分类：

> JS 中的内存分为堆内存和栈内存
> 堆内存：存储引用数据类型值（对象：键值对 函数：代码字符串）
> 栈内存：提供 JS 代码执行的环境和存储基本类型值

粗暴理解 var 定义的变量存在栈内存中，如 for 循环中的 i 是存在栈内存中的；而函数，它是存在堆内存中

**一般情况下，当函数执行完成，所形成的私有作用域（栈内存）都会自动释放掉（在栈内存中存储的值也都会释放掉）**，那为什么闭包的栈内存不会被自动释放掉，在 js 中也有特殊不被销毁的情况：

> 1.函数执行完成，当前形成的栈内存中，某些内容被栈内存以外的变量占用了，此时栈内存不能释放（一旦释放外面找不到原有的内容了） 2.全局栈内存只有在页面关闭的时候才会被释放掉

闭包则是属于第一种情况，onclick 函数形成的栈内存，被小函数【alert(i)，i 找到 onclick 作用域获取 i 值】占用了 onclick 函数的栈内存（变量 i 是存在栈内存中），故栈内存不能被释放，所以才会说闭包过度使用容易导致内存被占用过多，因为不会自动释放内存。

**堆内存的释放**

> 堆内存让所有引用堆内存空间地址的变量赋值为 null 即可（没有变量占用这个堆内存了，浏览器会在空闲的时候把它释放掉）
