JavaScript 执行原理深入理解:彻底搞懂变量提升与编译执行机制
目录
很多前端开发者写了几年 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 不是单纯逐行执行
- 未声明直接使用变量 → 直接报错
is not defined - var 变量先使用、后声明 → 不报错,值为
undefined - 函数先调用、后声明 → 不报错,正常执行
结论:JS 执行分为两个阶段 ——编译阶段 + 执行阶段。
变量提升真实发生阶段:编译阶段
JS 真的有编译阶段吗?
JS 是弱类型动态脚本语言,没有 Java、C++ 那种独立的提前编译过程。
但是!JS 在每一次代码运行前的一瞬间,会由 V8 引擎完成预编译。
变量提升全部发生在预编译阶段,执行阶段只负责逐行运行代码。
变量提升官方定义(V8 引擎机制)
变量提升:JS 引擎在预编译阶段,自动将 var 变量声明、函数声明提前解析、分配内存,相当于逻辑上提升到作用域顶部。
注意:
- 代码物理位置不会移动
- 只是内存提前分配、声明提前注册
JS 完整执行流程:编译阶段 → 执行阶段
错误理解
错误说法:变量提升会把代码物理移动到最前面。
真相:代码位置完全不变,只是编译阶段提前创建内存环境。
真正的执行流程
任何一段 JS 代码执行,永远是两步:
- 编译阶段(预编译)
- 语法检查
- 创建执行上下文
- 变量、函数提升,分配内存
- 执行阶段
- 从上到下逐行执行代码
- 赋值、运算、调用逻辑
编译阶段生成的两大核心环境

编译结束后,引擎会生成两样东西:
- 执行上下文(Execution Context):代码运行环境
- 可执行代码:真正逐行运行的代码
每调用一个函数,就会生成一个全新的函数执行上下文。
变量环境 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 底层原理的必经之路。
核心知识点复盘
- JS 执行分为 编译预编译阶段 和 逐行执行阶段
- 变量提升本质:编译阶段提前分配内存、注册声明,代码物理位置不变
- var 变量提升:仅声明提升,默认值 undefined
- 函数声明提升:整体提升,可先调用后定义
- 函数表达式不提升函数体,只有变量提升
- var 属于变量环境,let/const 属于词法环境
- 词法环境存在暂时性死区,声明前访问直接报错
- 执行上下文是每一段代码、每一个函数的运行环境
常见问题 & 避坑指南
- 为什么先使用 var 变量不报错? 编译阶段已提升并赋值为 undefined
- 为什么 let 提前使用直接报错? 词法环境暂时性死区机制保护
- 为什么函数可以先调用? 函数声明整体提升,内存已存在完整函数
- 函数表达式为什么不能提前调用? 只有变量提升,函数对象还未赋值
- 不要用 var:变量提升导致逻辑混乱、变量污染、可读性差
- 报错 is not defined:完全未声明,无任何提升
- 输出 undefined:var 变量已提升但未赋值
更多推荐

所有评论(0)