深入理解 JavaScript 执行机制:从编译到运行的完整揭秘

你真的了解 JS 代码是怎么执行的吗?一文带你彻底搞懂执行上下文、变量提升和作用域链

前言

对于每一位前端开发者来说,理解 JavaScript 的执行机制都是绕不开的必修课。你可能已经写过无数行 JS 代码,但当被问到"这段代码会输出什么"时,是否还会时常感到困惑?

本文将带你从底层原理出发,一步步拆解 JavaScript 的执行过程,让你彻底掌握这门语言的核心运行机制。

一、JS 执行的整体流程

JavaScript 代码的执行并非简单的逐行运行,而是经历一个完整的流程:

text

复制代码
读取代码 → 编译 → 执行

在这个流程中,编译总是在执行前一刻发生。V8 引擎会对代码进行即时编译,为后续执行做准备。

二、执行上下文(Execution Context)

执行上下文是 JS 执行环境中最重要的概念之一。它包含了代码执行所需的所有信息。

执行上下文的三大组件

  1. 变量环境 :存放 varfunction 声明的变量
  2. 词法环境 :存放 letconst 声明的变量,支持块级作用域
  3. 执行的代码:从上到下顺序执行的具体代码

三者共同组成了执行上下文对象,而全局和每个函数体的编译都会生成各自的执行上下文。

编译阶段到底发生了什么?

很多人误以为变量提升是"魔法",实际上它是在编译阶段被系统化处理的:

text

vbnet 复制代码
第一步:创建执行上下文对象
第二步:找形参和变量声明,将声明的变量名作为 key,值为 undefined
第三步:统一形参和实参的值(全局没有这个步骤)
第四步:找函数声明,将函数名作为 key,值为函数体

让我们用一个具体例子来说明:

javascript

javascript 复制代码
// 原始代码
showName('极客时间')
console.log(myname)
var myname = '胡航'

function showName(name) {
    console.log(name);
    var b = 1;
    console.log('函数someName执行', name);
}

经过编译阶段后,代码在 V8 引擎眼中变成了这样:

javascript

javascript 复制代码
// 编译后的样子
var myname  // 变量提升,值为 undefined

function showName(name) {
    console.log(name);
    var b = 1;
    console.log('函数someName执行', name);
}

showName('极客时间');  // 输出:极客时间
console.log(myname);    // 输出:undefined
myname = '胡航';        // 赋值操作留在原地

这就是为什么在变量声明之前访问它得到的是 undefined,而函数却可以正常调用。

函数声明的优先级

需要注意的是,函数声明会覆盖同名的变量声明:

javascript

javascript 复制代码
console.log(func);  // 输出:function func(){}

function func(){}

var func = '123'    // 这个赋值不会影响提升的结果

三、调用栈(Call Stack)

调用栈是 V8 引擎用来管理函数之间调用关系的一种数据结构。

核心特点

  • 单一执行:同一时刻只有一个函数在栈顶运行,其他函数需等待
  • 函数入栈:每当调用一个函数,其执行上下文会被压入栈顶
  • 函数出栈:函数执行完毕后,其上下文从栈顶弹出,控制权交还给上一层
  • 全局上下文始终在底:全局执行上下文位于栈底,是所有函数执行的基础
  • 栈溢出风险 :递归过深会导致栈空间耗尽,引发 RangeError

调用栈本质上就是执行上下文对象的容器,通过栈顶指针来指向当前正在执行的函数或全局代码。

案例演示

javascript

scss 复制代码
function varTest() {
    var x = 1;
    if(true) {
        var x = 2;
        console.log(x);  // 2
    }
    console.log(x);      // 2
}
varTest();  // 输出:2 2

这个例子中,var 声明的变量没有块级作用域,在函数内部会被同一个变量覆盖。

四、词法环境与块级作用域

letconst 的引入,解决了 var 长期以来存在的问题。

