一、JS 变量声明的历史背景与设计逻辑
在 ES6(2015)之前,JavaScript 仅提供var作为变量声明关键字,其设计初衷是为了满足动态类型语言的灵活性。但随着前端工程复杂化,var的缺陷逐渐暴露:函数作用域导致的变量污染、变量提升引发的逻辑歧义等。
ES6 引入let和const,旨在解决var的历史问题,推动 JavaScript 向更严谨的模块化开发演进。三者的核心差异可通过以下维度对比:
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 声明提升(值为undefined) | 无提升(存在暂时性死区) | 无提升(存在暂时性死区) |
| 重复声明 | 允许同一作用域内重复声明 | 禁止同一作用域内重复声明 | 禁止同一作用域内重复声明 |
| 值可变性 | 可变(重新赋值 / 重新声明) | 可变(重新赋值,不可重新声明) | 不可变(声明时必须初始化) |
| 应用场景 | 兼容旧项目 | 可变数据(块级作用域) | 常量 / 不可变引用 |
二、var 的核心特性与 "陷阱"
1. 函数作用域与变量提升
// 案例:var的变量提升
console.log(myName); // 输出:undefined(提升的是声明,赋值在执行阶段)
var myName = "Alice";
function foo() {
console.log(bar); // 输出:undefined(函数作用域内提升)
var bar = "hello";
}
foo();
- 提升机制:编译阶段,
var声明的变量会被 "移动" 到作用域顶部,但其初始值为undefined,赋值操作仍在原位置执行。 - 风险点:变量声明与赋值的分离可能导致代码逻辑与阅读顺序不一致,引发 "变量已声明但未赋值" 的隐性 bug。
2. 函数作用域引发的循环陷阱
// 经典问题:var在循环中的表现
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出:3(循环结束后i的值为3,函数作用域共享变量)
}, 100);
}
- 原因:
var声明的i属于函数作用域(此处为全局作用域),循环体内的定时器回调共享同一个i,最终输出循环结束后的最终值。
三、let 的现代特性:块级作用域与暂时性死区
1. 块级作用域的隔离性
// 案例:let的块级作用域
{
let a = 1;
var b = 2;
}
console.log(a); // ReferenceError(块外不可访问let声明的变量)
console.log(b); // 2(var声明的变量属于函数/全局作用域)
- 优势:通过
{}界定作用域,避免变量污染全局环境,适合模块化开发中的作用域隔离。
2. 暂时性死区(TDZ):声明前不可访问
// 案例:暂时性死区
console.log(c); // ReferenceError(c处于TDZ,声明前禁止访问)
let c = 3;
- 机制:
let/const声明的变量在作用域内存在 "声明 - 初始化" 的时间差,声明前的区域称为 TDZ,访问会直接报错。 - 实践意义:强制变量先声明后使用,避免因变量提升导致的逻辑混乱。
四、const 的 "不变性" 本质与应用场景
1. 基本类型:值不可变
// 案例:const声明基本类型
const PI = 3.14;
PI = 3.15; // TypeError(常量不可重新赋值)
2. 引用类型:引用不可变,属性可变
// 案例:const声明对象
const user = { name: "Bob" };
user.name = "Alice"; // 允许(修改对象属性)
user = { name: "Tom" }; // TypeError(禁止重新赋值引用)
- 注意:
const保证的是变量指向的内存地址不变,对于对象 / 数组等引用类型,其内部属性仍可修改。若需完全禁止修改,需配合Object.freeze()等方法。
3. 必须初始化的硬性要求
const age; // SyntaxError(声明时必须赋值)
五、最佳实践:如何选择声明方式?
1. 优先使用const的场景
- 声明常量(如配置项、枚举值):
const API_URL = "https://api.example.com"; - 声明不会被重新赋值的对象 / 数组:
const users = []; users.push("Alice");(仅修改内部数据)。
2. 使用let的场景
-
变量值会发生改变且需限制在块级作用域内:
for (let i = 0; i < 3; i++) { /* 正确写法,每个循环迭代创建独立的i */ }
3. 避免使用var的场景
- 新项目开发中完全摒弃
var,仅在兼容旧代码时保留; - 避免在循环、条件语句中使用
var,防止作用域污染。
六、常见面试题解析
问题 1:变量提升与函数提升的优先级
console.log(foo); // 输出:function foo() { ... }
var foo = 1;
function foo() {
console.log("bar");
}
- 解析:编译阶段,函数声明优先于变量声明提升,变量声明不会覆盖函数声明,但变量赋值会在执行阶段覆盖函数引用。
问题 2:TDZ 与作用域链的结合
const outer = "global";
function fn() {
console.log(outer); // ReferenceError(outer在TDZ内,未声明)
let outer = "local";
}
fn();
- 解析:函数内的
let outer声明会屏蔽外层的outer变量,在声明前访问属于 TDZ 报错,而非访问外层作用域的变量。
七、总结:从 "灵活" 到 "严谨" 的演进
-
var代表 JavaScript 的历史兼容性,但其函数作用域和变量提升特性已成为现代开发的 "糟粕"; -
let通过块级作用域和 TDZ 机制,强制变量声明与使用的一致性,提升代码可维护性; -
const则从设计层面鼓励 "数据不可变" 思维,更符合函数式编程理念,减少意外赋值导致的 bug。
建议:在新项目中遵循 " 能const就不let,坚决不用var" 的原则,结合 ESLint 规则(如no-var)强制规范变量声明,从根源上规避作用域与变量提升的潜在问题。