Skip to content
快速预览

ES3和ES5+执行过程阶段

✍️ w 🕒 2023-06-28 12:51:11(a year ago) 🔗 A.前端知识整理

先提前了解名词定义

名词解释
ECS执行上下文栈(Execution Context Stack):JavaScript引擎在执行代码时,会将创建的执行上下文(Execution Context)按照顺序存储在一个栈结构中,这个栈结构被称为执行上下文栈。当函数被调用时,会创建一个新的执行上下文并将其压入栈顶;当函数执行完毕后,会将栈顶的执行上下文弹出,返回到下一个执行上下文。
GEC全局执行上下文(Global Execution Context):在JavaScript代码开始执行时,会首先创建一个全局执行上下文。全局执行上下文负责管理全局变量、函数声明以及全局对象(如window对象)。全局执行上下文只有一个,它是执行上下文栈的底部。
FEC函数执行上下文(Functional Execution Context):当一个函数被调用时,会创建一个新的函数执行上下文。函数执行上下文负责管理函数内的变量、函数声明以及this指针。每次调用函数时,都会创建一个新的函数执行上下文。
VOVariable Object:在早期的ECMA规范中,变量对象(Variable Object)是用来存储执行上下文中的变量、函数声明和函数参数的对象。在最新的ECMA规范中,变量对象已经被替换为环境记录(Environment Record)。
VEVariable Environment:在最新的ECMA规范中,变量环境(Variable Environment)是用来存储执行上下文中的变量、函数声明和函数参数的环境记录。环境记录有两种类型:词法环境记录(Lexical Environment Record)和全局环境记录(Global Environment Record)。
GO全局对象(Global Object):在JavaScript代码开始执行时,会创建一个全局对象。全局对象用于存储全局变量、函数声明以及其他全局性质的属性。在浏览器环境中,全局对象通常是window对象;在Node.js环境中,全局对象是global对象。全局执行上下文中关联的变量对象就是全局对象。
AO函数对象(Activation Object):当一个函数被调用时,会创建一个函数对象(Activation Object)。函数对象用于存储函数内的变量、函数声明和函数参数。函数执行上下文中关联的变量对象就是函数对象。在最新的ECMA规范中,函数对象已经被替换为词法环境记录。

说明

随着 es 升级,规范也和之前不一样做了升级。主要可以分为 es3 和 es5+ 两个分水岭

当然可以。以下是旧概念与新概念的对照表:

旧概念新概念描述
变量对象(VO)环境记录(Environment Record)存储变量、函数声明和类声明等标识符
活动对象(AO)环境记录(Environment Record)存储变量、函数声明和类声明等标识符
全局对象(GO)全局对象(Global Object)JavaScript 运行时的全局作用域(如浏览器中的 window 对象,Node.js 中的 global 对象)
执行上下文栈(ECS)执行上下文栈(Execution Context Stack)存储和管理当前执行的所有执行上下文
全局执行上下文(GEC)执行上下文(Execution Context)包括全局执行上下文(在全局作用域中执行的代码)和函数执行上下文(在函数作用域中执行的代码)
函数执行上下文(FEC)执行上下文(Execution Context)包括全局执行上下文(在全局作用域中执行的代码)和函数执行上下文(在函数作用域中执行的代码)
变量环境(VE)词法环境(Lexical Environment)存储和管理变量、函数声明和类声明等标识符,实现作用域链和变量访问

Execution Contexts 执行上下文

产生执行上下文发生在编译阶段,之前说过如果想让 js 代码可以让机器识别,需要转义成机器码,这阶段就涉及到编译

编译阶段:在这个阶段,V8 引擎会对抽象语法树进行处理,生成字节码(Bytecode)。字节码是一种介于源代码和机器码之间的中间代码,可以被快速解释执行。同时,V8 引擎会创建执行上下文(Execution Context),每当代码进入一个不同的运行环境时,V8 引擎都会创建一个新的执行上下文。如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

图 4

下图看到经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。

图 1

因此执行上下文(EC)可以被理解为定义为执行 JavaScript 代码的环境。也就是我们代码都将在一个创建好的一个环境中执行,这个环境具备在特定时间代码可以访问的执行上下文,哪些情况下代码才算是一段代码,才会在执行之前就进行编译并创建执行上下文。一般说来,有这么三种情况:

  1. 全局执行上下文(Global Execution Context,GEC) :这是 JavaScript 代码执行时的默认环境,包含全局对象(如 window 对象)、内置对象和全局变量。

  2. 函数执行上下文(Functional Execution Context,FEC) :当调用一个函数时,会为该函数创建一个新的执行上下文。这个环境包含了函数的参数、局部变量和内部函数。

  3. Eval 函数执行上下文(Execution Context inside Eval Function) :当使用 eval 函数执行一段代码时,会为该代码创建一个特殊的执行上下文。