词法环境的核心特点

  • 块级作用域的实现 :当进入 {} 块时,会创建一个新的词法环境,并压入当前词法环境栈顶
  • 变量查找顺序:从当前词法环境栈顶开始,逐层向外(沿 Outer Environment 链)查找,直到全局作用域
  • 作用域链:当块级作用域执行完毕,对应的词法环境被弹出栈,其中的变量随之销毁

let/const 的特性

javascript

scss 复制代码
function varTest() {
    let x = 1;
    if(true) {
        let x = 2;      // 这是全新的变量,独立于外层的 x
        console.log(x); // 2
    }
    console.log(x);     // 1
}
varTest();  // 输出:2 1

从上面的代码可以看出,let 创建的变量具有真正的块级作用域,不会污染外层变量。

letconst 的核心限制:

  • 不可以重复声明
  • 不会变量提升(或者说"提升"到词法环境,但存在暂时性死区 TDZ)
  • 在声明之前访问会报错

这些设计都是为了修复 JS 早期设计中的缺陷。

五、综合案例:变量环境与词法环境的协作

javascript

ini 复制代码
function foo() {
    var a = 1;
    let b = 2;
    
    // 词法环境里做块级作用域的文章
    {
        let b = 3;      // 块级作用域中的新变量
        var c = 4;      // var 不受块级作用域限制
        let d = 5;
        console.log(a); // 1 - 从上层变量环境找到
        console.log(b); // 3 - 当前块级词法环境中的 b
    }
    
    console.log(b);     // 2 - 外层词法环境中的 b
    console.log(c);     // 4 - var 声明提升到了函数顶部
    console.log(d);     // ReferenceError - 块级作用域已销毁
}

foo();

这个例子完美展示了:

  1. var 声明的变量不受块级作用域限制
  2. let 声明的变量具有块级作用域
  3. 不同作用域的同名变量可以共存
  4. 块级作用域执行完毕后,其内部的 let/const 变量会被销毁

六、总结

通过本文的梳理,我们可以总结出 JavaScript 执行机制的几个关键点:

  1. 编译先行:所有代码在执行前都会经过编译阶段,这是变量提升的本质原因
  2. 执行上下文:每个函数和全局都有自己的执行上下文,包含变量环境、词法环境和可执行代码
  3. 调用栈管理:执行上下文通过调用栈来管理,确保函数的正确调用和返回
  4. 作用域链:变量查找沿着作用域链逐层进行,直到全局作用域
  5. 块级作用域letconst 通过词法环境实现了真正的块级作用域

理解这些底层机制,不仅能帮你轻松应对各种输出题,更重要的是能写出更可靠、更可预测的代码。

真正的精通,来自于对底层原理的理解。希望本文能帮助你在 JavaScript 的道路上更进一步!

相关推荐
云水一下1 小时前
TypeScript 从零基础到精通(四):面向对象编程(类与继承)
javascript·typescript
shmily麻瓜小菜鸡1 小时前
Bootstrap 4 常用工具类速查表
前端·javascript·bootstrap
CDN3601 小时前
【架构进阶】告别配置漂移!用 NodeNext + Workspace 打造优雅的 TypeScript Monorepo
前端·javascript·typescript
超人不会飞_Jay2 小时前
6.2前端笔记
前端·javascript·笔记
2401_868534782 小时前
常见的 vue面试题目
前端·javascript·vue.js
胡萝卜术2 小时前
从零搭建 NLP Demo:用 ES6 模块化 + DeepSeek API 构建你的第一个 AI 应用
javascript·面试
颂love2 小时前
TypeScript速学
前端·javascript·typescript
凌涘2 小时前
深入理解 JavaScript 执行机制:从执行上下文到调用栈全解析
前端·javascript
用户938515635072 小时前
从模块化到 Prompt 工程:我用 Node.js + LLM 复刻了传统 NLP 的流程
javascript·人工智能·node.js