深入理解 JavaScript 执行机制:从编译阶段到调用栈底层实现
- [一、 JavaScript 的执行流程概述](#一、 JavaScript 的执行流程概述)
- [二、 编译阶段的具体步骤与变量提升](#二、 编译阶段的具体步骤与变量提升)
-
- 代码案例分析
-
- [案例 1:基础变量提升与函数声明](#案例 1:基础变量提升与函数声明)
- [案例 2:V8 引擎视角的实际执行顺序](#案例 2:V8 引擎视角的实际执行顺序)
- [案例 3:变量与函数同名冲突](#案例 3:变量与函数同名冲突)
- [案例 4:函数体内部的复杂编译流程](#案例 4:函数体内部的复杂编译流程)
- [三、 执行上下文的内部构造](#三、 执行上下文的内部构造)
- [四、 调用栈(Call Stack)的底层管理](#四、 调用栈(Call Stack)的底层管理)
- [五、 let 与 const 的块级作用域实现机制](#五、 let 与 const 的块级作用域实现机制)
-
- [1. 作用域的表现差异](#1. 作用域的表现差异)
-
- [案例 5:`var` 声明的局限性](#案例 5:
var声明的局限性) - [案例 6:`let` 声明的块级作用域](#案例 6:
let声明的块级作用域)
- [案例 5:`var` 声明的局限性](#案例 5:
- [2. 词法环境内部的栈结构实现](#2. 词法环境内部的栈结构实现)
- [3. 暂时性死区(Temporal Dead Zone)与重复声明限制](#3. 暂时性死区(Temporal Dead Zone)与重复声明限制)
- [六、 总结](#六、 总结)
JavaScript 作为一门单线程动态语言,其执行机制并非简单的"一行行解释执行"。在 V8 引擎底层,代码的读取、编译与执行有着严密的逻辑协作。本文将结合底层编译步骤、执行上下文结构、调用栈管理以及 let/const 的块级作用域实现,深入剖析 JavaScript 的执行期行为。
一、 JavaScript 的执行流程概述
JavaScript 的核心执行流程可以概括为:读取代码 -> 编译阶段 -> 执行阶段。
[输入代码] ──> [V8 引擎编译] ──> [生成执行上下文与字节码/机器码] ──> [顺序执行]
在执行一段 JavaScript 代码(全局代码或函数代码)的前一刻,V8 引擎都会先对其进行编译 。编译阶段会创建对应的执行上下文对象(Execution Context) ,并将其存入调用栈中。编译完成后,代码才开始由上至下顺序执行。
二、 编译阶段的具体步骤与变量提升
编译阶段是理解 JavaScript 许多特性(如变量提升、函数重名覆盖)的关键。引擎在编译阶段会按照以下四个步骤运行:
- 创建执行上下文对象(Execution Context)。
- 寻找形参和变量声明 :将形参名和声明的变量名作为 key 存入执行上下文,初始值赋为
undefined。 - 统一形参和实参的值:将传入的实参赋值给对应的形参 key(注:全局编译阶段没有此步骤)。
- 寻找函数声明 :在当前作用域中寻找通过
function关键字声明的函数,将函数名作为 key,其值直接指向函数体。
代码案例分析
案例 1:基础变量提升与函数声明
javascript
// JavaScript 执行机制对开发者至关重要
// 代码这么执行的
showname('极客时间')
console.log(myname) // undefined
var myname = '苔藓'
function showname(name){
console.log(name);
var b = 1;
console.log('函数showName执行', name);
}
底层编译表现(全局上下文):
- 步骤 2 发现变量声明,在上下文中创建其对应的 key 并赋值为
undefined。 - 步骤 4 发现函数声明,创建对应的 key 并使其值直接指向函数体。
- 代码执行时,由于函数声明已被处理,故能在声明前正常调用;而普通变量在赋值前被访问则输出
undefined。
案例 2:V8 引擎视角的实际执行顺序
经过编译阶段的转化,代码在 V8 引擎眼中的实际执行等价于变量提升与函数声明被移动至作用域顶部,随后再顺序执行其余的赋值与调用语句。
javascript
// v8 引擎的眼里
var myname // 变量提升
function showName(){
console.log('函数showName执行');
}
showName();
console.log(myname);
myname = "苔藓";
案例 3:变量与函数同名冲突
javascript
console.log(func);
function func()
{
}
var func = '123'
分析:
- 步骤 2 扫描到变量声明,上下文添加对应变量名为 key 且值为
undefined。 - 步骤 4 扫描到同名函数声明,将该 key 的值改写为指向函数体(函数声明权重高于普通变量声明)。
- 编译结束进入执行阶段,在变量赋值语句执行前调用该名称,其值依然保持为函数体。
案例 4:函数体内部的复杂编译流程
javascript
var a = 1;
function fn(a){
console.log(a);
var a = 2;
function a(){}
var b = a;
console.log(a);
}
fn(3);
当调用函数并传入实参时,函数体的局部编译步骤如下:
- 创建该函数的函数执行上下文。
- 找形参和变量声明 :扫描局部作用域内的形参和所有变量声明,在上下文中将其初始化为
undefined。 - 统一形参和实参:将传入的实参赋值给对应的形参 key。
- 找函数声明:发现内部的函数声明,更新同名 key 的值使其指向函数体。
三、 执行上下文的内部构造
一个完整的执行上下文对象内部,主要由三部分组成:
- 变量环境(Variable Environment) :专门存放通过
var声明的变量以及function声明的函数。变量提升的属性均在此生成。 - 词法环境(Lexical Environment) :专门存放通过
let和const声明的变量。 - 执行的代码:按从上到下的顺序顺序执行。
四、 调用栈(Call Stack)的底层管理
V8 引擎用来管理函数之间调用关系的一种数据结构被称为调用栈。调用栈是执行上下文的容器,它遵循栈结构的特点。
- 入栈时机:全局代码和函数体在编译时会生成执行上下文,并存入调用栈。
- 当前执行指针:栈顶指针始终指向当前正在执行的函数或全局。
- 出栈与销毁:当一个函数执行完毕后,它的执行上下文会弹出调用栈,完成栈销毁。
五、 let 与 const 的块级作用域实现机制
在早期 JavaScript 版本中,由于 var 的设计缺陷,导致了诸多工程隐患。ES6 引入了 let 和 const,并通过词法环境完美支持了块级作用域。
1. 作用域的表现差异
案例 5:var 声明的局限性
javascript
function varTest(){
var x = 1;
if(true){
var x = 2;
console.log(x); // 2
}
console.log(x); // 2
}
varTest();
分析 :由于 var 声明对都存放在变量环境中,它不支持块级作用域。在代码块(如 if 块)内部使用 var 重新声明或赋值,修改的仍是当前函数体/全局级别变量环境中的同一变量。
案例 6:let 声明的块级作用域
javascript
function varTest(){
var x = 1;
if(true){
let x = 2;
// let b = 3;
console.log(x); // 2
}
// console.log(b); // 不能打印b
console.log(x); // 1
}
varTest();
分析 :在代码块内部通过 let 声明的变量,独立存在于块级作用域内,在外部无法被打印或访问,从而实现了块级隔离。
2. 词法环境内部的栈结构实现
JavaScript 引擎在单个执行上下文内部,仍然利用栈结构来管理不同块级作用域的变量。
javascript
function foo() {
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(b);
console.log(a);
}
console.log(b);
console.log(c);
console.log(d);
}
foo();
当代码执行到包含块级作用域的代码块内部时,其执行上下文的内部底层构造如下:
- 变量环境 中存放的是由
var和function声明的变量。 - 词法环境 内部维护着一个局部栈结构:
- 栈底存放着函数/全局根部由
let和const声明的变量。 - 当进入一个新的块级作用域时,该代码块内部由
let或const声明的变量会被压入词法环境的栈顶。
- 栈底存放着函数/全局根部由
- 变量查找规则:在执行代码进行变量查找时,会优先从词法环境的栈顶开始向下查找,如果词法环境栈中未找到,再去变量环境中查找。
- 块退出销毁:当代码块执行完毕退出时,词法环境栈顶对应的该块级作用域变量会直接弹出并销毁。
3. 暂时性死区(Temporal Dead Zone)与重复声明限制
let 和 const 具备严密的保护机制,消除了原有的语言缺陷:
- 禁止重复声明 :
let和const不能重复声明同一变量。 - 无变量提升与暂时性死区 :
let和const不会发生变量提升,在变量声明语句执行之前,该变量会进入暂时性死区(dead zone),期间访问该变量会直接报错。这些设计都是为了擦除和修正之前 JavaScript 的缺陷。
javascript
// let a = 1;
// let a = 2; // 报错:Identifier 'a' has already been declared
// var a = 1;
// var a = 2; // var允许重复声明
console.log(a); // 报错:Cannot access 'a' before initialization(处于暂时性死区)
var a = 1;
function a(){
}
六、 总结
- 先编译后执行:JavaScript 的执行流程为读取代码、编译、再执行。编译总是在执行前的一刻发生,并生成对应的执行上下文。
- 上下文隔离 :执行上下文对象中,变量环境负责管理
var和function的声明,而词法环境通过内部的栈结构支持并管理不同块级作用域的let和const变量。 - 调用栈管辖:调用栈作为执行上下文的容器,负责管理函数之间的调用关系,随函数的调用与结束进行执行上下文的压栈与弹出销毁。