JavaScript 变量声明:你真的懂var、let 和 const 吗
核心区别总结
作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
变量提升 | 提升并初始化为undefined | 提升但不初始化(TDZ) | 提升但不初始化(TDZ) |
重新赋值 | 允许 | 允许 | 不允许 |
重复声明 | 允许 | 不允许 | 不允许 |
循环中的行为 | 共享同一个变量 | 每次迭代创建新变量 | 不能用于传统for循环 |
作用域详解
1. 函数作用域 (var)
function test() { var x = 1; if (true) { var x = 2; // 同一个变量 } console.log(x); // 2 }
2. 块级作用域 (let/const)
function test() { let x = 1; if (true) { let x = 2; // 不同的变量 } console.log(x); // 1 }
暂时性死区 (TDZ)
console.log(a); // undefined (var) var a = 1; console.log(b); // ReferenceError (let/const在TDZ) let b = 2;
循环中的表现
1. 传统for循环
// var - 有问题 for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // 3,3,3 } // let - 正确 for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // 0,1,2 } // const - 报错 for (const i = 0; i < 3; i++) {} // TypeError
2. for...of/in循环
// 数组遍历 const arr = [1, 2, 3]; for (const item of arr) { console.log(item); // 1,2,3 } // 对象遍历 const obj = {a:1, b:2}; for (const key in obj) { console.log(key); // 'a','b' }
最佳实践
- 默认使用 const用于所有不需要重新赋值的变量更安全,意图更明确
- 需要重新赋值时使用 let循环计数器需要后续修改的变量
- 永远不要使用 var只在维护旧代码时使用新项目完全避免
- 异步代码特别注意
常见问题解答
Q: 为什么const声明的对象可以修改属性?A: const只保证变量绑定不变,对象内容可以修改。若要完全不可变,使用Object.freeze()
Q: 什么时候必须用let?A: 1) 需要重新赋值的变量 2) 传统for循环的计数器
Q: TDZ是什么?A: 暂时性死区(Temporal Dead Zone),指从进入作用域到变量声明之间的区域,访问会报错
记忆口诀
- "var会提升,let会报错"
- "const不变,let可变"
- "循环异步,let是必须"
- "遍历数据,const优先"
掌握这些区别和最佳实践,可以避免JavaScript中大多数变量相关的问题!
var let 和const
var 函数作用域
函数作用域:
- 由函数创建的作用域
- 使用
var
声明的变量具有函数作用域 - 在整个函数内部都可用,无论声明在函数的哪个位置
块级作用域:
- 由代码块(
{}
)创建的作用域 - 使用
let
和const
声明的变量具有块级作用域 - 只在声明它的代码块内可用
let块级作用域 定义时候可以不初始化
const 块级作用域 定义时候要初始化
三种变量提升的场景 function example() { console.log(x); // 输出 undefined 而不是报错 变量提升但是不赋值 var x = 5; } { console.log(y); // 暂时性死区 报错ReferenceError: Cannot access 'y' before initialization let y = 10; } { console.log(y); // 暂时性死区 报错ReferenceError: Cannot access 'y' before initialization const y = 10; }
三个错误使用const的场景 for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出:3, 3, 3 异步共享变量 for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出:0, 1, 2 for (const i = 0; i < 3; i++) { // TypeError: Assignment to constant variable setTimeout(() => console.log(i), 100); } // 直接报错! for (const i of [0, 1, 2]) { setTimeout(() => console.log(i), 100); } // 输出:0, 1, 2 <button id="btn0">按钮0</button> <button id="btn1">按钮1</button> <button id="btn2">按钮2</button> <script> // 用 var 的错误写法 for (var i = 0; i < 3; i++) {//一开始就运行完了 i = 3 document.getElementById(`btn${i}`).addEventListener('click', () => { console.log(`点击了按钮${i}`); //点击按钮时候才输出 }); } // 无论点击哪个按钮,都会输出"点击了按钮3" </script>
使用 const 的场景:当且仅当循环变量在迭代过程中不会被重新赋值 1. for...of 循环中使用 const javascript const arr = [10, 20, 30]; for (const item of arr) { console.log(item); // 输出 10, 20, 30 } 为什么可以用 const? for...of 循环在每次迭代时都会创建一个新的 item 绑定 你不会(也不能)在循环体内修改 item 的值 每次迭代的 item 都是一个全新的常量 错误尝试: javascript for (const item of [1,2,3]) { item = item * 2; // TypeError: Assignment to constant variable } 2. for...in 循环中使用 const javascript const obj = {a: 1, b: 2}; for (const key in obj) { console.log(key); // 输出 'a', 'b' } 为什么可以用 const? 每次迭代都会创建一个新的 key 绑定 你通常不需要修改遍历得到的键名 如果你尝试修改会报错