你真的了解 JS 代码是怎么执行的吗?一文带你彻底搞懂执行上下文、变量提升和作用域链
前言
对于每一位前端开发者来说,理解 JavaScript 的执行机制都是绕不开的必修课。你可能已经写过无数行 JS 代码,但当被问到"这段代码会输出什么"时,是否还会时常感到困惑?
本文将带你从底层原理出发,一步步拆解 JavaScript 的执行过程,让你彻底掌握这门语言的核心运行机制。
一、JS 执行的整体流程
JavaScript 代码的执行并非简单的逐行运行,而是经历一个完整的流程:
text
读取代码 → 编译 → 执行
在这个流程中,编译总是在执行前一刻发生。V8 引擎会对代码进行即时编译,为后续执行做准备。
二、执行上下文(Execution Context)
执行上下文是 JS 执行环境中最重要的概念之一。它包含了代码执行所需的所有信息。
执行上下文的三大组件
- 变量环境 :存放
var和function声明的变量 - 词法环境 :存放
let和const声明的变量,支持块级作用域 - 执行的代码:从上到下顺序执行的具体代码
三者共同组成了执行上下文对象,而全局和每个函数体的编译都会生成各自的执行上下文。
编译阶段到底发生了什么?
很多人误以为变量提升是"魔法",实际上它是在编译阶段被系统化处理的:
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 声明的变量没有块级作用域,在函数内部会被同一个变量覆盖。
四、词法环境与块级作用域
let 和 const 的引入,解决了 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 创建的变量具有真正的块级作用域,不会污染外层变量。
let 和 const 的核心限制:
- 不可以重复声明
- 不会变量提升(或者说"提升"到词法环境,但存在暂时性死区 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();
这个例子完美展示了:
var声明的变量不受块级作用域限制let声明的变量具有块级作用域- 不同作用域的同名变量可以共存
- 块级作用域执行完毕后,其内部的
let/const变量会被销毁
六、总结
通过本文的梳理,我们可以总结出 JavaScript 执行机制的几个关键点:
- 编译先行:所有代码在执行前都会经过编译阶段,这是变量提升的本质原因
- 执行上下文:每个函数和全局都有自己的执行上下文,包含变量环境、词法环境和可执行代码
- 调用栈管理:执行上下文通过调用栈来管理,确保函数的正确调用和返回
- 作用域链:变量查找沿着作用域链逐层进行,直到全局作用域
- 块级作用域 :
let和const通过词法环境实现了真正的块级作用域
理解这些底层机制,不仅能帮你轻松应对各种输出题,更重要的是能写出更可靠、更可预测的代码。
真正的精通,来自于对底层原理的理解。希望本文能帮助你在 JavaScript 的道路上更进一步!