JavaScript 执行上下文与作用域深度解析
本文深入解析 JavaScript 代码执行的核心机制,涵盖编译器预编译三阶段、执行栈与三种执行上下文类型。详细阐述词法环境、变量环境及作用域链的构建过程,对比全局、函数与块级作用域,并厘清执行上下文与作用域在 this 指向和变量访问上的本质区别。
前言
若希望成为合格的前端开发者,必须掌握 JavaScript 代码的执行流程,理解执行上下文、作用域及变量提升等核心概念,并能将其熟练应用于实际开发。本文内容参考了《你不知道的 JavaScript》与《JavaScript 高级程序设计》及相关技术博客。
正文
1. JavaScript 代码的执行过程相关概念
JavaScript 代码的执行主要经历编译器编译与 JS 引擎及作用域执行两个阶段。其中,编译器的编译阶段(即预编译阶段)包含分词(词法分析)、解析(语法分析)以及代码生成三个子步骤。
(1)在分词/词法分析阶段,编译器负责将源代码分割处理,将其转换为词法单元流或数组。
(2)在解析/语法分析阶段,编译器将上一步生成的词法单元流转换为由嵌套元素组成的、符合程序语法结构的抽象语法树(AST)。
(3)在代码生成阶段,编译器将抽象语法树转换为可执行代码,并将其交付给 JS 引擎。
JavaScript 代码执行涉及三个重要角色:
(1)JS 引擎:负责代码执行的整个过程。
(2)编译器:负责 JavaScript 代码的语法解析和生成可执行代码。
(3)作用域:负责收集并维护所有声明的标识符,根据特定规则确定当前代码对声明标识符的访问权限。
2. 执行上下文和执行栈
JavaScript 代码运行时始终处于执行上下文中。理解执行上下文需先了解执行栈,执行栈即其他编程语言中的“调用栈”,是一种基于 LIFO(后进先出)数据结构设计的栈,用于存储代码运行时创建的执行上下文。当 JS 引擎首次遇到可执行代码时,会创建一个全局执行上下文并压入当前执行栈。每当引擎遇到函数调用,便为该函数创建一个新的执行上下文并压入栈顶。JS 引擎执行栈顶的函数,当该函数执行完毕后,其执行上下文从栈中弹出,控制流程转移至下一个上下文。每个执行上下文包含三个关键属性:变量对象、作用域链和 this,需深入理解这些属性。
2.1 、上下文调用栈
var scope1 = "global scope";
function checkscope1(){
var scope1 = "local scope";
function f(){
console.log(scope1);
}
return f();
}
checkscope1();
var scope2 = "global scope";
function checkscope2(){
var scope2 = "local scope";
function f(){
console.log(scope2);
}
return f;
}
checkscope2()();
上述两段代码均会输出 'local scope'。
在上述代码中,scope 必定为局部变量,只需查找块级作用域即可。无论何时何地执行 f(),这种绑定在执行 f() 时依然有效。虽然结果相同,但两段代码的执行上下文栈变化过程不同:
第一段代码:push(<checkscope1>functionContext)=>push(<f>functionContext)=>pop()=>pop()
第二段代码:push(<checkscope2>functionContext)=>pop()=>push(<f>functionContext)=>pop()
2.2 、三种执行上下文类型
(1)全局上下文
JS 引擎开始解析代码时,首先遇到的是全局代码。初始化时,调用栈中会压入一个全局执行上下文,直到整个应用程序结束才会清空执行上下文栈,栈底始终保留全局执行上下文。这是默认的基础全局作用域,所有函数内部的代码均处于全局作用域中。系统首先创建一个全局 window 对象,并将 this 的值设置为该全局对象。程序中仅存在一个全局执行上下文。在顶层 JS 代码中,可以使用 this 引用全局对象,因为全局对象是作用域链的头部,意味着所有非限定性的变量和函数都作为该对象的属性进行查询。
总之,全局执行上下文只有一个,在客户端环境中通常由浏览器创建,即我们熟知的 window 对象,我们可以通过 this 直接访问它。
(2)函数上下文
每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都拥有自己的上下文,但这是在函数调用时创建的。需要注意的是,同一个函数被多次调用时,都会创建新的上下文。
(3)eval 和 with 上下文
在 eval 和 with 函数内部执行的代码拥有其专属的执行上下文。由于 JavaScript 开发者并不经常使用 eval,此处不作深入讨论。
2.3 、执行上下文创建阶段
执行上下文的创建分为创建阶段与执行阶段两个部分。
JS 引擎在执行上下文创建阶段主要负责三件事:确定 this 的值、创建词法环境组件、创建变量环境组件(目前部分细节尚需进一步理解)。
(1)确定 this,此处不做详解。
(2)创建词法环境组件
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。其中,环境记录器用于存储当前环境中的变量和函数声明的实际位置;外部环境引用记录器则用于保存自身环境可以访问的其他外部环境。提到这一点,是否让人联想到作用域链?
词法环境有两种类型:
- 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用为 null。它拥有内建的 Object/Array 等、在环境记录器内的原型函数(关联全局对象,如 window 对象)以及任何用户定义的全局变量,且 this 的值指向全局对象。
- 在函数环境中,函数内部用户定义的变量存储在环境记录器中。引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
(3)创建变量环境组件
变量环境也可视为一种词法环境,具备词法环境的所有属性,同样拥有环境记录与外部环境引入。在 ES6 中,两者的唯一区别在于:词法环境用于存储函数声明与 let、const 声明的变量,而变量环境仅存储 var 声明的变量。
3. JavaScript 作用域和作用域链
3.1、作用域
词法作用域在编写代码或定义时确定,而动态作用域在运行时确定(this 也是动态的)。词法作用域关注函数在何处声明,动态作用域关注函数从何处调用。JavaScript 采用词法作用域,其作用域由编写代码时将变量和块作用域写在哪里决定,因此当词法分析器处理代码时会保持作用域不变。可以理解为作用域是一个独立的地盘,防止变量外泄或暴露。也就是说,作用域最大的用处是隔离变量,不同作用域下同名变量不会发生冲突。
在理解作用域之前,先看一道例题。
function foo() {
console.log(value);
}
var value = 1;
function bar() {
var value = 2;
console.log(value);
foo();
}
bar();
上述代码会输出什么?首先在全局上下文中声明 foo() 函数、value 变量(其值为 undefined)以及 bar() 函数。在代码执行阶段,bar 函数上下文入栈并执行,打印出 value 为 2,然后执行 foo()。foo() 入栈,在打印 value 时找不到该变量,JS 引擎会向上层作用域查找,即全局作用域,于是打印出 1。随后函数执行完毕,上下文出栈。再看下面这个函数,作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。

