手把手教会javascript 中的‘变量提升’

65 阅读6分钟

彻底搞懂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的全面对比

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升否(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

六、为什么会有变量提升?

变量提升的存在主要有以下几个原因:

  1. 历史设计遗留 :JavaScript在1995年由Brendan Eich快速设计,采用了宽松的语法规则。
  2. 支持函数先使用后声明 :符合人类的自然思维方式(先使用后定义)。
  3. 解释执行模型的需要 :JavaScript引擎在执行前需要了解所有变量和函数的存在,避免频繁报错。
  4. 向后兼容性 :如果移除变量提升,大量旧代码将无法正常运行。

七、最佳实践:如何写出更可靠的代码

  1. 优先使用const :对于不改变的值,使用 const 可以提高代码的可读性和安全性。
  2. 其次使用let :对于需要重新赋值的变量,使用 let 。
  3. 尽量避免使用var : var 的函数作用域和变量提升容易导致bug。
  4. 始终在变量声明后使用 :无论使用哪种声明方式,都应该在声明后再使用,避免暂时性死区。
  5. 使用块级作用域 :利用 let 和 const 的块级作用域,将变量限制在需要的范围内。
  6. 函数声明优先于函数表达式 :在需要函数提升的场景下,使用函数声明;否则使用箭头函数。

八、总结

变量提升是JavaScript早期设计的产物,它允许变量和函数声明在代码执行前被提升到作用域顶部。ES6引入的 let 和 const 关键字,通过 块级作用域 和 暂时性死区 ,改变了这一行为,使得JavaScript的变量声明更加严格和安全。

理解变量提升的原理,有助于我们编写更可靠、更易维护的代码。在实际开发中,我们应该优先使用 const 和 let ,遵循"先声明后使用"的原则,避免变量提升带来的潜在问题。

参考资料 :

  • MDN Web Docs:变量提升
  • ECMAScript 6 入门:let和const命令

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发~