很多前端开发者写了几年 JS,依然搞不懂:

  • 为什么变量先使用后声明不报错,只会输出 undefined?
  • 为什么函数可以先调用、后声明?
  • 为什么 var 和 let 提升行为完全不一样?
  • 为什么 JS 代码看起来不是逐行执行?

所有诡异现象的根源,都来自 JS 编译阶段 + 变量提升机制 + 执行上下文

本文从零底层拆解 JS 真正的执行流程,彻底吃透变量提升本质、编译阶段、执行阶段、词法环境与变量环境,彻底告别“凭感觉写代码”。

变量提升(Hoisting)核心概念

变量提升与函数提升
JavaScript 并不是逐行解释执行那么简单。在代码真正运行前,JS 引擎会先进行一次预编译扫描

变量提升定义:在编译阶段,JS 引擎会将 var 变量声明、函数声明提前提升到当前作用域顶部。

1)普通变量提升(var
只提升声明,不提升赋值,默认初始值为 undefined
在这里插入图片描述
2)函数声明提升
整体提升,声明 + 函数体全部提升,值为完整函数对象,因此可以先调用后定义。
3)函数是一等对象
在 JS 中,函数是一等公民,可以被赋值、传递、作为参数、作为返回值,所以函数提升优先级高于普通变量。

JS 到底是怎么运行的?

传统认知误区:代码按顺序执行
我们初学 JS 都会认为:代码从上到下、逐行依次执行。
如果完全按顺序执行,下面代码应该直接报错:

showName();
console.log(myName);
var myName = '极客时间';
function showName(){
  console.log('showName被调用');
}

按“逐行执行”逻辑:

  • 第一行调用函数,还没定义 → 应该报错
  • 第二行打印变量,还没定义 → 应该报错

但真实结果是:不报错
输出:

函数showName被执行了
undefined

三个反直觉现象(理解 JS 的关键

这三个现象直接证明:JS 不是单纯逐行执行

  1. 未声明直接使用变量 → 直接报错 is not defined
  2. var 变量先使用、后声明 → 不报错,值为 undefined
  3. 函数先调用、后声明 → 不报错,正常执行

结论:JS 执行分为两个阶段 ——编译阶段 + 执行阶段

变量提升真实发生阶段:编译阶段

JS 真的有编译阶段吗?
JS 是弱类型动态脚本语言,没有 Java、C++ 那种独立的提前编译过程。
但是!JS 在每一次代码运行前的一瞬间,会由 V8 引擎完成预编译
变量提升全部发生在预编译阶段,执行阶段只负责逐行运行代码


变量提升官方定义(V8 引擎机制

变量提升:JS 引擎在预编译阶段,自动将 var 变量声明、函数声明提前解析、分配内存,相当于逻辑上提升到作用域顶部。
注意:

  • 代码物理位置不会移动
  • 只是内存提前分配、声明提前注册

JS 完整执行流程:编译阶段 → 执行阶段

错误理解
错误说法:变量提升会把代码物理移动到最前面。
真相:代码位置完全不变,只是编译阶段提前创建内存环境


真正的执行流程
任何一段 JS 代码执行,永远是两步:

  1. 编译阶段(预编译
  • 语法检查
  • 创建执行上下文
  • 变量、函数提升,分配内存
  1. 执行阶段
  • 从上到下逐行执行代码
  • 赋值、运算、调用逻辑

编译阶段生成的两大核心环境

在这里插入图片描述

编译结束后,引擎会生成两样东西:

  1. 执行上下文(Execution Context):代码运行环境
  2. 可执行代码:真正逐行运行的代码

每调用一个函数,就会生成一个全新的函数执行上下文在这里插入图片描述


变量环境 VariableEnvironment
存储 var 变量、函数声明,支持变量提升,声明前可访问,值为 undefined。
示例编译后的变量环境:

VariableEnvironment:
    myName -> undefined
    showName -> 函数对象

词法环境 LexicalEnvironment

重点
词法环境中的变量 没有变量提升,存在暂时性死区
在声明之前无法访问,一旦访问直接报错:

console.log(myName); // 报错:Cannot access 'myName' before initialization
let myName = "极客时间";

完整案例演示:还原真实提升过程

经典案例:全局变量提升
原代码:

showName();
console.log(myName);
var myName = '极客时间';
function showName(){
    var a = 1;
    console.log('函数showName被执行');
}

编译阶段(引擎暗中做的事

// var 变量提升
var myName = undefined;
// 函数整体提升
function showName(){
    var a = 1;
    console.log('showName被调用');
}

// 下面才是执行阶段逐行代码
showName();
console.log(myName);
myName = '极客时间';


完美解释输出结果:

  • 函数提前存在,直接调用执行成功
  • myName 已提升,值为 undefined,不报错

函数声明 vs 函数表达式 对比

// 函数声明:可提升
foo(); // foo
function foo(){
  console.log('foo');
}

// 函数表达式:只有变量提升
console.log(bar); // undefined
bar(); // 报错
var bar = function(){
  console.log('bar');
}

在这里插入图片描述
核心区别

  • 函数声明:无赋值操作,整体提升
  • 函数表达式:本质是 var 变量赋值,只提升变量,赋值留在原地

var 与 let 提升本质区别

var 的提升机制
变量存入 变量环境,编译阶段分配内存、赋值 undefined,声明前可访问。


let/const 的提升机制
变量存入 词法环境,不初始化默认值,进入暂时性死区,禁止提前访问。
很多人说“let 没有提升”,这句话不完全准确
准确说法:

  • let 在编译阶段也会扫描、占用内存
  • 不初始化、不可访问,形成暂时性死区
  • 必须等到执行到声明行,才能解除死区、正常使用

终极对比代码

// var 版本
console.log(a); // undefined
var a = 10;

// let 版本
console.log(b); // 直接报错
let b = 20;

全文总结

JS 代码并不是简单的从上到下逐行执行,而是分为编译预编译阶段和代码执行阶段。变量提升、函数提升、暂时性死区等所有诡异语法行为,全部来自预编译阶段的内存分配机制。

var 存在变量提升、挂载变量环境、允许声明前访问;let/const 挂载词法环境、存在暂时性死区、禁止提前访问。函数声明整体提升,函数表达式仅变量提升,这也是日常开发的核心考点。

理解执行上下文、变量环境、词法环境,是掌握 JS 底层原理的必经之路。

核心知识点复盘

  1. JS 执行分为 编译预编译阶段逐行执行阶段
  2. 变量提升本质:编译阶段提前分配内存、注册声明,代码物理位置不变
  3. var 变量提升:仅声明提升,默认值 undefined
  4. 函数声明提升:整体提升,可先调用后定义
  5. 函数表达式不提升函数体,只有变量提升
  6. var 属于变量环境,let/const 属于词法环境
  7. 词法环境存在暂时性死区,声明前访问直接报错
  8. 执行上下文是每一段代码、每一个函数的运行环境

常见问题 & 避坑指南

  1. 为什么先使用 var 变量不报错? 编译阶段已提升并赋值为 undefined
  2. 为什么 let 提前使用直接报错? 词法环境暂时性死区机制保护
  3. 为什么函数可以先调用? 函数声明整体提升,内存已存在完整函数
  4. 函数表达式为什么不能提前调用? 只有变量提升,函数对象还未赋值
  5. 不要用 var:变量提升导致逻辑混乱、变量污染、可读性差
  6. 报错 is not defined:完全未声明,无任何提升
  7. 输出 undefined:var 变量已提升但未赋值

更多推荐