JavaScript 变量提升,执行上下文里的各种门道

明明写在后面的函数,在前面调用却不报错?

明明没定义的变量,打印出来是 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 代码时,会先编译,再执行。

编译阶段产出:

  1. 执行上下文(Execution Context)

    这是代码执行所需的环境,里面包含:

    • 变量环境(Variable Environment) :存放 var 声明的变量和函数声明
    • 词法环境(Lexical Environment) :存放 let、const 声明的变量
    • 其他(如 this 绑定)
  2. 可执行代码

    去掉声明部分后,剩下的赋值、函数调用等语句。

举个例子,输入这段代码:

js

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

编译后:

  • 执行上下文 中:

    变量环境:myName = undefined,showName = function...

    词法环境:空(因为没有 let/const)

  • 可执行代码 为:

    js

    ini 复制代码
    showName();
    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 放词法环境。


七、整个流程回顾

  1. 编译阶段:

    • 创建执行上下文(变量环境 + 词法环境)
    • 将 var 声明和函数声明放入变量环境,初始值 undefined
    • 将 let/const 声明放入词法环境,但处于 TDZ,不可访问
    • 生成可执行代码
  2. 执行阶段:

    • 按顺序执行可执行代码
    • 遇到变量赋值,修改变量环境或词法环境中的值
    • 遇到函数调用,去执行上下文里找函数

八、总结与建议

声明方式 提升 初始值 能否提前访问
var 是 undefined 可以(值是 undefined)
function 声明 是 函数对象 可以(正常调用)
let 是(TDZ) 未初始化 不可以(报错)
const 是(TDZ) 未初始化 不可以(报错)
函数表达式 (var f = function) 只提升 var f undefined 不可以(调用报错)

我的习惯:

  • 默认用 let 和 const,别用 var
  • 函数声明放在文件顶部,变量声明也尽量靠近顶部
  • 不要在声明前使用变量,即使对 var 可以,也不利于理解

变量提升是 JS 设计早期的一个"特性",虽然理解它有点绕,但一旦搞懂了编译阶段和执行阶段的区别,后面看代码就清晰多了。

相关推荐
星栈19 分钟前
pnpm 12 升级实测
前端·javascript
Ai-_Man1 小时前
您您这可以把Microsofat Copilot的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。AI导出鸭
javascript·人工智能·ai·小程序·电脑·copilot
Bruce_Liuxiaowei4 小时前
网页版 DeepSeek “harness desktop“ WebGL 上下文创建失败排查实录
开发语言·javascript·webgl
小溪学编程5 小时前
从 C++ 的规范变迁看语言发展
jvm·c++·面试
小凯在掘金5 小时前
为什么箭头函数不能作为构造函数?
前端·javascript
可乐鸡翅yeah_5 小时前
网页端 M3U8 播放器性能监控,怎么看播放卡顿指标
javascript·ffmpeg·音视频·safari·m3u8
可乐鸡翅yeah_5 小时前
M3U8 防盗链 URL‑Token 签名,新手开发实操避坑
开发语言·javascript·ios·音视频·safari
ShineWinsu5 小时前
对于Redis:string类型的解析
java·c++·redis·分布式·缓存·面试·string
2603_965896626 小时前
JS原型与原型链|对象继承与实例底层原理
开发语言·javascript·原型模式
JustHappy6 小时前
「vConsole MCP🛠️」我让 AI 直接看见任何 H5 的日志和请求帮你 debug
前端·javascript·程序员