JavaScript 变量作用域深度解析:var、let 与闭包陷阱
前言
在 JavaScript 开发中,变量作用域是一个基础但容易混淆的概念。今天我们就来深入探讨 var、let 的作用域差异,以及那个经典的 for 循环闭包问题。
基础作用域对比
var 的函数作用域
javascript
function checkScope() {
var i = 'function scope';
if (true) {
i = 'block scope'; // 修改的是同一个变量
console.log('Block scope i is: ', i); // block scope
}
console.log('Function scope i is: ', i); // block scope
return i;
}
let 的块级作用域
javascript
function checkScope() {
let i = 'function scope';
if (true) {
let i = 'block scope'; // 不同的变量,只在块内有效
console.log('Block scope i is: ', i); // block scope
}
console.log('Function scope i is: ', i); // function scope
return i;
}
关键区别:
var只有函数作用域,没有块级作用域let有块级作用域,{}内声明的变量外部无法访问
变量提升的差异
var 的变量提升
javascript
console.log(a); // undefined,不会报错
var a = 5;
console.log(a); // 5
// 实际执行顺序:
var a;
console.log(a); // undefined
a = 5;
console.log(a); // 5
let 的暂时性死区
javascript
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;
经典闭包问题解析
问题重现
javascript
// 使用 var - 出现意外结果
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出: 3, 3, 3
}
// 使用 let - 符合预期
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出: 0, 1, 2
}
为什么会出现这种情况?
var 的执行机制
javascript
// 实际执行过程:
var i; // 提升到外部作用域
for (i = 0; i < 3; i++) {
// 创建闭包,但都引用同一个 i
setTimeout(function() {
console.log(i); // 都指向同一个 i
}, 100);
}
// 循环结束时 i = 3
关键点:
- 整个循环只有一个
i变量 - 循环快速执行完毕,
i最终值为 3 - 100ms 后所有回调执行,都读取同一个
i = 3
let 的执行机制
javascript
// 实际执行过程:
// 第一次迭代
{
let i = 0;
setTimeout(() => console.log(i), 100); // 捕获 i=0
}
// 第二次迭代
{
let i = 1;
setTimeout(() => console.log(i), 100); // 捕获 i=1
}
// 第三次迭代
{
let i = 2;
setTimeout(() => console.log(i), 100); // 捕获 i=2
}
关键点:
- 每次迭代创建新的块级作用域
- 每个
setTimeout回调捕获各自迭代中的i - 每个回调都有独立的变量引用
历史解决方案(ES6 之前)
在 let 出现之前,我们使用 IIFE(立即执行函数表达式)来解决:
javascript
// 使用 IIFE 创建闭包
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(() => console.log(index), 100);
})(i); // 立即传入当前的 i 值
}
// 输出: 0, 1, 2
// 或者使用函数参数
for (var i = 0; i < 3; i++) {
setTimeout((function(index) {
return function() {
console.log(index);
};
})(i), 100);
}
实际开发建议
1. 优先使用 const 和 let
javascript
// 推荐
const PI = 3.14;
let count = 0;
// 避免
var globalVar = 'old style';
2. 理解闭包的本质
javascript
function createCounter() {
let count = 0; // 闭包捕获的变量
return {
increment: () => ++count,
getValue: () => count
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
3. 注意循环中的异步操作
javascript
// 正确做法
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', () => {
console.log(`Clicked element ${i}`);
});
}
// 或者使用 forEach
elements.forEach((element, index) => {
element.addEventListener('click', () => {
console.log(`Clicked element ${index}`);
});
});
总结
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 是 | 否(暂时性死区) | 否(暂时性死区) |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许 |
最佳实践:
- 默认使用
const - 需要重新赋值时使用
let - 避免使用
var - 理解闭包和作用域链
- 注意循环中的异步操作
掌握这些概念可以帮助我们写出更 predictable 的代码,避免常见的陷阱。希望这篇笔记对你有所帮助!