深入解析 JavaScript 变量声明:var、let 与 const 的核心差异与实践指南

94 阅读5分钟

一、JS 变量声明的历史背景与设计逻辑

在 ES6(2015)之前,JavaScript 仅提供var作为变量声明关键字,其设计初衷是为了满足动态类型语言的灵活性。但随着前端工程复杂化,var的缺陷逐渐暴露:函数作用域导致的变量污染、变量提升引发的逻辑歧义等。
ES6 引入letconst,旨在解决var的历史问题,推动 JavaScript 向更严谨的模块化开发演进。三者的核心差异可通过以下维度对比:

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升声明提升(值为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)强制规范变量声明,从根源上规避作用域与变量提升的潜在问题。