看一个案例

  1. 首先,从全局执行上下文中,取出 add 函数代码。
  2. 其次,对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。
  3. 最后,执行代码,输出结果。
js
var a = 2
function add(){
var b = 10
return  a+b
}
add()
  • 全局上下文

图 3

  • 函数上下文为例子

图 2

上面的图中 发现在上下文中看到两个概念,词法环境(LexicalEnvironment)变量环境(VariableEnvironment) 都属于词法环境(Lexical Environment) 大的定义 。

构成执行上下文的三个不同部分 又可以分为三种 LexicalEnvironment/VariableEnvironment/PrivateEnvironment

  • LexicalEnvironment:它是用于描述代码在执行过程中如何访问变量和函数声明的环境。LexicalEnvironment 主要用于处理 let、const 和函数声明等。

  • VariableEnvironment:它与 LexicalEnvironment 类似,但主要用于处理 var 声明的变量。在 ES6 之前,JavaScript 只有 var 声明,因此 VariableEnvironment 和 LexicalEnvironment 实际上是相同的。从 ES6 开始,由于引入了 let 和 const,LexicalEnvironmentVariableEnvironment 开始有所区别。

  • PrivateEnvironment:它是 ECMAScript 规范中描述私有字段(private fields)的概念。私有字段是 ES6 引入的类(class)中的一种特殊字段,它们以 # 开头,只能在类的内部访问。PrivateEnvironment 用于存储这些私有字段及其值

词法环境(Lexical Environment)

变量环境(VE) :这个概念已经被 词法环境(Lexical Environment) 替代。用于描述代码在执行过程中如何访问变量和函数声明。每个代码块(如函数、模块或全局作用域)都有一个与之关联的 Lexical Environment。Lexical Environment 主要包含两个部分:Environment RecordReference to outer lexical environment

  • Environment Record(环境记录) :这是词法环境的一个组成部分,它实际上是一个存储标识符与变量映射的数据结构。换句话说,环境记录就是用来存储在当前作用域中声明的变量、函数和类的地方
  • Reference to outer lexical environment(对外部词法环境的引用) :这是一个指向外部词法环境的引用,用于实现作用域链。当在当前作用域中找不到某个变量时,引擎会沿着这个引用查找外部词法环境,直到找到该变量或者到达全局作用域。

词法环境(Lexical Environment)Environment Record(环境记录) 已经替代了之前 变量对象(VO)和活动对象(AO) 相比之前分类更细致,环境记录用于存储变量、函数声明和类声明等标识符。它包括声明式环境记录(用于存储 var、let、const 声明的变量,以及 function 和 class 声明的函数和类)和对象环境记录(用于存储通过 with 语句和全局对象创建的变量)又可以分为五类

  • Declarative Environment Records(声明式环境记录):记录 var、const、let、class、import、function 等声明。
  • Object Environment Records(对象环境记录):与某一对象相绑定,记录该对象中具有 string 标识符的属性。主要用于 with 语句创建。
  • Global Environment Records(全局环境记录):包含顶层声明和全局对象的属性,类似于声明式环境记录和对象环境记录的结合。
  • Function Environment Records(函数环境记录):用于函数的顶层,提供 this 和 super 方法的绑定(如果非箭头函数和引用了 super)。
  • Module Environment Records(模块环境记录):用于 ES module 的顶层,包含常量、变量声明和不可变的 import 绑定。

作用域链

其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer

当一段代码使用了一个变量时,JavaScript 引擎首先会在 当前的执行上下文 中查找该变量,如果没有找到讲会通过 outer 找到他的上级作用域

js
function bar() {
    console.log(myName)
}
function foo() {
    var myName = " 极客邦 "
    bar()
}
var myName = " 极客时间 "
foo()

图 8

从图中可以看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链

执行

整个代码在执行最开始的过程时候,JS引擎为了执行代码,引擎内部会有一个执行上下文栈(Execution Context Stack,简称ECS),JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中

图 5

全局执行上下文

有了能运行管理上下文栈(Execution Context Stack,简称ECS) 这个容器,下一步就要首先将全局环境变量压入这个栈中也就是全局执行上下文(GEC)

