什么是闭包 (Closure)

2019/8/10 javascript你不知道的 javascript

# 我对闭包的最初认知

  1. 函数foo中返回函数bar,此时就产生了闭包。
function foo() {
  let a = 1;

  function bar() {
    console.log(a);
  }
  // 产生闭包
  return bar;
}
const baz = foo();
baz(); // 1  ---> 这就是闭包的效果
1
2
3
4
5
6
7
8
9
10
11

Closure-1

  1. 闭包就是是:假如一个函数能访问外部的变量,那么就形成了一个闭包,不是一定要返回一个函数。
/** 举个🌰 */
let a = "hello qile";
/**产生闭包 */
function foo() {
  console.log(a);
}
1
2
3
4
5
6

Closure-2 ![]

# 闭包的作用

闭包使得函数可以继续访问定义是的词法作用域

# 闭包的实质

当函数可以记住并访问所在词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

用上面的例子来解释就是,函数bar是在函数foo中定义的,但函数bar确在定义时的词法作用域以外的地方被调用了。

最后它调用之后可以打印出foo中的变量a

# 循环和闭包

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

从上面的代码行为上预期是分别输出数字 1 ~ 5 ,每秒一次,每次一个。但实际上可能不是这样

tu

从图片上可以看到,上面的代码在运行是会以每秒一次的频率输出五次 6。这个 6 是循环终止条件首次命中时 i 的值。

因为定时器中的回调函数是在循环结束后才会被执行,因为同步代码执行完之后才会去执行微任务和宏任务。

那么这个缺陷的原因是啥呢?其实就是我们想在每次迭代的时候去捕获一个 i 当时的副本,但是根据作用域的工作原理,实际情况是尽管

循环中的 5 个定时回调函数是分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此它们打印的 i 就是最后一次循环条件被终止时的 i。

# 如何解决上面代码的缺陷

  1. 给每个回调函数都产生一个闭包作用域
for (var i = 1; i <= 5; i += 1) {
  // 使用 IIFE (立即调用函数)为每一个迭代都产生一个新的作用域。使得延迟函数的回调
  // 可以将新的作用域封闭在每个迭代的内部,每个迭代中都会含有一个具有正确值的 i 供访问。
  (function (j) {
    setTimeout(() => {
      console.log(j);
    }, j * 1000);
  })(i);
}
1
2
3
4
5
6
7
8
9

tu

  • 使用块作用域
// let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
// for 循环头部使用 let 声明会产生一种特殊行为,会让 i 在每次迭代时都会重新声明
// 而且之后的每个迭代都会用上一次迭代结束时的值来初始化 i 这个变量
for (let i = 1; i <= 5; i += 1) {
  setTimeout(() => {
    console.log(i);
  }, i * 1000);
}
1
2
3
4
5
6
7
8

tu

Last Updated: 2022/8/15 上午1:01:16