自 ES6 以来,JavaScript 中的作用域分为全局作用域、函数作用域、块级作用域和欺骗作用域。
3.1.1、全局作用域
在代码中任何地方都能访问到的对象拥有全局作用域。最外层函数和在最外层函数外面定义的变量拥有全局作用域,所有未定义直接赋值的变量自动声明为拥有全局作用域。
3.1.2、函数作用域
函数作用域的定义是:函数内部的所有变量均可在函数体内及嵌套作用域中被访问和复用。该原则要求软件设计应最小化暴露必要内容,其余部分予以隐藏。函数表达式允许匿名形式,而函数声明必须包含函数名。3.1.3、块作用域:通常指 {} 代码块内部的作用域。(1)if 语句和 try/catch 语句可创建块作用域;(2)let 关键字可将变量绑定至任意作用域(通常为 {} 内部);(3)for 循环头部使用 let 声明变量 i 时,不仅将其绑定至循环块,还会在每次迭代中重新绑定,确保使用上一次迭代结束时的值进行赋值;(4)const 同样用于创建块作用域变量,其值为固定常量,但创建对象时对象内部属性值可被修改。3.1.4、欺骗词法作用域的方法,eval()和with():eval() 接收字符串参数,并在非严格模式下将其内容作为代码执行;with() 用于在重复引用对象多个属性时,避免重复书写对象名。
3.2、作用域链
作用域链本质上就是根据名称查找变量(标识符名称)的一套规则。规则非常简单,在自己的变量对象里找不到变量,就上父级的变量对象查找,当抵达最外层的全局上下文中,无论找到还是没找到,查找过程都会停止。查找会在找到第一个匹配的变量时停止,这被称为遮蔽效应。
作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链:当函数定义时,系统生成 [scope] 属性,该属性保存该函数的作用域链。该作用域链的第 0 位存储当前环境下的全局执行期上下文 GO,GO 里存储全局下的所有对象,其中包含函数和全局变量。当函数执行的前一刻,预编译的时候,作用域链的顶端(第 0 位)存储函数生成的执行上下文 AO,同时第一位存储 GO。查找变量是到函数存储的作用域链中从顶端开始依次向下查找(函数内部作用域在最顶端,证明了函数可以访问外部的变量,而外部无法访问函数内部的变量)。
4.执行上下文和作用域的区别
每个函数调用都有与之相关的作用域和上下文。从根本上说,范围是基于函数(function-based)而上下文是基于对象(object-based)。换句话说,作用域是和每次函数调用时变量的访问有关,并且每次调用都是独立的。上下文总是关键字 this 的值,是调用当前可执行代码的对象的引用。作用域是函数定义的时候就确定好的了,函数当中的变量是和函数所处的作用域有关,函数运行的作用域也是与该函数定义时的作用域有关。而上下文,主要是关键字 this 的值,这个是由函数运行时决定的,简单来说就是谁调用此函数,this 就指向谁。
5.最后
以上就是 JavaScript 执行上下文和作用域的相关总结的详细内容,更多关于 JavaScript 执行上下文和作用域的资料请关注本站其它相关文章!
JavaScript异步发送HTTP请求时怎么优雅地处理错误
2026-11-05 17:10:44
5款主流JS在线运行工具对比:CodePen、JSFiddle与StackBlitz怎么选
2026-11-05 17:04:04
JavaScript入门书籍避坑:这些书不适合初学者
2026-11-05 16:31:15
JS使用Math.round实现保留两位小数的原理与代码
2026-11-05 15:35:55
JavaScript 原生 Date 对象怎么实现日期加减
2026-11-05 14:30:00
