ES6 引入了 let 和 const 两个新的变量声明关键字,彻底改变了 JavaScript 的变量作用域规则。
🎯 var 的问题#
先看 var 存在的问题:
// 问题1:变量提升console.log(name) // undefined,而不是报错var name = '张三'
// 问题2:没有块级作用域for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100)}// 输出:3, 3, 3(而不是 0, 1, 2)
// 问题3:可以重复声明var count = 1var count = 2 // 不报错,容易造成 buglet 声明#
基本用法#
let age = 25age = 26 // 可以重新赋值
let username = '李四'// let username = '王五'; // SyntaxError: 不能重复声明块级作用域#
let 声明的变量只在当前代码块内有效:
{ let a = 10 var b = 20}// console.log(a); // ReferenceError: a is not definedconsole.log(b) // 20经典的循环问题用 let 轻松解决:
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100)}// 输出:0, 1, 2 ✅暂时性死区(TDZ)#
从块的开始到 let 声明之间的区域叫做”暂时性死区”(Temporal Dead Zone):
// TDZ 开始console.log(x) // ReferenceError: Cannot access 'x' before initializationlet x = 10 // TDZ 结束🔶 TDZ 的存在让代码更加可预测,强制你先声明后使用。
一个隐蔽的 TDZ 场景:
function foo(x = y, y = 2) { return [x, y]}foo() // ReferenceError: y 在 TDZ 中const 声明#
基本用法#
const 声明一个只读常量,必须在声明时初始化:
const PI = 3.14159// PI = 3.14; // TypeError: Assignment to constant variable
// const MAX; // SyntaxError: Missing initializer对象和数组#
🤔 const 保证的是变量指向的内存地址不变,而不是值不变:
const user = { name: '张三' }user.name = '李四' // ✅ 可以修改属性user.age = 25 // ✅ 可以添加属性// user = {}; // ❌ TypeError: 不能重新赋值
const list = [1, 2, 3]list.push(4) // ✅ 可以修改数组内容// list = []; // ❌ TypeError如果需要真正不可变的对象,使用 Object.freeze():
const frozen = Object.freeze({ name: '张三' })frozen.name = '李四' // 静默失败,严格模式下报错console.log(frozen.name) // '张三'
// 注意:freeze 是浅冻结const nested = Object.freeze({ info: { age: 25 },})nested.info.age = 30 // 仍然可以修改console.log(nested.info.age) // 30深度冻结需要递归处理:
function deepFreeze(obj) { Object.keys(obj).forEach((key) => { if (typeof obj[key] === 'object' && obj[key] !== null) { deepFreeze(obj[key]) } }) return Object.freeze(obj)}
const config = deepFreeze({ api: { url: 'https://api.example.com' },})config.api.url = 'xxx' // 无效块级作用域的应用#
替代 IIFE#
ES5 时代用 IIFE 创建私有作用域:
// ES5;(function () { var private = '私有变量'})()ES6 直接用块:
// ES6{ let private = '私有变量'}条件声明#
function process(condition) { if (condition) { let result = '处理结果' return result } // result 在这里不可访问,避免意外使用 return null}switch 语句#
// 🔶 没有块作用域会报错switch (type) { case 'A': let msg = 'Type A' // 报错:重复声明 break case 'B': let msg = 'Type B' break}
// ✅ 正确写法:用块包裹switch (type) { case 'A': { let msg = 'Type A' console.log(msg) break } case 'B': { let msg = 'Type B' console.log(msg) break }}全局变量#
var 和 function 声明的全局变量会成为 window 的属性,let 和 const 不会:
var globalVar = 'var'let globalLet = 'let'const globalConst = 'const'
console.log(window.globalVar) // 'var'console.log(window.globalLet) // undefinedconsole.log(window.globalConst) // undefined最佳实践#
1. 默认使用 const#
// ✅ 推荐const config = { debug: true }const API_URL = 'https://api.example.com'const numbers = [1, 2, 3]
// 只有需要重新赋值时才用 letlet count = 0count++2. 不再使用 var#
// ❌ 避免var name = '张三'
// ✅ 推荐const name = '张三'3. 在块的顶部声明#
function process(items) { // 变量声明放在块的顶部 const result = [] let total = 0
for (const item of items) { result.push(item.value) total += item.value }
return { result, total }}4. 循环中的选择#
// for...of 和 for...in 使用 constfor (const item of items) { console.log(item)}
for (const key in obj) { console.log(key)}
// 传统 for 循环使用 letfor (let i = 0; i < 10; i++) { console.log(i)}常见问题#
🙋 let 和 const 有变量提升吗?#
有,但行为不同。它们会被提升到块的顶部,但在声明之前处于 TDZ,访问会报错:
// JavaScript 引擎知道 x 存在,但不让你访问console.log(x) // ReferenceErrorlet x = 1🙋 什么时候用 let?#
只有在需要重新赋值时才用 let:
// 计数器let count = 0items.forEach(() => count++)
// 累加器let sum = 0for (const num of numbers) { sum += num}
// 状态标记let isReady = false// ... 某些操作后isReady = true🙋 const 声明的对象如何实现不可变?#
// 方案1:Object.freeze()(浅冻结)const obj = Object.freeze({ a: 1 })
// 方案2:深冻结函数// 方案3:使用 Immutable.js 等库// 方案4:TypeScript 的 Readonly 类型(编译时检查)总结对比#
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 是(初始化为 undefined) | 是(但有 TDZ) | 是(但有 TDZ) |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许 |
| 全局属性 | 是(挂载到 window) | 否 | 否 |
| 必须初始化 | 否 | 否 | 是 |
结论:默认使用 const,需要重新赋值时使用 let,不再使用 var。