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) :存放 letconst 声明的变量
    • 其他(如 this 绑定)
  2. 可执行代码

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

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

js

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

编译后:

  • 执行上下文 中:

    变量环境:myName = undefinedshowName = function...

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

  • 可执行代码 为:

    js

    ini 复制代码
    showName();
    console.log(myName);
    myName = '极客时间';

然后进入 执行阶段,一行一行运行可执行代码。

图片中有流程图:输入一段 JavaScript 代码 → 编译 → 执行上下文 + 可执行代码 → 执行 → 输出结果。


六、let 和 const 怎么不一样?

letconst 也会提升,但处于 暂时性死区(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 不可以(调用报错)

我的习惯

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

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

相关推荐
weixin_471383031 小时前
由浅入深递归练习
前端·javascript·vue.js
UTF_82 小时前
一次NSMutableAttributedString误用的思考
ios·面试·github
丷丩2 小时前
MapLibre GL JS第21课:绘制GeoJSON点图标、注记
前端·javascript·gis·mapbox·maplibre gl js
程序员卷卷狗2 小时前
Java转Go面试速记:Go基础22问,一篇理清高频易错点一篇理清高频易错点
java·面试·golang
丷丩2 小时前
MapLibre GL JS第20课:更新GeoJSON多边形
前端·javascript·gis·mapbox·maplibre gl js
swipe3 小时前
DeepAgents middleware 工程实战:把复杂 Agent 的运行时基建交给可组合中间件
前端·面试·llm
丷丩3 小时前
MapLibre GL JS第33课:渲染世界副本
javascript·gis·map·mapbox·maplibre gl js
bonechips3 小时前
深入理解 JavaScript的历史包袱——变量提升(Hoisting)
javascript·深度学习
丷丩3 小时前
MapLibre GL JS第31课:添加实时数据
javascript·gis·map·mapbox·maplibre gl js