明明写在后面的函数,在前面调用却不报错?
明明没定义的变量,打印出来是 undefined?
这背后藏着 JS 代码执行的秘密:编译阶段 + 执行阶段。
一、先看一段"奇怪"的代码
打开浏览器控制台,输入下面这段:
js
ini
showName();
console.log(myName);
var myName = '极客时间';
function showName() {
console.log('函数showName被执行了');
}
你觉得输出是什么?
我一开始猜:第一行报错 showName is not defined,第二行也报错。
结果输出是:
text
javascript
函数showName被执行了
undefined
函数调用成功了,变量打印是 undefined 而不是报错。
这不符合"一行一行顺序执行"的直觉。
再改一下:
js
javascript
console.log(add);
var add = function(x, y) { return x + y; };
输出 undefined,而不是函数体。
同样声明函数,function showName(){} 可以提前调用,var add = function(){} 却不行。
为什么?
二、变量提升(Hoisting):编译阶段的小动作
JavaScript 代码在执行前,会先经历一个 编译阶段 。
这个阶段会把 变量声明 和 函数声明 提升到当前作用域的顶部。
注意:提升的是声明,不是赋值。而且函数声明的优先级高于变量声明。
2.1 模拟变量提升后的代码
上面的代码在编译后,大概变成这个样子:
js
javascript
// ----- 编译阶段:变量提升部分 -----
var myName = undefined; // 变量声明提升,默认值 undefined
function showName() { // 函数声明整体提升
console.log('函数showName被执行了');
}
// ----- 执行阶段:可执行代码 -----
showName(); // 此时函数已经在内存中,调用成功
console.log(myName); // 输出 undefined
myName = '极客时间'; // 赋值操作留在原地
这就是 变量提升 的模拟过程。
函数 showName 被完整提升,所以可以在定义前调用。
var myName 只提升了声明(赋值为 undefined),赋值语句留在原地,所以打印时是 undefined。

图片里也展示了这个转换过程:左边是原始代码,右边是提升后的代码。
三、var 的声明和赋值是"两回事"

js
ini
var myName = '极客时间';
这行代码可以拆成两步:
| 阶段 | 操作 | 含义 |
|---|---|---|
| 编译阶段 | var myName |
声明变量,默认值 undefined |
| 执行阶段 | myName = '极客时间' |
赋值,覆盖 undefined |
所以写 var myName = '极客时间',在编译后等价于:
js
ini
var myName = undefined; // 提升到顶部
// ... 其他代码 ...
myName = '极客时间'; // 原地赋值
这就是为什么在赋值前打印变量,得到的是 undefined,而不是报错。
四、函数声明 vs 函数表达式

| 写法 | 类型 | 提升方式 |
|---|---|---|
function foo() {} |
函数声明 | 整体提升(函数体一起提升) |
var bar = function() {} |
函数表达式 | 只提升 var bar(值为 undefined),赋值留在原地 |
所以:
js
scss
foo(); // 正常执行
function foo() { console.log('foo'); }
bar(); // TypeError: bar is not a function
var bar = function() { console.log('bar'); };
编译后等价于:
js
javascript
// 提升部分
function foo() { console.log('foo'); }
var bar = undefined;
// 执行部分
foo();
bar(); // bar 还是 undefined,调用报错
bar = function() { console.log('bar'); };
图片中有一张对比图,明确标注了函数声明是"完整的函数声明",函数表达式是"声明 + 赋值"两步。
五、编译阶段到底干了什么?
当浏览器(比如 Chrome 的 V8 引擎)拿到一段 JS 代码时,会先编译,再执行。
编译阶段产出:
-
执行上下文(Execution Context)
这是代码执行所需的环境,里面包含:
- 变量环境(Variable Environment) :存放
var声明的变量和函数声明 - 词法环境(Lexical Environment) :存放
let、const声明的变量 - 其他(如 this 绑定)
- 变量环境(Variable Environment) :存放
-
可执行代码
去掉声明部分后,剩下的赋值、函数调用等语句。
举个例子,输入这段代码:
js
ini
showName();
console.log(myName);
var myName = '极客时间';
function showName() {
console.log('showName被调用');
}
编译后:
-
执行上下文 中:
变量环境:
myName = undefined,showName = function...词法环境:空(因为没有
let/const) -
可执行代码 为:
js
inishowName(); console.log(myName); myName = '极客时间';
然后进入 执行阶段,一行一行运行可执行代码。
图片中有流程图:输入一段 JavaScript 代码 → 编译 → 执行上下文 + 可执行代码 → 执行 → 输出结果。
六、let 和 const 怎么不一样?
let 和 const 也会提升,但处于 暂时性死区(TDZ) ,在声明前访问会报错。
js
ini
console.log(myname); // ReferenceError: Cannot access 'myname' before initialization
let myname = '极客时间';
为什么?
因为 let 声明的变量不在 变量环境 中,而在 词法环境 中。
词法环境里的变量在声明前是不可访问的,这叫暂时性死区。

图片中展示了"变量环境"和"词法环境"的分工:
var 和函数声明放变量环境,let/const 放词法环境。
七、整个流程回顾
-
编译阶段:
- 创建执行上下文(变量环境 + 词法环境)
- 将
var声明和函数声明放入变量环境,初始值undefined - 将
let/const声明放入词法环境,但处于 TDZ,不可访问 - 生成可执行代码
-
执行阶段:
- 按顺序执行可执行代码
- 遇到变量赋值,修改变量环境或词法环境中的值
- 遇到函数调用,去执行上下文里找函数

八、总结与建议
| 声明方式 | 提升 | 初始值 | 能否提前访问 |
|---|---|---|---|
var |
是 | undefined |
可以(值是 undefined) |
function 声明 |
是 | 函数对象 | 可以(正常调用) |
let |
是(TDZ) | 未初始化 | 不可以(报错) |
const |
是(TDZ) | 未初始化 | 不可以(报错) |
函数表达式 (var f = function) |
只提升 var f |
undefined |
不可以(调用报错) |
我的习惯:
- 默认用
let和const,别用var - 函数声明放在文件顶部,变量声明也尽量靠近顶部
- 不要在声明前使用变量,即使对
var可以,也不利于理解
变量提升是 JS 设计早期的一个"特性",虽然理解它有点绕,但一旦搞懂了编译阶段和执行阶段的区别,后面看代码就清晰多了。