从 ECMAScript 6 角度谈谈执行上下文
大家好,我是归思君
起因是最近了解JS执行上下文的时候,发现很多书籍和资料,包括《JavaScript高级程序设计》、《JavaScript权威指南》和网上的一些博客专栏,都是从 ES3 角度来谈执行上下文,用ES6规范解读的比较少,所以想从ES6的角度看一下执行上下文。
下面我尝试用ECMAScript 6规范文档,来聊聊执行上下文,文章主要从这几个方面介绍:
- ES6规范中的词法环境
- ES6规范中定义的执行上下文结构
- 从ES6规范看实际代码的执行流程
一、 什么是执行上下文
咱们先来看看 ES6 中怎么定义执行上下文的:
An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation. At any point in time, there is at most one execution context that is actually executing code. This is known as the running execution context.
A stack is used to track execution contexts. The running execution context is always the top element of this stack. A new execution context is created whenever control is transferred from the executable code associated with the currently running execution context to executable code that is not associated with that execution context. The newly created execution context is pushed onto the stack and becomes the running execution context.执行上下文是一种规范类型,用于跟踪 ECMAScript 实现(也就是 JavaScript 语言)代码的执行状态。在任意(代码执行)的时间点中,最多有一个执行上下文在实际执行代码。这称为运行执行上下文。
堆栈用于跟踪执行上下文。正在运行的执行上下文始终是该堆栈的顶部元素。每当控制从与当前运行的执行上下文关联的可执行代码转移到不与该执行上下文关联的可执行代码时,就会创建新的执行上下文。新创建的执行上下文被压入堆栈并成为正在运行的执行上下文。
为什么执行上下文是一种“specification device”呢?
因为 EcmaScript
实际上是由 ECMA(European Computer Manufactures Association, 欧洲计算机制造协会) 制定的一种语言规范,而像 JavaScript、Adobe ActionScript 都是 ECMAScript 的一种实现,所以上述描写中的执行上下文,是一种在规范下的定义。
从上面的定义可知:
- 执行上下文是 JavaScript 执行代码时的运行环境
- 跟踪执行上下文的堆栈是执行上下文调用栈(call stack),正在运行的执行上下文的是栈顶元素
- 在执行上下文切换新的可执行代码时,会创建新的执行上下文(函数调用)
在分析执行上下文时,先来了解一下词法环境的概念:
二、Lexical Environments(词法环境)
A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code.
词法环境是一种规范类型,在词法嵌套的 ECMAScript 代码中,用于定义标识符与特定变量和函数关联,也就是说JS中的变量和函数存在这个词法环境中
通常当function
声明,with
语句或try..catch
语句执行时,都会有一个新的词法环境被创建
根据ES6的规范,Lexical Environments
主要由两个部分组成:
A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
Environment Record
(环境记录项)outer Lexical Environment
(外部词法环境的引用)
1.Environment Record
(环境记录项)
ES6规范中,是这样定义Environment Record
的:
An Environment Record records the identifier bindings that are created within the scope of its associated Lexical Environment. It is referred to as the Lexical Environment’s EnvironmentRecord
一个环境记录项记录着,在其关联的词法环境内创建的标识符绑定,它被称为词法环境的环境记录
可以将Environment Record
(环境记录项)看成在存储词法环境中,与标识符绑定的变量和函数的对象。
从规范角度,Environment Record
(环境记录项)可以视作一个面向对象结构的抽象类,并且拥有三个子类
For specification purposes Environment Record values are values of the Record specification type and can be thought of as existing in a simple object-oriented hierarchy where Environment Record is an abstract class with three concrete subclasses,
此外声明式环境记录项中还有function Environment Record
和 module Environment Record
两种类型
declarative Environment Record
(声明式环境记录项)function Environment Record
(函数式环境记录项)module Environment Record
(模块式环境记录项)
object Environment Record
(对象式环境记录项)global Environment Record
(全局式环境记录项)
1.1 declarative Environment Record
(声明式环境记录项)
Declarative Environment Records are used to define the effect of ECMAScript language syntactic elements such as FunctionDeclarations, VariableDeclarations, and Catch clauses that directly associate identifier bindings with ECMAScript language values.
声明式环境记录项用于定义那些将标识符与ECMAScript 语言值绑定的ECMAScript 语法元素,比如 FunctionDeclarations(function 声明), VariableDeclarations(var 声明), and Catch clauses(catch 语句)
像日常使用的var
,let
、const
、function
声明的变量,就存放在declarative Environment Record
这种词法环境中,比如如下变量和函数都会放在声明式环境记录项中:
//所有元素,包括a,b,c,e都绑定在声明式环境记录项中
function foo(a){
var b = 10;
function c() {}
}
try {
...
} catch(e){}
声明式环境记录项又分为function Environment Record
(函数式环境记录项) 和module Environment Record
(模块式环境记录项)
function Environment Record
(函数式环境记录项)
函数式环境记录项是声明式环境记录项的一种,用于表示函数顶级作用域。有以下特殊情况需要注意:
- 除箭头函数外的其他函数,其环境记录项都会进行
this
绑定 - 非箭头函数且有
super
引用的函数,其环境记录项会包含从函数内部执行super
方法调用的状态
除了声明式环境记录项的规范方法外,还有以下字段:
字段名 | 值 | 解释 |
---|---|---|
[[thisValue]] |
任意值 | 用于此函数调用的this值 |
[[thisBindingStatus]] |
“lexical”, “initialized”, “uninitialized” | 如果值为"lexical",说明是箭头函数,该函数也不会拥有this值 |
[[FunctionObject]] |
Object | 表示被调用的函数对象,一旦这个函数对象被调用,此环境记录项就会创建 |
[[HomeObject]] |
Object, undefined |
如果该函数拥有super属性值,并且不是箭头函数。[[HomeObject]] 指函数作为方法绑定的对象,默认值为undefined |
[[NewTarget]] |
Object, undefined |
如果该环境记录项是由[[Construct]] 内部方法创建的,[[NewTarget]] 的值是[[Construct]] 中newTarget参数的值。默认值为undefined |
module Environment Record
(模块式环境记录项)
A module Environment Record is a declarative Environment Record that is used to represent the outer scope of an ECMAScript Module. In additional to normal mutable and immutable bindings, module Environment Records also provide immutable import bindings which are bindings that provide indirect access to a target binding that exists in another Environment Record.
模块式环境记录项也是声明式环境记录项的一种,用于表示ECMAScript 模块的外部范围。除了正常的可变和不可变绑定之外,模块环境记录还提供不可变导入绑定,这些导入绑定提供了对另一个环境记录中存在的目标绑定的间接访问。
用自己的话解释就是,它不仅包括模块的顶级声明外,还包括由模块显式导入的绑定。其outer
值指向全局环境的词法环境:
moduleEnvironment = {
environmentRecord: {
...
}
//引用全局
outer: global.LexicalEnvironment
}
1.2 object Environment Record
(对象式环境记录项)
Each object Environment Record is associated with an object called its binding object. An object Environment Record binds the set of string identifier names that directly correspond to the property names of its binding object.
每一个对象式环境记录项都有一个关联的对象,这个对象被称作绑定对象。对象式环境记录项直接将一系列标识符与其绑定对象的属性名称建立一一对应关系。
对象式环境记录项记录其绑定对象的属性名称以及对应值,比如对于一个对象和对应的对象式环境记录项:
var obj = {
name: "obj",
number: 1
}
假设其在浏览器环境下,则其伪代码如下:
obj.lexicalEnvironment = {
environmentRecord: { name: "obj", number: 1},
outer: window.lexicalEnvironment
}
此外,对象是环境记录项用在with
声明语句中,每当with语句执行时,都会创建一个带有对象环境记录的新词法环境,比如下面的代码:
var a = 10;
var b = 20;
with ({a: 30}) {
//这里创建了一个新词法环境,
//内部的环境项和with内部声明的对象一一绑定
console.log(a + b);//50
}
console.log(a + b);//30
假设其在浏览器全局作用域下,那么其伪代码如下:
//全局环境下词法环境,初始状态
window.lexicalEnvironment = {
environmentRecord: {a: 10, b: 20},
outer: null
};
//当执行到with语句时
//1.暂存当前词法环境
previousEnvironment = window.lexicalEnvironment;
//2.创建一个新的词法环境
withEnvironment = {
environmentRecord: {a:30},
outer: window.lexicalEnvironment
};
//3.替代当前词法环境
window.lexicalEnvironment = withEnvironemt;
//with语句执行完后,复原词法环境
context.lexicalEnvironment = previousEnvironment;
1.3 global Environment Record
(全局式环境记录项)
A global Environment Record is used to represent the outer most scope that is shared by all of the ECMAScript Script elements that are processed in a common Realm (8.2). A global Environment Record provides the bindings for built-in globals (clause 18), properties of the global object, and for all top-level declarations (13.2.8, 13.2.10) that occur within a Script.
全局环境记录用于表示在共同领域中处理的所有ECMAScript脚本元素共享的最外部作用域。全局环境记录为内置全局变量,全局对象的属性以及脚本中发生的所有顶级声明提供了绑定。
用伪代码可以表示为:
globalEnvironment = {
environmentRecord: {
type: "global",
},
outer: null
}
逻辑上全局式环境记录项只有一个,当它实际上是对象式环境记录项和声明式环境记录项的复合对象。全局式环境记录项基于领域中的全局对象,此外包含所有内置全局变量的绑定,FunctionDeclaration
引入的所有绑定,以及GeneratorDeclaration
,AsyncFunctionDeclaration
,AsyncGeneratorDeclaration
或VariableStatement
全局代码。
全局环境记录项中有这些字段
字段名 | 值 | 解释 |
---|---|---|
[[ObjectRecord]] |
Object Environment Record | 绑定对象是global object。 它包含全局内置绑定以及FunctionDeclaration , GeneratorDeclaration , 和 VariableDeclaration 在全局代码中绑定相关联的领域(realm). |
[[DeclarativeRecord]] |
Declarative Environment Record | 包含除了FunctionDeclaration ,GeneratorDeclaration 和VariableDeclaration 绑定之外的关联作用域代码的全局代码中的所有声明的绑定. |
[[VarNames]] |
List of String | 由相关领域的全局代码中的FunctionDeclaration ,GeneratorDeclaration 和VariableDeclaration 声明绑定的字符串名称。 |
2.outer Lexical Environment
(外部词法环境的引用)
The outer environment reference is used to model the logical nesting of Lexical Environment values. The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment.
外部词法环境引用用于表示词法环境的逻辑嵌套关系模型。(内部)词法环境的外部引用是逻辑上包含内部词法环境的词法环境。
outer
是指向外部词法环境的引用,它在不同环境下,其值会随之不同:
- 全局环境下,没有词法环境对其进行包围,所以其词法环境的
outer
为null
- 模块环境下,全局环境将其包围,因此其
outer
指向全局环境的词法环境 - 函数环境下,其外部词法环境是该函数声明时包围其的词法环境,比如:
//声明a时,处于全局环境下,因此a词法环境的outer指向全局环境的词法环境
function a(){ //a:lexicalEnvironment.outer = global.lexicalEnvironment
console.log(name);
}
function b(){ //b:lexicalEnvironment.outer = global.lexicalEnvironment
var name = "b";
a();//global
}
var name = "global";
b();//global
如果将函数的声明放在嵌套函数词法环境内部:
function b(){ //b:lexicalEnvironment.outer = global.lexicalEnvironment
var name = "b";
//声明a时,处于b的词法环境下,因此a词法环境的outer指向b的词法环境
function a(){ //a:lexicalEnvironment.outer = b.lexicalEnvironment
console.log(name);
}
a();//b
}
var name = "global";
b();//b
发现没,如果把嵌套的不同词法环境的outer
值连接在一起,就形成了一条作用域链。
举个例子,在浏览器环境下的多个嵌套函数,其作用域链为: foo3->foo2->foo1->windows
//作用域链 foo3->foo2->foo1->windows
function foo1() {
//...
function foo2() {
//...
function foo3() {
//...
}
}
}
介绍完词法环境,下面就进入正题,具体来看看执行上下文的结构:
三.执行上下文的结构
Execution context has state components
执行上下文拥有以下组件
组件 | Purpose |
---|---|
code evaluation state | Any state needed to perform, suspend, and resume evaluation of the code associated with this execution context. 记录执行上下文代码执行、挂起和恢复等状态 |
Function | If this execution context is evaluating the code of a function object, then the value of this component is that function object. If the context is evaluating the code of a Script or Module, the value is null. 如果当前执行上下文正在执行的是函数对象的代码,Function 值指向正在执行的函数对象,如果是执行的是脚本和模块,该值为 null。正在运行的执行上下文的 Function 值也称为活动函数对象 |
Realm | The Realm from which associated code accesses ECMAScript resources. 关联代码访问ECMAScript资源,指代当前上下文所属领域的资源,包括全局对象、与此领域相关的代码使用的内在值等等,用于隔离其他领域 |
LexicalEnvironment |
Identifies the Lexical Environment used to resolve identifier references made by code within this execution context. 标识符,标识用于解析此执行上下文中引用的词法环境,let 和const 声明的变量会挂载到该标识符引用的词法环境中 |
VariableEnvironment |
Identifies the Lexical Environment whose EnvironmentRecord holds bindings created by VariableStatements within this execution context. 标识符,标识词法环境,其绑定由 var 声明的EnvironmentRecord,也就是var声明的变量会存储在此环境中 |
Generator | The GeneratorObject that this execution context is evaluating. 记录当前正在解析的执行器对象 |
用伪代码表示:
ExecutionContext = {
codeEvaluationState,
Function,
Realm,
LexicalEnviroment: {...},
VariableEnvironment: {...},
Generator: {...},
}
1. code evaluation state
At some later time a suspended execution context may again become the running execution context and continue evaluating its code at the point where it had previously been suspended. Transition of the running execution context status among execution contexts usually occurs in stack-like last-in/first-out manner.
code evaluation state
是记录当前上下文在上下文执行栈中的状态,用于切换栈中的不同执行上下文,主要有:
- perform(执行)
- suspend(挂起)
- resume(恢复)
2. Function
Function
值是记录当前执行上下文是否为函数执行上下文:
- 若当前是全局或函数执行上下文,其值为全局或函数
- 若当前是脚本_Script_或模块_Module_ ,其值为
null
3. Realm
Before it is evaluated, all ECMAScript code must be associated with a Realm. Conceptually, a realm consists of a set of intrinsic objects, an ECMAScript global environment, all of the ECMAScript code that is loaded within the scope of that global environment, and other associated state and resources.
根据ES6的定义,所有ECMAScript
代码都有一个与之关联的Realm
领域。一个realm
由一系列内置对象,一个ECMAScript
全局环境,加载到全局环境中的ECMAScript
代码以及其他关联状态和资源组成。
Realm
以Realm Record
的形式来表示,一个Realm Record
主要由以下字段组成:
字段名 | 值 | 解释 |
---|---|---|
[[intrinsics]] |
Objects | 当前Realm中的内部固有对象,比如Object ,Function ,Boolean 等 |
[[globalThis]] |
Object | 当前Realm中的全局对象 |
[[globalEnv]] |
Lexical Environment | 当前Realm中的词法环境 |
[[templateMap]] |
A List of Record {[[strings]]: List, [[array]]: Object} | 当前Realm中的模版(比如字符串模版)的存储信息,比如JavaScript具体实现中,是用来存储模板字符串(template string)的缓存。下次再找模版会优先从此处查询 |
Realm
是ECMAScipt
规范定义的一个概念,和上节提到的作用域概念有些重合。事实上Realm
包含了作用域概念,除了作用域的变量和函数,它还加上了内置对象,比如Object
,Function
,Boolean
等,以及加载到全局环境中的其他代码等。
实际上在浏览器环境中,window
是就是一个Realm
, node中的global也是一个Realm
,对比我们平常熟知的作用域概念,Realm更符合JS代码实际执行中需要的“执行环境”。
4. LexicalEnvironment
Identifies the Lexical Environment used to resolve identifier references made by code within this execution context.
标识用于解析在此执行上下文中由代码创建的标识符引用的词法环境。一般是
let
,const
声明的变量存储在该词法环境中
这里要和Lexical Environment
词法环境(中间有空格)区分一下:
LexicalEnvironment
是执行上下文中的一个标识符,引用的是存储let
,const
声明变量的词法环境Lexical Environment
是ES6规范定义的一个概念,包括 Environment Record 和 outer 引用两个部分
5. VariableEnvironment
Identifies the Lexical Environment whose EnvironmentRecord holds bindings created by VariableStatements within this execution context.
标识执行上下文中的词法环境,其词法环境是在
var
声明创建绑定的词法环境,也就是这个词法环境存储的是var
声明的变量
无论是LexicalEnvironment
还是LexicalEnvironment
,在执行上下文中都是词法环境。在执行上下文创建时,其内部的LexicalEnvironment
和LexicalEnvironment
值相等。
除了这些字段,执行上下文中还有一些抽象方法。下面根据上下文中的抽象方法,来看看执行上下文中的this
值是怎样变化的:
6. 执行上下文中的this
值
执行上下文中主要通过GetThisEnvironment ( )
来确定,来看看ES6规范里面是怎么说的:
The abstract operation GetThisEnvironment finds the Environment Record that currently supplies the binding of the keyword this.
抽象操作 GetThisEnvironment 查找当前提供关键字this绑定的环境记录
执行上下文在实际执行中,通过调用GetThisEnvironment ( )
来获取其this
绑定值。其具体执行步骤如下
GetThisEnvironment performs the following steps:
- Let lex be the running execution context’s LexicalEnvironment.
- Repeat
a. Let envRec be lex’s EnvironmentRecord.
b. Let exists be envRec.HasThisBinding().
c. If exists is true, return envRec.
d. Let outer be the value of lex’s outer environment reference.
e. Let lex be outer.
获取当前执行上下文的this
值可以用如下伪代码表示:
function GetThisEnvironment(){
var lex = currentLexicalEnvironment;
while(true){
var envRec = lex.EnvironmentRecord;
if(envRec.HasThisBinding()){
return envRec;
}
lex = envRec.outer;
}
}
//返回一个提供this绑定的环境记录项
var envRec = GetThisEnvironment();
//通过环境记录项内部抽象方法获取this值
envRec.GetThisBinding();
四、 执行上下文栈 ( Call Execution stack )
先来看ES6规范中是如何定义执行上下文栈的:
At any point in time, there is at most one execution context that is actually executing code. This is known as the running execution context. A stack is used to track execution contexts. The running execution context is always the top element of this stack.
A new execution context is created whenever control is transferred from the executable code associated with the currently running execution context to executable code that is not associated with that execution context. The newly created execution context is pushed onto the stack and becomes the running execution context.
在任意(代码执行)的时间点中,最多有一个执行上下文在实际执行代码。这称为运行执行上下文。堆栈用于跟踪执行上下文。正在运行的执行上下文始终是该堆栈的顶部元素。
每当控制从与当前运行的执行上下文关联的可执行代码转移到不与该执行上下文关联的可执行代码时,就会创建一个新的执行上下文。新创建的执行上下文被压入堆栈并成为正在运行的执行上下文。
从ES6规范我们知道:
- 执行上下文栈是用来跟踪执行上下文的,当前处于栈顶的是正在运行的执行上下文
- 调用其他关联的可执行代码时,会创建一个新的执行上下文,并将这个新的执行上下文压入栈顶
借助一个例子来说明:
function a() {
console.log("function a");
}
function b() {
console.log("function b");
a();
}
//执行b()
b();
在 chrome devtools 中debugger看执行上下文栈的执行情况:
第一步:在执行b()
前会创建一个全局执行上下文,就是下图中的(anonymous)
第二步:将b()
函数执行上下文压入栈中:
第三步:当b()
调用a()
时,将a()
函数执行上下文继续压入栈:
第四步:执行完a()
后,将a()
函数执行上下文出栈:
第五步:执行完b()
后,将b()
函数执行上下文出栈,最后只留下全局执行上下文
五、从 ECMAScript6 角度看代码的执行流程
代码的执行主要分为两个阶段:
- 编译阶段
- 执行阶段
下面以这一段代码,用 ECMAScript 6 规范解读代码的执行流程。
var a = 10;
let b = 20;
const c = 30;
function add(d, e) {
var f = 40;
return d + e + f;
}
foo(50, 60);
在开始前,先回顾一下ES6规范中的执行上下文,用伪代码表示:
ExecutionContext = {
codeEvaluationState, //记录当前上下文在上下文执行栈中的状态
Function, //当前执行上下文在执行中是否有函数对象,有的话Function值就指向这个函数对象
Realm, //当前执行上下文的领域/作用域
LexicalEnviroment: {...}, //let,const等变量声明存储在此类词法环境
VariableEnvironment: {...},//var变量声明存储在此类词法环境
Generator: {...},//当前执行上下文在执行中是否有生成器函数,有的话Generator值就指向这个生成器函数
}
在日常代码分析中,在执行上下文中,对codeEvaluationState
,Function
,Realm
和Generator
关注的较少,我们着重分析LexicalEnviroment
,VariableEnvironment
和其记录项中的this
绑定值。下面就开始分析吧:
1.编译阶段
在这个阶段,JS引擎会扫描变量和函数声明,创建一个全局上下文,做好执行之前的代码编译和初始化工作,用伪代码表示:
//全局上下文
GlobalExectionContext = {
//词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// let和const变量声明不会提升
b: < uninitialized >,
c: < uninitialized >,
// add 进行函数提升
add: < func >,
//记录项的this值绑定到全局对象
ThisBinding: <Global Object>,
}
outerEnv: <null>,
},
//变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// var变量声明会进行提升
a: undefined,
ThisBinding: <Global Object>
}
outerEnv: <null>,
}
}
上述的执行上下文对应代码范围如下:
var a = 10;
let b = 20;
const c = 30;
function add(d, e) {
var f = 40;
return d + e + f;
}
当add(d,e)
函数被调用时,会创建一个函数执行上下文,并将这个上下文压入调用栈中,用伪代码表示add(d, e)
函数执行上下文:
//add(d, e)函数执行上下文
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Arguments标识符绑定,并将实参传入其中
Arguments: {0: 50, 1: 60, length: 2},
ThisBinding: <Global Object or undefined>,
},
outerEnv: <GlobalLexicalEnvironment>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
//var声明变量提升
d: undefined,
ThisBinding: <Global Object or undefined>
},
outerEnv: <GlobalLexicalEnvironment>,
}
}
2.执行阶段
执行阶段主要是这一段代码的执行:
foo(50, 60);
此时全局执行上下文的变化为:
let
和const
声明的变量得到赋值:b 赋为 20,c赋为30var
声明的变量 a 由 undefined覆盖为 10
//全局上下文
GlobalExectionContext = {
//词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// let和const声明的变量得到赋值
b: 20,
c: 30,
add: < func >,
//记录项的this值指向到全局对象
ThisBinding: <Global Object>,
}
outerEnv: <null>,
},
//变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// var变量声明会进行提升
a: 10,
ThisBinding: <Global Object>
}
outerEnv: <null>,
}
}
函数执行上下文的变化为:
var
声明的变量 f 由 undefined覆盖为 40- 在
add(d, e)
函数执行上下文在执行完毕后,会返回计算结果值 150
//add(d, e)函数执行上下文
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Arguments标识符绑定,并将实参传入其中
Arguments: {0: 50, 1: 60, length: 2},
ThisBinding: <Global Object or undefined>,
},
outerEnv: <GlobalLexicalEnvironment>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
d: 40,
},
ThisBinding: <Global Object or undefined>,
outerEnv: <GlobalLexicalEnvironment>,
}
}
在函数执行完毕后,该add(d,e)
函数执行上下文会出栈,该函数执行上下文内的变量也随之销毁。
参考文章
https://www.linkedin.com/pulse/javascript-under-hood-part-2-simple-example-execution-kabir
https://blog.openreplay.com/explaining-javascript-s-execution-context-and-stack/
- 点赞
- 收藏
- 关注作者
评论(0)