闭包是怎么产生的?

在处于活动状态的执行上下文环境中创建了一个新的执行上下文,并且新的执行上下文中引用了除自身以外的变量的时候,就会产生一个闭包。

下面通过代码来理解一下上面这句话。

示例一

  1. function foo() {
  2. var str = "你好🔥";
  3. function log() {
  4. console.log(str);
  5. }
  6. log(str)
  7. }
  8. foo();

以上代码就会产生一个闭包,闭包中会保存对 str 变量的引用,怎么证实呢?我们通过浏览器来断点看一下。
我们在代码第 4 行位置加一个断点,来看一下执行上下文中的内容。
image.png
我们可以看到 str 变量是保存在 Closure 中,Closure 是关闭/封闭的意思,它其实就是闭包,由此就可以证实,闭包确实被创建了。

接下来我们再来看另外两个也会产生闭包对例子。

示例二

  1. function sum(arr) {
  2. return function () {
  3. return arr.reduce((a, b) => a + b)
  4. }
  5. }
  6. console.log(sum([1, 2, 3])()); // 6

这个例子和上面的例子基本一致,只不过函数被 return 出去了。
image.png
可以看到依然创建了闭包。

示例三

  1. <ul>
  2. <li>1</li>
  3. <li>2</li>
  4. <li>3</li>
  5. <li>4</li>
  6. <li>5</li>
  7. <li>6</li>
  8. <li>7</li>
  9. <li>8</li>
  10. <li>9</li>
  11. <li>10</li>
  12. </ul>
  13. <script>
  14. function start() {
  15. var liEls = document.getElementsByTagName("li");
  16. for (var i = 0; i < liEls.length; i++) {
  17. liEls[i].onclick = function () {
  18. console.log(i);
  19. }
  20. }
  21. }
  22. start();
  23. </script>

这可以说是一个很经典对示例,我们暂且先不考虑 i 的取值问题,先来看一下 i 是否也被保存在来闭包中,同样我们来断点调试一下。
image.png
当 li 元素被点击时,触发断点,我们看到 i 也是在 Closure 中,所以这段代码的执行也创建了闭包。

怎么避免闭包

有些时候我们不希望产生闭包,因为如果使用不当的话,闭包确实会产生一些性能问题,通过上面几个例子,我们进行一些改造,来看看怎么避免产生闭包。

示例一

  1. function foo() {
  2. var str = "你好🔥";
  3. function log(str) { // 通过参数形式将 str 传入
  4. console.log(str);
  5. }
  6. log(str)
  7. }
  8. foo();

我们再来断点看一下
image.png
可以看到,这次没有产生闭包,因为我们把 str 以参数的形式传入到 log 执行上下文的 AO(活动对象)中了。

通过上面几个例子基本可以看出,只要函数发生嵌套,并且函数内引用了外部函数的变量,就会产生闭包。

示例二

  1. function sum(arr) {
  2. return function () {
  3. return arr.reduce((a, b) => a + b)
  4. }
  5. }
  6. var data = [1, 2, 3];
  7. var tmp = sum(data);
  8. console.log(tmp()); // 6

示例一的方法在示例二中不太适用,因为我们的应用场景就是要先把数组传入 sum 函数中,等需要等时候在进行计算,所以我们就没有办法使用示例一中的方法了,既然产生闭包是必然的,那么我们就考虑如何避免闭包产生的内存泄漏问题呢?

怎么使用闭包

闭包其实没有平常说的那么可怕,只要平时使用得当,是完全没有问题的。而且闭包还会给我们带来很多便利,接下来说一下闭包的一些使用场景。