在 JavaScript 的发展历程中,变量声明机制经历了从混乱到规范的重大演进。早期的 JavaScript(ES5 及之前)仅提供 var 关键字来声明变量,但其行为常常与开发者的直觉相悖,尤其是在变量提升(Hoisting)和作用域 方面。随着 ES6(ECMAScript 2015)的发布,let 和 const 被引入,极大地改善了语言的可读性、可维护性和安全性,使 JavaScript 更适合大型企业级项目的开发。
本文将结合代码实例,深入剖析 var、let、const 的核心差异,帮助开发者理解其底层机制,并掌握最佳实践。
一、var:被时代淘汰的变量声明方式
1. 变量提升(Hoisting)------"编译阶段"与"执行阶段"的分离
var 最令人困惑的特性是变量提升 。JavaScript 引擎在执行代码前会进行一个"编译"阶段,此时会收集所有 var 声明的变量,并将它们提升到当前作用域的顶部。
ini
console.log(age); // 输出:undefined
var age = 18;
这段代码的实际执行过程如下:
javascript
// 编译阶段:变量提升
var age; // 声明被提升,但未赋值
// 执行阶段
console.log(age); // 此时 age 存在但值为 undefined
age = 18; // 赋值
这种行为导致了"提前访问但值为 undefined"的问题,容易引发难以调试的 bug。
2. 缺乏块级作用域
var 声明的变量只具有函数作用域 或全局作用域 ,不支持块级作用域(如 {} 内的作用域)。
ini
{
var age = 18;
let height = 188;
}
console.log(age); // 输出:18 ------ var 变量在块外仍可访问
console.log(height); // 报错:ReferenceError: height is not defined
height 使用 let 声明,受限于块级作用域,无法在块外访问,而 age 使用 var 声明,其作用域被提升至全局,破坏了代码的封装性。
报错:ReferenceError: height is not defined
该报错是因为 试图访问一个在当前作用域中不存在的变量 height,这是因为在块级作用域外访问了 let/const 声明的变量。
二、let:现代 JavaScript 的变量声明标准
let 的引入解决了 var 的两大痛点:变量提升带来的可读性问题 和 缺乏块级作用域。
1. 暂时性死区(Temporal Dead Zone, TDZ)
let 声明的变量虽然在编译阶段也会被"提升",但不会被初始化,访问未初始化的 let 变量会抛出 ReferenceError。
ini
console.log(PI); // 报错:ReferenceError: Cannot access 'PI' before initialization
let height = 188;
const PI = 3.1415926;
这被称为暂时性死区------从进入作用域到变量被实际声明之间的区域。在此区域内访问变量是非法的,这迫使开发者遵循"先声明后使用"的编程规范,提高了代码的健壮性。
报错:ReferenceError: Cannot access 'PI' before initialization
该报错是因为 在 const 或 let 声明之前访问了该变量,处于"暂时性死区"(TDZ),JavaScript 禁止在此阶段访问,以保证安全的初始化顺序。
2. 支持块级作用域
let 支持真正的块级作用域,变量仅在声明它的代码块 {} 内有效。
arduino
{
let height = 188;
}
console.log(height); // 报错:ReferenceError: height is not defined
这一特性使得变量的作用范围更加清晰,避免了命名冲突和意外修改,是现代大型项目开发的核心基础。
报错:ReferenceError: height is not defined
该报错是因为 试图访问一个未声明或在当前作用域中不存在的变量 height,这是因为在块级作用域外访问了 let/const 声明的变量。
三、const:声明不可变的常量
const 用于声明一个常量,其值在声明后不可重新赋值。
1. 基本数据类型的不可变性
对于字符串、数字等基本数据类型,const 确保其值不可更改。
ini
const key = 'abc123';
key = 'abc234'; // 报错:TypeError: Assignment to constant variable.
报错:TypeError: Assignment to constant variable.
该报错是因为 尝试给用 const 声明的常量重新赋值,这是不允许的,因为 const 变量一旦初始化就不能再被重新赋值。
2. 引用类型的"引用不可变性"
对于对象、数组等复杂数据类型,const 保证的是引用地址不可变,但可以修改其内部的属性或元素。
ini
const person = {
name: "zjy",
age: 21
};
person.age = 22; // 合法:修改对象内部属性
console.log(person); // { name: "zjy", age: 22 }
person = {}; // 报错:TypeError: Assignment to constant variable.
报错:TypeError: Assignment to constant variable.
该报错是因为 尝试给用 const 声明的常量重新赋值,这是不允许的,因为 const 变量一旦初始化就不能再被重新赋值。
3. 如何真正冻结对象?
如果希望对象完全不可变,可以使用 Object.freeze():
ini
const wes = Object.freeze(person);
wes.age = 17; // 在严格模式下会报错,非严格模式下静默失败
console.log(wes); // 仍为 { name: "zjy", age: 22 }
Object.freeze() 可以深度冻结对象,防止其属性被修改、添加或删除。
四、函数提升:var 与函数声明的对比
JavaScript 中函数是"一等公民",函数声明也会被提升,且连同函数体一起提升。
scss
setWidth(); // 可以正常调用
function setWidth() {
var width = 100;
console.log(width); // 输出:100
}
注意:var 声明的变量只会提升声明(初始化为 undefined),而函数声明会提升整个函数定义,因此可以在声明前调用。
但如果是函数表达式 ,则行为与 var 相同:
scss
getAge(); // 报错:TypeError: getAge is not a function
var getAge = function() {
console.log(18);
};
报错:TypeError: getAge is not a function
调用了一个名为 getAge 的函数,但它实际上是一个未初始化的 var 变量(如函数表达式),由于变量提升导致其值为 undefined,因此无法调用。
五、最佳实践总结
var 、let 、const的主要区别
| 特性 | var |
let |
const |
|---|---|---|---|
| 作用域 | 函数/全局 | 块级 | 块级 |
| 变量提升 | 是(值为 undefined) |
是(进入暂时性死区) | 是(进入暂时性死区) |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许 |
| 推荐使用 | ❌ 不推荐 | ✅ 推荐用于变量 | ✅ 推荐用于常量 |
✅ 推荐做法:
- 永远不要再使用
var。它已被let和const取代。 - 优先使用
const。如果变量不会被重新赋值,就用const。 - 仅在需要重新赋值时使用
let。 - 对复杂数据类型使用
Object.freeze()来确保完全不可变。
为了更好的理解这三者的区别,以下是关于变量提升 、暂时性死区 和块级作用域三个核心概念的对比解析
| 概念 | var 表现 |
let 表现 |
const 表现 |
|---|---|---|---|
| 变量提升 (Hoisting) | ✅ 声明被提升到作用域顶部,初始化为 undefined (可访问,值为 undefined) |
✅ 声明被提升,但不初始化 (进入"暂时性死区",访问报错) | ✅ 声明被提升,但不初始化 (进入"暂时性死区",访问报错) |
| 暂时性死区 (Temporal Dead Zone, TDZ) | ❌ 不存在 (提升后即可访问,值为 undefined) |
✅ 存在 从进入作用域到变量声明前的区域 访问会抛出 ReferenceError |
✅ 存在 规则与 let 相同 在声明前访问会报错 |
| 块级作用域 (Block Scope) | ❌ 不支持 仅函数/全局作用域 在 {} 内声明的 var 变量可在外部访问 |
✅ 支持 变量仅在 {} 块内有效 块外访问会抛出 ReferenceError |
✅ 支持 规则与 let 相同 常量作用域限制在声明的代码块内 |
✅ 概念精要总结:
- 变量提升 :JS 引擎在"编译阶段"收集变量和函数声明,并提升到作用域顶部。
var提升后初始化为undefined,而let/const提升但不初始化,形成"暂时性死区"。 - 暂时性死区 :
let和const变量从进入作用域到被实际声明之间的"禁用区",在此区域内访问变量会抛出ReferenceError,强制开发者遵循"先声明后使用"的良好习惯。 - 块级作用域 :由
{}定义的作用域范围。let和const支持块级作用域,使变量生命周期更清晰,避免命名污染;var不支持,容易导致意外的变量泄漏。
掌握这三个概念,就掌握了现代 JavaScript 变量声明机制的底层逻辑。
针对var 、let 、const 运用时会出现的报错对 ReferenceError 和 TypeError 两种错误类型进行的结构化解析:
| 错误类型 | var 相关典型表现 |
let 相关典型表现 |
const 相关典型表现 |
|---|---|---|---|
| ReferenceError | ✅ 访问未声明的变量 例如:console.log(age); 但从未声明 var age; 或函数表达式提前调用导致 is not a function |
✅ 在声明前访问变量 例如:console.log(height); let height = 188; 触发"暂时性死区"错误 |
✅ 在声明前访问常量 例如:console.log(PI); const PI = 3.14; 同样进入暂时性死区,报错 |
| TypeError | ✅ 调用未赋值的函数表达式 例如:getAge(); var getAge = function() {...}; 输出:TypeError: getAge is not a function |
✅ 尝试重新声明已存在的 let 变量(非赋值) 例如:let a = 1; let a = 2; (严格来说是语法错误,但引擎常报 TypeError 或 SyntaxError) |
✅ 修改常量引用 例如:const key = 'abc'; key = 'def'; 输出:TypeError: Assignment to constant variable. |
✅ 报错类型精要总结:
- ReferenceError :表示"找不到这个变量或标识符",通常是因为变量未声明 、拼写错误 或在
let/const的暂时性死区内提前访问。 - TypeError :表示"操作不被允许",通常是因为试图改变
const的值 、调用尚未赋值的函数表达式,或对数据类型执行了非法操作。
理解这两类错误的触发场景,有助于快速定位 JavaScript 中的声明与执行逻辑问题,尤其是在涉及变量提升与作用域机制时。
结语
JavaScript 的演进,本质上是对开发者心智模型的不断贴近。var 所带来的变量提升与作用域泄漏,曾让无数开发者陷入困惑;而 let 与 const 的引入,则是对"直觉编程"的一次回归------先声明,再使用;块内声明,块内有效;常量一旦定义,便不可更改。
这不仅是语法的更新,更是编程思维的升级。体现了 JavaScript 语言从"脚本语言"向"现代编程语言"的成熟。选择 const 而非 let,选择块级作用域而非函数作用域,是在用代码表达意图,是在构建更可靠、更易维护的系统。在现代 JavaScript 开发中,摒弃 var 不仅是一种技术选择,更是一种对代码质量的承诺。
掌握 let 与 const,意味着我们不再与语言的怪癖对抗,而是借助其设计,写出更清晰、更安全、更具可读性的程序。这才是语言进步的真正意义。