**全局执行上下文(GEC)**是在打开页面时、首次加载一个 JavaScript 文件或运行一段 JavaScript 代码之前创建的。在全局代码执行之前,JavaScript 引擎会先创建一个全局执行上下文,并将其压入执行环境栈中。因此,全局执行上下文位于执行环境栈的底部。它包含主要部分

  • 执行代码前的变量作用域提升(hoisting)和实际的代码执行。在这个过程中,全局定义的变量、函数等会被添加到全局对象中, 创建this并将其绑定到全局对象
  • 将变量声明存储在内存堆和全局执行上下文中的变量中,初始值为undefined,函数会提前声明,这个过程也称之为变量的作用域提升
  • 在代码执行阶段,这些变量和函数会被赋予实际的值

补充说明关于 global object 全局对象 (在不同环境中表现不同在浏览器中为window在node 中为global,当然在新es2020规范可以用globalThis代替),新规范是为了解决,跨环境访问全局对象的标准方法中提出要提供全局变量,在过去览器中为window, frames在WebWorkers中self,node环境中为global,但现在为统一目前已经指定了对应的标准,称之为globalThis,使用场景假设我们的环境是一个浏览器,我们将在这里使用 window。如果脚本可能在其他环境中运行,最好使用 globalThis。在新的浏览器执行 globalThis === window 结果是 true因此实际上全局对象为提供在任何地方都可用的变量和函数。默认情况下,那些内置在语言或环境中的一些属性和方法放一个单独的内存中(Date、Array、String、Number、setTimeout、setInterval),还有通过var/function声明的变量是直接存储到全局对象中

在这个js运行的生命周期中,【GEC将只创建一次,只有一个】 ,即一个程序中只能存在一个全局执行上下文, 只有页面关闭时才会释放掉,刷新页面时,会把之前的上下文全都释放调,然后创建全新的上下文

图 5

js
var b = 1
const a = {}
function bFun() {
    console.log(this) // a
}
bFun()

图 1

在上图中的 Global 中还包含了 我们常见的(Date、Array、String、Number、setTimeout、setInterval)这些全局对象,基于var/function声明的变量是直接存储到GO对象上,也就是说function 和 var声明的变量都是挂在到 'window' 上的

图 2

函数执行上下文

