JavaScript 闭包

83 阅读3分钟

闭包是什么?

引用 MDN 的描述:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

案例分析

闭包的形成

上面的话可以简单理解为 一个定义在函数内部的函数,且内部函数引用外部函数的值,我们来看一个简单的例子:

function count() {
    var i = 0;
    return function demo() {
        i += 1;
        console.log(i);
    }
}
let fn = count();
fn(); // 1

从上面的例子可以得出,要形成一个闭包,只需要 内部函数引用了外部函数的变量

闭包的特点

闭包的特点除了显而易见的 变量私有化 外,还有最重要的一点就是 被内部函数引用的变量,不会在父函数调用结束后被回收机制回收,看下面的例子:

function count() {
    var i = 0;
    return {
        add: function() {
            i += 1;
        },
        show: function() {
            console.log(i);
        },
    };
}
// 一般情况下 i 会随着父函数调用完成被回收
var Count = count();
Count.add();
Count.show(); // 1
Count.add();
Count.show(); // 2

多次调用后,i 被成功叠加,由此可以看出 i 一直被保存在内存中,并没有随着调用结束被回收
为什么会这样?因为 闭包函数会一直保存在内存中,而被依赖引用的变量也会一直保存在内存中 ,所以 i 不会在父函数调用完成后被回收机制自动回收
部分文章说到因为闭包会延长父函数变量的生命周期,所有会有内存泄漏风险,但是我认为应该是 不好的编码习惯导致存在内存泄漏的风险 ,不再使用的闭包应该及时手动清空

循环中的引用

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>标题</title>
  </head>
  <body>
    <button>按钮1</button>
    <button>按钮2</button>
    <button>按钮3</button>
    <script>
      let btns = document.querySelectorAll('button');
      for (var i = 0; i < btns.length; i++) {
        btns[i].onclick = function () {
          console.log(i); // 3
        }
      }
    </script>
  </body>
</html>

上面这个例子我相信已经不是第一次见到了,我们很清楚的知道 每次点击按钮,控制台打印的都是 3

因为 用 var 创建的 i 是一个全局变量,所以每一次循环就会覆盖原来的 i,导致打印出来的永远都是最后一个值。

答案显而易见,我们只需要一个 不会重复覆盖 i 的局部作用域 , 修改一下 for 循环的代码

for (var i = 0; i < btns.length; i++) {
    btns[i].onclick = (function(index) {
      return function() {
          console.log(index);
      }
    })(i);
}

总结

  • 如何产生一个闭包:
    1. 嵌套函数
    2. 子函数引用父函数的变量
  • 闭包的特点:
    1. 成员变量私有化,命名空间隔离
    2. 延长父函数变量的生命周期。只要闭包还在使用,父函数变量就不会被释放
  • 闭包使用注意点:
    1. 由于会延长变量的生命周期,所以当不再使用时,需要 手动将闭包设置为 null 释放内存,如 showCount = null