深入剖析 JavaScript 执行机制:从变量提升到调用栈

75 阅读7分钟

引言

JavaScript 作为一门动态解释型语言,其执行机制与传统编译型语言有着显著差异。许多开发者在日常编码中经常会遇到一些"奇怪"的现象:变量在声明前就可以访问、函数调用似乎不受位置限制、相同变量重复声明却不报错等等。这些现象背后隐藏着 JavaScript 引擎复杂而精妙的工作机制。本文将深入探讨 JavaScript 的执行过程,揭示 V8 引擎如何处理代码,以及调用栈在其中扮演的关键角色。

JavaScript 的两阶段执行模型

编译阶段与执行阶段

与许多开发者直观感受不同,JavaScript 代码并非逐行直接执行。实际上,V8 引擎采用了两阶段处理模式:编译阶段执行阶段

javascript

复制下载

// 1.js
showName();
console.log(myname);
var maName = '张三';
let name = '李四'
function getName(){
  console.log('此函数被执行')
}

在这段看似简单的代码中,执行顺序与编写顺序并不一致。这是因为在代码真正执行前,V8 引擎会先进行编译处理。

变量提升的本质

在编译阶段,V8 引擎会扫描整个代码,进行语法检查并创建执行上下文。其中最重要的操作之一就是变量提升

javascript

复制下载

// 2.js - V8 引擎看待这段代码的方式
var myName; //undefined
function showName(){
  console.log('执行');
}
showName();
console.log(myName);
myName = '张三';

通过变量提升,所有使用 var 声明的变量和函数声明都会被"提升"到当前作用域的顶部。需要注意的是,函数声明比变量声明具有更高的优先级。

执行上下文与调用栈

执行上下文的结构

当 V8 引擎执行代码时,会创建一个称为执行上下文的环境,用于管理代码执行期间所需的所有信息。执行上下文包含三个核心部分:

  • 变量环境:存储使用 var 和 function 声明的变量和函数
  • 词法环境:存储使用 let 和 const 声明的变量和常量
  • 可执行代码:实际的代码指令

lQLPKINfgz4ClCvNASzNAUGwD3ItS5f39akI59vxBI3vAA_321_300.png

调用栈的工作机制

调用栈是 V8 引擎用来管理执行上下文的数据结构,遵循后进先出(LIFO)原则:

  1. 首先,全局执行上下文被压入调用栈
  2. 当函数被调用时,创建对应的函数执行上下文并压入栈顶
  3. 函数执行完毕后,其执行上下文从栈中弹出
  4. 继续执行下一个上下文中的代码

lQLPJwqSgSxbduvNAdrNAimwbE4UQi5QtTUI596JPiceAA_553_474.png

深入分析函数执行过程

复杂的函数执行上下文

让我们通过一个更复杂的例子来理解函数执行上下文的创建过程:

javascript

复制下载

// 3.js
var a = 1;
function fn(a){
  console.log(a); // 输出函数而非数字
  var a = 2;
  function a(){};
  var b = a;
  console.log(a);
}
fn(3);
console.log(a);

这段代码的执行结果可能会让许多开发者感到意外。要理解这个结果,我们需要详细分析函数 fn 的执行上下文创建过程。

函数执行上下文的创建步骤

当调用 fn(3) 时,V8 引擎会创建函数执行上下文,这个过程包括:

  1. 收集形参和变量声明:将形参 a 和所有 var 声明的变量添加到变量环境,初始值为 undefined
  2. 统一形参和实参:将实参值赋给对应的形参
  3. 处理函数声明:将函数声明添加到变量环境,如果已有同名变量则覆盖

