JavaScript 执行原理深入理解

9 阅读3分钟

变量提升(Hoisting)

变量和函数提升

在编译阶段,JavaScript 引擎会将声明部分提前处理:

  • 普通变量:提升变量到作用域顶部,值是 undefined
  • 函数声明:整体提升到作用域顶部,值是完整的函数对象(函数是一等对象)。
  • 函数表达式:仅作为普通变量进行提升,值为 undefined

JS 是怎么运行的

传统认知与现象

按照常理,代码是按顺序执行的。如果遵循这个逻辑,未定义的 myName 或 showName() 调用都应该直接报错,但实际运行中却没有报错。这说明:

  • 在执行的过程中,若使用了真正未声明的变量,会报错(明显的代码错误)。
  • 在一个变量定义之前使用它,不一定会报错,但该变量的值为 undefined,而不是定义时的值。
  • 在一个函数定义之前使用它,不会出错,且函数能正确执行。

后两个现象极其反直觉,表明 JS 代码并不是一行一行机械执行的。JS 不仅有执行阶段,还有编译阶段。编译阶段会为执行阶段做准备,准备好“执行上下文”。

变量提升发生在编译阶段(Hoisting)

要彻底搞懂这个问题,需要先了解编译阶段、执行阶段、作用域、执行上下文以及调用栈。

JS 是一门弱类型动态的脚本语言,它没有像 C++ 那样独立的编译阶段。所谓的变量提升,是指在 JavaScript 执行过程中,V8 引擎在运行前的那一刹那(即编译阶段),把变量的声明部分和函数的声明提取出来放入内存的行为。模拟这个过程,函数和变量在执行之前都是被提前处理到了代码开头,并且会给变量设置默认值 undefined

破除物理移动的误区

“变量提升”意味着变量和函数声明会在物理层面移动到代码的最前面吗?
这并不准确。实际上,变量和函数声明在源码中的位置是不会改变的。它们是在编译阶段被 JS 引擎扫描并放入了对应的内存环境中。JS 代码在执行之前需要被引擎编译,编译完成后,才会进入执行阶段。

编译阶段的产物

输入一段代码,经过编译阶段,会生成两部分核心内容:可执行代码执行上下文对象(Execution Context) 。执行上下文是 JS 执行一段代码的运行环境。例如,当调用一个函数时,就会进入这个函数的上下文,找到里面的变量环境和词法环境。

以如下结构为例,编译后的变量环境(VariableEnvironment)大致如下:

text

编辑

VariableEnvironment:
     myname -> undefined, 
     showName -> function : {console.log(myname)}

let/const 与暂时性死区(面试高频考点)

对于使用 let / const 声明的词法环境变量,在声明之前,它们处于暂时性死区(TDZ) 中,绝对不能使用。

面试标准回答话术
let 和 const 实际上也是提升了的。它们在编译阶段同样会被登记到词法环境中,但是不会被初始化赋值。从进入作用域开始到声明语句执行前,这段区域被称为‘暂时性死区’。如果在死区内访问该变量,引擎会直接抛出 ReferenceError(如:Cannot access 'myname' before initialization)。这种设计是为了更安全、更严谨地处理变量,强制开发者遵循先声明后使用的规范,避免传统 var 带来的隐蔽 Bug。