大家好,我是歸思君
起因是最近了解JS執行上下文的時候,發現很多書籍和資料,包括《JavaScript高階程式設計》、《JavaScript權威指南》和網上的一些部落格專欄,都是從 ES3 角度來談執行上下文,用ES6規範解讀的比較少,所以想從ES6的角度看一下執行上下文。
下面我嘗試用ECMAScript 6規範檔案,來聊聊執行上下文,文章主要從這幾個方面介紹:
咱們先來看看 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 的一種實現,所以上述描寫中的執行上下文,是一種在規範下的定義。
從上面的定義可知:
在分析執行上下文時,先來了解一下詞法環境的概念:
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
(外部詞法環境的參照)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
(全域性式環境記錄項)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
}
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;
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 宣告繫結的字串名稱。 |
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: {...},
}
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
是記錄當前上下文在上下文執行棧中的狀態,用於切換棧中的不同執行上下文,主要有:
Function
值是記錄當前執行上下文是否為函數執行上下文:
null
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 | 當前Realm中的模版(比如字串模版)的儲存資訊,比如JavaScript具體實現中,是用來儲存模板字串(template string)的快取。下次再找模版會優先從此處查詢 |
Realm
是ECMAScipt
規範定義的一個概念,和上節提到的作用域概念有些重合。事實上Realm
包含了作用域概念,除了作用域的變數和函數,它還加上了內建物件,比如Object
,Function
,Boolean
等,以及載入到全域性環境中的其他程式碼等。
實際上在瀏覽器環境中,window
是就是一個Realm
, node中的global也是一個Realm
,對比我們平常熟知的作用域概念,Realm更符合JS程式碼實際執行中需要的「執行環境」。
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 參照兩個部分VariableEnvironment
Identifies the Lexical Environment whose EnvironmentRecord holds bindings created by VariableStatements within this execution context.
標識執行上下文中的詞法環境,其詞法環境是在
var
宣告建立繫結的詞法環境,也就是這個詞法環境儲存的是var
宣告的變數
無論是LexicalEnvironment
還是LexicalEnvironment
,在執行上下文中都是詞法環境。在執行上下文建立時,其內部的LexicalEnvironment
和LexicalEnvironment
值相等。
除了這些欄位,執行上下文中還有一些抽象方法。下面根據上下文中的抽象方法,來看看執行上下文中的this
值是怎樣變化的:
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();
先來看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()
函數執行上下文出棧,最後只留下全域性執行上下文
程式碼的執行主要分為兩個階段:
下面以這一段程式碼,用 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
繫結值。下面就開始分析吧:
在這個階段,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>,
}
}
執行階段主要是這一段程式碼的執行:
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覆蓋為 40add(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/
https://blog.openreplay.com/explaining-javascript-s-execution-context-and-stack/