javascript

深入理解 JavaScript 中的 闭包、作用域与 var、let 在 for 循环中的行为

作用域决定了变量和函数的可访问性。在 JavaScript 中,作用域分为全局作用域、函数作用域和块级作用域。var和let声明的变量的作用域是不同的,var的作用域是函数作用域,而let的作用域是块级作用域。闭包是指一个函数可以记住并访问它外部函数的变量。即使外部函数已经执行完

2024-12-04·阅读约 9 分钟·计算中...

深入理解 JavaScript 中的 闭包、作用域与 varletfor 循环中的行为

在 JavaScript 中,闭包和作用域是两个非常重要的概念,理解它们对写出高质量的代码至关重要。特别是当你在 for 循环中使用 varlet 时,常常会遇到一些棘手的问题。今天我们将通过一个简单的代码示例,详细解析闭包、作用域以及 varletfor 循环中的不同表现,帮助你避免常见的错误。

示例代码

function createCounters() {
  let counters = [];
  for (var i = 0; i < 3; i++) {
    counters.push(function() {
      return i;
    });
  }
  return counters;
}

const counters = createCounters();
console.log(counters[0]()); // ?
console.log(counters[1]()); // ?
console.log(counters[2]()); // ?

你可能会好奇,为什么上面的代码输出是 3 3 3,而不是 0 1 2?这个问题的关键在于 JavaScript 中的作用域闭包的行为。接下来我们将一步一步解答这一问题。

1. 作用域和闭包的基本概念

什么是作用域?

作用域决定了变量和函数的可访问性。在 JavaScript 中,作用域分为全局作用域、函数作用域和块级作用域。varlet 声明的变量的作用域是不同的,var 的作用域是函数作用域,而 let 的作用域是块级作用域

什么是闭包?

闭包是指一个函数可以记住访问它外部函数的变量。即使外部函数已经执行完毕,闭包仍然可以访问和操作外部函数的变量。这是因为闭包会捕获外部函数的变量引用,而不是值本身。

2. 闭包与 for 循环中的 var

让我们先来看看代码中 for 循环使用 var 的情况。

function createCounters() {
  let counters = [];
  for (var i = 0; i < 3; i++) {
    counters.push(function() {
      return i;
    });
  }
  return counters;
}

const counters = createCounters();
console.log(counters[0]()); // ?
console.log(counters[1]()); // ?
console.log(counters[2]()); // ?
闭包捕获的是变量引用

for 循环中,每次调用 push 时,都会将一个函数推入 counters 数组中。每个函数都形成了一个闭包,捕获了外部变量 i

但是这里的关键是,i 是通过 var 声明的,var 的作用域是函数作用域,而不是块级作用域。这意味着,在整个 for 循环中,所有的闭包共享同一个 i 变量。每次 push 时,并没有立即执行闭包中的函数,而是将它们推入数组中。直到你调用 counters[0](), counters[1](), counters[2](), 这些函数才执行。

此时,由于 i 是在整个 for 循环的作用域中共享的,i 在所有迭代结束后,其值为 3。因此,无论你调用哪个闭包,它们都访问的是同一个 i,并返回 i 的最终值 3

输出结果:

3
3
3

3. letvar 的区别:块级作用域 vs 函数作用域

如果将 var 改为 let,行为将发生显著变化。

function createCounters() {
  let counters = [];
  for (let i = 0; i < 3; i++) {
    counters.push(function() {
      return i;
    });
  }
  return counters;
}

const counters = createCounters();
console.log(counters[0]()); // 0
console.log(counters[1]()); // 1
console.log(counters[2]()); // 2
let 创建了独立的作用域

let 的作用域是块级作用域,每次循环迭代时,i 都会创建一个新的作用域。这样,每个闭包捕获的是自己独立的 i 值,而不是同一个共享的 i

  • 在第一次循环时,闭包捕获的是 i = 0
  • 在第二次循环时,闭包捕获的是 i = 1
  • 在第三次循环时,闭包捕获的是 i = 2

因此,counters[0](), counters[1](), counters[2]() 分别返回 012

输出结果:

0
1
2

4. 使用 IIFE (立即执行函数表达式) 来模拟 let 的作用域

如果我们想继续使用 var,但又希望每个闭包能够捕获独立的 i 值,可以使用 IIFE(立即执行函数表达式)来模拟块级作用域。

function createCounters() {
  let counters = [];
  for (var i = 0; i < 3; i++) {
    (function(j) {
      counters.push(function() {
        return j;
      });
    })(i);  // 传递当前的 i 值给 j
  }
  return counters;
}

const counters = createCounters();
console.log(counters[0]()); // 0
console.log(counters[1]()); // 1
console.log(counters[2]()); // 2
解释:
  • IIFE 是一个立即执行的匿名函数,允许我们在每次循环时,立即执行并将当前的 i 值传递给内部函数的参数 j
  • 每次调用 counters.push 时,j 都会捕获当前的 i 值,而不是 i 的引用。

通过这种方式,我们成功模拟了块级作用域,使得每个闭包捕获的是当前的 i,而不是最后一次循环结束时的值。

输出结果:

0
1
2

5. 总结

通过这篇文章,我们探讨了 JavaScript 中的作用域闭包varlet 的差异,并理解了为什么在 for 循环中使用 var 会导致闭包共享同一个变量引用,而 let 可以解决这个问题。

关键点总结:
  1. var 的作用域是函数作用域,导致所有闭包共享同一个变量 i,因此它们都会返回循环结束时的 i 值。
  2. let 的作用域是块级作用域,确保每次迭代都会创建一个新的变量 i,从而让每个闭包捕获当前的 i 值。
  3. **IIFE(立即执行函数表达式)**可以用来模拟 let 的块级作用域,避免共享变量引用的问题。

掌握这些基础概念后,你会在编写 JavaScript 代码时更加得心应手,避免一些常见的陷阱。如果你有更多问题或疑惑,欢迎继续交流!

订阅 FreeMac

每周精选:Mac 高效技巧、免费替代付费软件、开发者工具推荐。用对你的 MacBook,省钱 + 提效。