深入理解 JavaScript 中的 闭包、作用域与 var、let 在 for 循环中的行为
在 JavaScript 中,闭包和作用域是两个非常重要的概念,理解它们对写出高质量的代码至关重要。特别是当你在 for 循环中使用 var 和 let 时,常常会遇到一些棘手的问题。今天我们将通过一个简单的代码示例,详细解析闭包、作用域以及 var 和 let 在 for 循环中的不同表现,帮助你避免常见的错误。
示例代码
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 中,作用域分为全局作用域、函数作用域和块级作用域。var 和 let 声明的变量的作用域是不同的,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. let 与 var 的区别:块级作用域 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]() 分别返回 0、1 和 2。
输出结果:
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 中的作用域、闭包、var 和 let 的差异,并理解了为什么在 for 循环中使用 var 会导致闭包共享同一个变量引用,而 let 可以解决这个问题。
关键点总结:
var的作用域是函数作用域,导致所有闭包共享同一个变量i,因此它们都会返回循环结束时的i值。let的作用域是块级作用域,确保每次迭代都会创建一个新的变量i,从而让每个闭包捕获当前的i值。- **IIFE(立即执行函数表达式)**可以用来模拟
let的块级作用域,避免共享变量引用的问题。
掌握这些基础概念后,你会在编写 JavaScript 代码时更加得心应手,避免一些常见的陷阱。如果你有更多问题或疑惑,欢迎继续交流!
订阅 FreeMac
每周精选:Mac 高效技巧、免费替代付费软件、开发者工具推荐。用对你的 MacBook,省钱 + 提效。