具体到 fn 函数:

  1. 编译阶段:

    • 变量环境中:a = undefined(来自形参),b = undefined
    • 处理函数声明:a = function a(){}(覆盖之前的 a
    • 统一形参和实参:此时 a 已被函数声明覆盖,所以实参 3 不起作用
  2. 执行阶段:

    • 第一个 console.log(a) 输出函数 a
    • 执行 var a = 2,将变量环境中的 a 更新为 2
    • 函数声明 function a(){} 在编译阶段已处理,执行阶段不再处理
    • 执行 var b = a,将 a 的值 2 赋给 b
    • 第二个 console.log(a) 输出 2

lQLPJxG93Vx3l9vNAjLNAhywFOzCT1voxesI599ANyBVAA_540_562.png

var、let 与 const 的差异

变量声明方式的区别

JavaScript 提供了三种变量声明方式:varlet 和 const,它们在执行机制上有着重要区别。

javascript

复制下载

// 4.js
var a = 1;
var a = 2; // 重复声明,不报错,会被忽略
console.log(a); // 2

// 6.js
let func = () =>{
  console.log('函数表达式不会提升');
}

var 的特点

  • 存在变量提升
  • 允许重复声明
  • 函数作用域

let/const 的特点

  • 存在暂时性死区,不存在变量提升
  • 不允许重复声明
  • 块级作用域

严格模式的影响

在严格模式下,某些宽松的行为会被限制:

html

复制下载运行

<!-- 5.html -->
<script>
  'use strict';
  // 严格模式
  var a = 1;
  var a = 2; // 在严格模式下,重复声明会报错
</script>

内存管理与数据存储

基本数据类型与引用数据类型

JavaScript 中的变量在内存中的存储方式取决于其数据类型:

javascript

复制下载

// 7.js
let str = 'hello'; // 基本数据类型
let str2 = str; // 值的拷贝
console.log(str, str2);

let obj = { // 引用数据类型
  name: '张三',
  age: 18
}
let obj2 = obj; // 地址值一样 引用式拷贝
obj2.age++;
console.log(obj, obj2);

基本数据类型(如字符串、数字、布尔值等):

  • 存储在栈内存中
  • 赋值时进行值拷贝
  • 变量直接包含数据值

引用数据类型(如对象、数组等):

  • 数据存储在堆内存中
  • 变量存储的是指向堆内存的地址引用
  • 赋值时进行引用拷贝,多个变量可能指向同一数据

内存管理机制

V8 引擎使用自动垃圾回收机制管理内存。当一个执行上下文从调用栈中弹出后,其内部定义的局部变量就会失去引用,成为垃圾回收的候选对象。

JavaScript 执行流程总结

完整的代码执行过程

通过前面的分析,我们可以总结出 JavaScript 代码的完整执行流程:

  1. 代码输入:源代码进入 V8 引擎

  2. 编译阶段

    • 语法检查
    • 创建执行上下文
    • 变量提升(函数声明优先)
    • 构建作用域链
  3. 执行阶段

    • 全局代码执行
    • 遇到函数调用时暂停当前执行
    • 创建新的函数执行上下文
    • 执行函数代码
    • 函数执行完毕,上下文销毁
    • 回到之前的执行上下文继续执行

lQLPKIUtiTN1OCvMsc0C_7BSr3Rszy2BSQjn1hCD3CcA_767_177.png

与传统编译语言的差异

JavaScript 的执行机制与 C++ 或 Java 等传统编译语言有着根本区别:

  • 即时编译:JavaScript 采用即时编译(JIT)技术,一边编译一边执行
  • 动态类型:类型检查在运行时进行,而非编译时
  • 灵活的作用域:作用域在函数调用时动态确定

实际开发中的应用与注意事项

理解 JavaScript 的执行机制对于编写高质量代码至关重要:

  1. 避免变量提升带来的陷阱

    • 尽量使用 let 和 const 代替 var
    • 在作用域顶部声明变量
  2. 理解函数声明与函数表达式的区别

    • 函数声明会提升,函数表达式不会
    • 根据需求选择合适的定义方式
  3. 注意闭包与内存泄漏

    • 理解作用域链的形成
    • 避免不必要的闭包引用导致内存无法释放
  4. 优化性能

    • 减少全局变量的使用
    • 合理设计函数作用域

结语

JavaScript 的执行机制虽然复杂,但一旦理解其核心原理,就能更好地驾驭这门语言,写出更可靠、高效的代码。V8 引擎通过编译阶段与执行阶段的分离,以及调用栈的精细管理,实现了 JavaScript 的动态特性。作为开发者,深入理解这些底层机制,不仅能够避免常见的陷阱,还能在性能优化和代码设计方面做出更明智的决策。

通过本文的分析,我们希望读者能够建立起对 JavaScript 执行机制的全面理解,从而在日常开发中更加得心应手,写出符合 JavaScript 特性的高质量代码。