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 设计早期的一个"特性",虽然理解它有点绕,但一旦搞懂了编译阶段和执行阶段的区别,后面看代码就清晰多了。

相关推荐
lemon_sjdk29 分钟前
JavaScript 常用关键字全解析
开发语言·javascript·ecmascript
晚安日记wanna1 小时前
分布式事务 6 连问从 Seata AT 到本地消息表落地
面试·架构
高申航1 小时前
Vue 3 + DYMO Connect Framework 实战:从标签模板到打印服务的完整实现
前端·javascript·vue.js
雪芽蓝域zzs1 小时前
分页组件 + el-config-provider 中文国际化 + 靠右布局
前端·javascript·vue.js
志尊宝2 小时前
Vue3 零基础每日笔记(016):v-for 列表渲染——key 的作用与为什么不能用 index
javascript·vue.js·笔记
toooooop82 小时前
thinkphp查询数据表最后的自增id
前端·javascript·数据库
晚安日记wanna2 小时前
简历写精通浏览器原理一问 URL 到渲染直接哑火...
前端·面试
garmin Chen2 小时前
MySQL精简面试题
数据库·后端·mysql·面试
晚安日记wanna2 小时前
四年前端被问 Code Review90人只看得懂空格和命名
前端·面试
honkun62 小时前
vue 甘特图组件 vxe-gantt 配置拖拽调整任务起止日期时实时显示 Tooltip 提示
javascript·vue.js·甘特图·vxe-gantt