彻底搞懂JavaScript变量提升:从var到let/const的进化
作为前端开发者,你一定遇到过这样的代码:
console.log(a); // undefined
var a = 10;
或者这样的:
sayHello(); // "Hello!"
function sayHello() {
console.log("Hello!");
}
为什么在变量声明之前访问它不会报错?为什么函数可以在声明之前调用?这就是JavaScript中著名的 变量提升 (Hoisting)现象。而ES6引入的 let 和 const 关键字,又彻底改变了这一行为。今天,我们就来彻底搞懂变量提升的原理,以及 let 和 const 为什么不会提升。
一、什么是变量提升?
变量提升 是JavaScript引擎在执行代码之前,将变量和函数声明提升到其所在作用域顶部的现象。
1.1 核心特点
- 只有声明会被提升 ,赋值不会
- 函数声明比变量声明优先级更高
- 只提升到所在作用域的顶部
二、var的变量提升:一个经典案例
让我们从一个简单的例子开始:
console.log(a); // 输出:undefined
var a = 10;
2.1 执行过程分解
JavaScript引擎在执行代码前,会进行 预解析 (编译阶段),将变量声明提升到作用域顶部。上面的代码在预解析后,会被JavaScript引擎理解为:
var a; // 声明被提升到作用域顶部
console.log(a); // 此时a已声明但未赋值,所以输出undefined
a = 10; // 赋值操作留在原地执行
2.2 图解var的执行上下文
为了更好地理解,我们用图解方式展示执行上下文的生命周期:
┌─────────────────────────────────────────┐
│ 执行上下文创建阶段 (Creation Phase) │
│ 1. 创建变量对象(VO) │
│ - 添加var声明的变量a,初始化为undefined│
│ - 添加函数声明(如果有) │
│ 2. 建立作用域链 │
│ 3. 确定this指向 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 执行上下文执行阶段 (Execution Phase) │
│ 1. 执行console.log(a) → undefined │
│ 2. 执行a = 10 → a的值变为10 │
└─────────────────────────────────────────┘
三、函数声明的提升:优先级高于变量
不仅变量会提升,函数声明也会提升,而且 优先级更高 。
3.1 函数声明可以先调用后声明
sayHello(); // 输出:Hello!
function sayHello() {
console.log("Hello!");
}
3.2 函数表达式不会被提升
注意区分 函数声明 和 函数表达式 :
sayHi(); // 报错:TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi!");
};
这里的 sayHi 是一个变量,它的值是一个函数表达式。变量声明 var sayHi 会被提升,但赋值操作不会,所以 sayHi 在声明前是 undefined ,调用它会报错。
四、ES6的革命:let和const的诞生
ES6引入了 let 和 const 关键字,它们具有 块级作用域 ,并且 不会发生传统意义上的变量提升 。
4.1 暂时性死区(TDZ)
当使用 let 或 const 声明变量时,变量会被"创建",但不会被"初始化",直到执行到声明语句。在声明之前访问变量,就会进入 暂时性死区 (Temporal Dead Zone,TDZ),导致报错。
console.log(b); // 报错:ReferenceError: Cannot access 'b' before
initialization
let b = 20;
4.2 图解let的执行过程
与 var 不同, let 声明的变量在创建阶段不会被初始化为 undefined ,而是处于未初始化状态:
┌─────────────────────────────────────────┐
│ 执行上下文创建阶段 (Creation Phase) │
│ 1. 创建变量对象(VO) │
│ - 添加let声明的变量b,处于未初始化状态 │
│ 2. 建立作用域链 │
│ 3. 确定this指向 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 执行上下文执行阶段 (Execution Phase) │
│ 1. 执行console.log(b) → 进入TDZ,报错 │
│ 2. 执行let b = 20 → b被初始化并赋值为20 │
└─────────────────────────────────────────┘
4.3 const的特性
const 与 let 类似,也具有块级作用域和暂时性死区,但它还有一个重要特性: 声明的变量不能重新赋值 。
const c = 30;
c = 40; // 报错:TypeError: Assignment to constant variable.
注意 : const 声明的对象,其属性可以修改,因为 const 只保证变量指向的内存地址不变,而不保证对象本身不变。
const obj = { name: "张三" };
obj.name = "李四"; // 允许,对象属性可以修改
obj = {}; // 报错,不能重新赋值
五、var、let、const的全面对比
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 是 | 否(TDZ) | 否(TDZ) |
| 重复申明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 不允许 | 不允许 |
| 初始值 | 可选 | 可选 | 必须 |
| 暂时性死区 | 无 | 有 | 有 |
5.1 块级作用域的威力
// var的函数作用域
if (true) {
var x = 10;
}
console.log(x); // 输出:10(x泄露到外部作用域)
// let的块级作用域
if (true) {
let y = 20;
}
console.log(y); // 报错:ReferenceError: y is not defined(y被限制在
块级作用域内)
5.2 重复声明的处理
// var允许重复声明
var a = 10;
var a = 20; // 不会报错,a的值变为20
// let不允许重复声明
let b = 10;
let b = 20; // 报错:SyntaxError: Identifier 'b' has already been
declared
六、为什么会有变量提升?
变量提升的存在主要有以下几个原因:
- 历史设计遗留 :JavaScript在1995年由Brendan Eich快速设计,采用了宽松的语法规则。
- 支持函数先使用后声明 :符合人类的自然思维方式(先使用后定义)。
- 解释执行模型的需要 :JavaScript引擎在执行前需要了解所有变量和函数的存在,避免频繁报错。
- 向后兼容性 :如果移除变量提升,大量旧代码将无法正常运行。
七、最佳实践:如何写出更可靠的代码
- 优先使用const :对于不改变的值,使用 const 可以提高代码的可读性和安全性。
- 其次使用let :对于需要重新赋值的变量,使用 let 。
- 尽量避免使用var : var 的函数作用域和变量提升容易导致bug。
- 始终在变量声明后使用 :无论使用哪种声明方式,都应该在声明后再使用,避免暂时性死区。
- 使用块级作用域 :利用 let 和 const 的块级作用域,将变量限制在需要的范围内。
- 函数声明优先于函数表达式 :在需要函数提升的场景下,使用函数声明;否则使用箭头函数。
八、总结
变量提升是JavaScript早期设计的产物,它允许变量和函数声明在代码执行前被提升到作用域顶部。ES6引入的 let 和 const 关键字,通过 块级作用域 和 暂时性死区 ,改变了这一行为,使得JavaScript的变量声明更加严格和安全。
理解变量提升的原理,有助于我们编写更可靠、更易维护的代码。在实际开发中,我们应该优先使用 const 和 let ,遵循"先声明后使用"的原则,避免变量提升带来的潜在问题。
参考资料 :
- MDN Web Docs:变量提升
- ECMAScript 6 入门:let和const命令
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发~