全局执行栈是 js 代码第一步必须有,这样才能创建一个 代码整体运行环境步骤,函数执行上下文(FEC)是在函数调用时创建的,而不是在声明函数时创建的。它的作用是为每个函数调用创建一个执行上下文,这个执行上下文可以保护函数内部的私有变量不受外界干扰。每次调用同一个函数时,都会创建一个新的私有执行上下文(这个环境包含了函数的参数、局部变量和内部函数。

  1. 函数执行上下文FEC,作用是为每个函数调用创建一个执行上下文。但它不是在声明函数时创建的,而是在调用函数时创建
  2. 创建一个新的函数的上下文会被添加到执行栈的顶部
  3. 这个私有上下文可以保护里面的私有变量和外界互不干扰
  4. 即使是同一个函数但每一次被调用,都会创建一个新的私有上下文。
  5. 每当一个函数执行完毕,则这个函数的私有执行上下文也将从栈中弹出,等到所有函数都运行完毕,要关闭页面的时候,全局上下文也将出栈释放,程序运行结束。
  6. 只要当前上下文中的某些内容,被当前上下文以外的东西占用,那么当前上下文是不能被释放的闭包
  7. 调用过程,在创建函数上下文过程中,默认情况下,JS 引擎会在本地执行上下文中创建一个 arguments 对象和一个this对象,函数内部预期的键值对参数存储在 arguments 对象中。它还有一个名为 length 的默认属性,用于计算该函数有多少个参数。当函数的参数为​​空时,参数对象默认为长度:0。根据调用函数的方式,函数执行上下文中的this对象会发生变化。如果使用对象引用来调用它,则 this 的值设置为该对象。否则,此变量的值将设置为gloab对象或严格模式下为undefined。

图 3

动图

  • ES3 函数活动对象AO(Activation Object),而一个函数内的变量包括:形参变量、局部变量、自身函数对象引用量、arguments、this。 为了保存这些变量,所以特意创建了一个对象,称它为AO,它在创建函数的过程中,函数内部的代码不管对还是错我都可以成功加载到页面中,因为我们还没有调用执行它,也就是我们声明一个函数时,你又没有调用它,那么它就失去了它的作用,相当于内部储存一堆字符串 在 ES5 + 换到了 Function Environment Records(函数环境记录)

图 4

综合案例

js
var a = 2
function add(b,c){
  return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)

console.log(d)
  • 从下到下执行后全部压入栈中的效果

图 6

  • 执行结果报错b is not defined 原因是 b 属于 函数自己执行上下文中的 Function Environment Records(函数环境记录)记录当执行完后自动也会被消失的不属于全局的Go,如果函数上下文 已经出栈了 图 7

块执行上下文

如果在大括号(判断体/循环体/代码块)即'块',中出现了 let/const/function/class 等关键词声明变量,则当前大括号会产生一个'块级私有上下文';它的上级上下文是所处的环境;var不产生,也不受块级上下文的影响,简单的说var 没有块级作用域

js
console.log(a)
var a = 12
let b = 13
if (1 == 1) {
    console.log(a)
    var a = 100
    let b = 200
    console.log(a)
    console.log(b)
}
console.log(a)
console.log(b)
打印结果
undefined
12
100
200
100
13

图 5

在块级作用域中声明的function会在全局作用域创建一份副本。但需要注意的是,块级作用域中的function副本与全局作用域的function并不相互影响。在块级作用域之前调用全局作用域的function时,只会得到声明而没有定义的值,而在块级作用域内部,则会同时具备声明和定义的特性

js
console.log(foo)  // 因为块级声明的foo function 在块级之前全局里调用只是声明不在具有定义性质因此undefined
if (1 === 1) {
    console.log(foo) // 在块级时候是声明加定义
    function foo() {}
    foo = 1
    console.log(foo) // 重新给foo 赋值
}
console.log(foo) // 全局的foo function 会复制一份从块,此时二者没有关系,因此块级中function改变并不影响全局

// 打印结果
undefined
[Function: foo]
1
[Function: foo]

整体案例总结

js
// global environment
let a = 1;
const b = 2;
function f(arg1) {
  let c = arg1;
  var d = 3;
}
f(a);
  • 在执行的时候创建 创建GEC
js
GEC = { 
	// lexicalEnvironment表示词法环境 
	lexicalEnvironment: { 
		// environmentRecord表示环境记录 
		environmentRecord: { 
			// type表示环境记录的类型,这里是全局环境
			 type: "Global",
			  // declarativeRecord表示声明式环境记录
				 declarativeRecord: { 
					// type表示环境记录的类型,这里是声明式环境记录 
					type: "Declarative", 
					// a和b是变量名,<uninitialized>表示未初始化 
					a: '<uninitialized>', 
					b: '<uninitialized>',
					// f是函数名,<function>表示函数对象	 
					f: "<function>"
				}, 
				// objectRecord表示对象式环境记录 
				objectRecord: { 
					// type表示环境记录的类型,这里是对象式环境记录 
					type: "Object", 
					// Infinity是全局对象的属性,表示正无穷大 
					Infinity: "+∞", 
					// isFinite是全局对象的属性,表示判断一个数是否有限 
					isFinite: "<function>" } 
				}, 
						// refToOuter表示对外部环境的引用,这里是null表示没有外部环境
				refToOuter: null 
			} 
	}
  • 函数被调用时候结构
js
FEC = {
  lexicalEnvironment: {
    environmentRecord: {
      type: "Function",
      Arguments: {0: 1, length: 1},
      c: <uninitialized>,
      this: <Global Object>
    },
    refToOuter: GEC // global execution context
  },
  variableEnvironment: {
    environmentRecord: {
      type: "Function",
      d: undefined,
      this: <Global Object>
    },
    refToOuter: GEC // global execution context
  }
}

文章参考

浏览器工作原理与实践_李兵

JavaScript的执行过程

A horrifying globalThis polyfill in universal JavaScript

Global object

19 The Global Object

proposal-global

JavaScript Execution Context

JavaScript Execution Context – How JS Works Behind The Scenes

结合 JavaScript 规范来谈谈 Execution Contexts 与 Lexical Environments

JavaScript execution context — from compiling to execution (part 1-4)

A Complete Guide for JavaScript Execution Context

Explaining JavaScript's Execution Context And Stack

Variable scope, closure

JavaScript Closures: A Step-by-Step Guide (Examples)

how-js-works-lexical-environment

Executable Code and Execution Contexts

How do JavaScript’s global variables really work?

Released under the MIT License.