一、什麼是執行上下文
執行上下文(Execution Context),簡稱EC。
網上有很多關於執行上下文定義的描述,簡單理解一下,其實就是作用域,也就是運行這段JavaScript代碼的一個環境。
二、執行上下文的組成和分類
1. 組成
對於每個執行上下文EC,都有三個重要的屬性:
- 變量對象Variable Object(變量聲明、函數聲明、函數形參)
- 作用域鏈 Scope Chain
- this指針
2. 分類
執行上下文分為3類
- 全局執行上下文
- 函數執行上下文
- eval執行上下文(幾乎不用,暫時不做解釋)
全局執行上下文
術語理解
代碼開始執行前首先進入的環境。
特點
全局執行上下文有且只有一個。客户端中一般由瀏覽器創建,也就是window對象。
注意點
(1)使用
var聲明的全局變量,都可以在window對象中訪問到,可以理解為window是var聲明對象的載體。(2)使用
let聲明的全局變量,用window對象訪問不到。
函數執行上下文
術語理解
函數被調用時,會創建一個函數執行上下文。
特點
函數執行上下文可以有多個,即使調用自身,也會創建一個新的函數執行上下午呢。
以上是對全局執行上下文和函數執行上下文的區別。
下面再來看看執行上下文的生命週期。
三、執行上下文的生命週期
執行上下文的生命週期可以分為3個階段:
- 創建階段
- 執行階段
- 回收階段
1. 創建階段
發生在當函數被調用,但是在未執行內部代碼之前。
創建階段主要做的事情是:
(1)創建變量對象Variable Object(創建函數形參、函數聲明、變量聲明)
(2)創建作用域鏈Scope Chain
(3)確定this指向This Binding
我們先用代碼來更直觀的理解下創建階段的過程:
function foo(i){
var a = 100;
var b = function(){};
function c(){}
}
foo(20);
當調用foo(20)的時候,執行上下文的創建狀態如下:
ExecutionContext:{
scopeChain:{ ... },
this:{ ... },
variableObject:{
arguments:{
0: 20,
length: 1
},
i: 20,
c:<function>,
a:undefined,
b:undefined
}
}
2. 執行階段
創建完成後,程序自動進入執行階段,執行階段主要做的事情是:
(1)給變量對象賦值:給VO中的變量賦值,給函數表達式賦值。
(2)調用函數
(3)順序執行代碼
還是以上面的代碼為例,執行階段給VO賦值,用偽代碼表示如下:
ExecutionContext:{
scopeChain:{ ... },
this:{ ... },
variableObject:{
arguments:{
0: 20,
length: 1
},
i: 20,
c:<function>,
a:100,
b:function
}
}
3. 回收階段
所有代碼執行完畢,程序關閉,釋放內存。
上下文出棧後,虛擬機進行回收。
全局上下文只有當關閉瀏覽器時才會出棧。
根據以上內容,我們瞭解到執行上下文的創建需要創建變量對象,那變量對象到底是什麼呢?
四、變量對象 VO 和 活動對象 AO
1. VO 概念理解
變量對象Variable Object,簡稱VO。簡單理解就是一個對象,這個對象存放的是:全局執行上下文的變量和函數。
VO === this === Global
VO的兩種特殊情況:
(1)未經過var聲明的變量,不會存在VO中
(2)函數表達式(與函數聲明相對),也不在VO中
2. AO 概念理解
活動對象Activation Object,也叫激活對象,簡稱AO。
激活對象是在進入函數執行上下文時(函數執行的前一刻)被創建的。
函數執行上下文中,VO是不能直接訪問,所以AO扮演了VO的角色。
VO === AO,並且添加了形參類數組和形參的值
Arguments Object是函數上下文AO的一個對象,它包含的屬性有:
(1)callee:指向當前函數的引用
(2)length:真正傳遞參數的個數
(3)properties-indexes:函數的參數值(按照參數列表從左到右排列)
3. VO 的初始化過程
(1)根據函數參數,創建並初始化arguments
變量聲明var、函數形參、函數聲明
(2)掃描函數聲明
函數聲明,是變量對象的一個屬性,其屬性名和值都是函數對象創建出來的。若變量對象已經包含了相同名字的屬性,則替換它的值。
(3)掃描變量聲明
變量聲明,即變量對象的一個屬性,其屬性名即變量名,其值為undefined。如果變量名和已經聲明的函數名或者函數的參數名相同,則不影響已經存在的屬性。
注:函數聲明優先級高於變量聲明優先級
五、示例分析
1. 如何理解函數聲明中“若變量對象已經包含了相同名字的屬性,則替換它的值”
用代碼來理解一下:
function fun(a){
console.log(a); // function a(){}
function a(){}
}
fun(100);
我們調用了fun(100),傳入a的值是100,為什麼執行console語句後結果卻不是100呢?別急,我們接着分析~
創建階段:
步驟 1-1:根據形參創建arguments,用實參賦值給對應的形參,沒有實參的賦值為undefined
AO_Step1:{
arguments:{
0: 100,
length:1
},
a: 100
}
步驟 1-2:掃描函數聲明,此時發現名稱為a的函數聲明,將其添加到AO上,替換掉已經存在的相同屬性名稱a,也就是替換掉形參為a的值。
AO_Step2:{
arguments:{
0: 100,
length:1
},
a: 指向function a(){}
}
步驟 1-3:掃描變量聲明,未發現有變量。
執行階段:
步驟 2-1:沒有賦值語句,第一行執行console命令,而此時a指向的是funciton,所以輸出function a(){}
2. 如何理解變量聲明中“如果變量名和已經聲明的函數名或者函數的參數名相同,則不影響已經存在的屬性”
用代碼來理解一下
情景1:變量與參數名相同
function fun2(a){
console.log(a); // 100
var a = 10;
console.log(a) // 10
}
fun2(100);
// 分析步驟:
創建階段:
步驟 1-1:根據arguments創建並初始化AO
AO = {
arguments:{
0: 100,
length:1
},
a:100
}
步驟 1-2:掃描函數聲明,此時沒有額外的函數聲明,所以AO還是和上次一致
AO = {
arguments:{
0: 100,
length:1
},
a:100
}
步驟 1-3:掃描變量聲明,發現AO中已經存在了a屬性,所以不修改已存在的屬性。
AO = {
arguments:{
0: 100,
length:1
},
a:100
}
執行階段:
步驟 2-1:按順序執行console語句,此時AO中的a是100,所以輸出100.
步驟 2-2:執行到賦值語句,對AO中的a進行賦值,此時a是10。
步驟 2-3:按順序執行,執行console語句,此時a是10,所以輸出10。
情景2:變量與函數名相同
function fun3(){
console.log(a); // function a(){}
var a = 10;
function a(){}
console.log(a) // 10
}
fun3();
// 分析步驟:
創建階段:
步驟 1-1:根據arguments創建並初始化AO
AO={
arguments:{
length:0
}
}
步驟 1-2:掃描函數聲明,此時a指向函數聲明(Function Declaration)
AO={
arguments:{
length:0
},
a: FD
}
步驟 1-3:掃描變量聲明,發現AO中已經存在了a屬性,則跳過,不影響已存在的屬性。
AO={
arguments:{
length:0
},
a: FD
}
執行階段:
步驟 2-1:執行第一行語句console,此時a指向的是函數聲明,所以輸出函數聲明。
AO={
arguments:{
length:0
},
a: FD
}
步驟 2-2:執行第二句對AO中的變量對象進行賦值,所以a的值改為10。
AO={
arguments:{
length:0
},
a: 10
}
步驟 2-3:執行第三句,是函數聲明,在執行階段不會再將其添加到AO中,直接跳過。所以AO還是上次的狀態。
AO={
arguments:{
length:0
},
a: 10
}
步驟 2-4:執行第四句,此時a的值是10,所以輸出10。
AO={
arguments:{
length:0
},
a: 10
}
根據以上的示例,我們已經大致明白了EC以及EC的生命週期。
同時,我們知道函數每次調用都會產生一個新的函數執行上下文。
那麼,如果有若干個執行上下文呢,JavaScript是怎樣執行的?
這就涉及到 執行上下文棧 的相關知識。
六、執行上下文棧
1. 術語理解
執行上下文棧(Execution context stack,ECS),簡稱ECS。
簡單理解就是若干個執行上下文組成了執行上下文棧。也稱為執行棧、調用棧。
2. 作用
用來存儲代碼執行期間的所有上下文。
3. 特點
我們知道棧的特點是先進後出。可以理解為瓶子,先進來的東西永遠在最底部。
所以
執行上下文棧的特點就是LIFO(Last In First Out)
也就是後進先出。
4. 存儲機制
- JS首次執行時,會將全局執行上下文存入棧底,所以全局執行上下文永遠在最底部。
- 當有函數調用時,會創建一個新的函數執行上下文存入執行棧。
-
永遠是棧頂處於當前正在執行狀態,執行完成後出棧,開始執行下一個。
5. 示例分析
我們用代碼簡單理解一下
示例1:
function f1(){
f2();
console.log(1)
}
function f2(){
f3();
console.log(2)
}
function f3(){
console.log(3)
}
f1(); // 3 2 1
根據執行棧的特點進行分析:
(1)我們假設執行上下文棧是數組ECStack,則ECStack=[globalContext],存入全局執行上下文(我們暫且叫它globalStack)
(2)調用f1()函數,進入f1函數開始執行,創建f1的函數執行上下文,存入執行棧,即ECStack.push('f1 context')
(3)f1函數內部調用了f2()函數,則創建f2的函數執行上下文,存入執行棧,即ECStack.push('f2 context'),f2執行完成之前,f1無法執行console語句
(4)f2函數內部調用了f3()函數,則創建f3的函數執行上下文,存入執行棧,即ECStack.push('f3 context'),f3執行完成之前,f2無法執行console語句
(5)f3執行完成,輸出3,並出棧,ECStack.pop()
(6)f2執行完成,輸出2,並出棧ECStack.pop()
(7)f1執行完成,輸出1,並出棧ECStack.pop()
(8)最後ECStack只剩[globalContext]全局執行上下文
示例2:
function foo(i){
if(i == 3){
return
}
foo(i+1);
console.log(i)
}
foo(0); // 2,1,0
分析:
(1)調用foo函數,創建foo函數的函數執行上下文,存入EC,傳0,i=0,if條件不滿足不執行,
(2)執行到foo(1),再次調用foo函數,創建一個新的函數執行上下文,存入EC,此時傳入的i為1,if條件不滿足不執行,
(3)又執行到foo(2),又創建新的函數執行上下文,存入EC,此時i為2,if條件不滿足不執行
(3)又執行到foo(3),再次創建新的函數執行上下文,存入EC,此時i為3,if滿足直接退出,EC彈出foo(3)
(4)EC彈出foo(3)後執行foo(2)剩下的代碼,輸出2,foo(2)執行完成,EC彈出foo(2)
(5)EC彈出foo(2)後執行foo(1)剩下的代碼,輸出1,foo(1)執行完成,EC彈出foo(1)
(6)EC彈出foo(1)後執行foo(0)剩下的代碼,輸出0,foo(0)執行完成,EC彈出foo(0),此時EC只剩下全局執行上下文。
七、總結
- 全局執行上下文只有一個,並且在棧底。
- 當瀏覽器關閉時,全局執行上下文才會出棧。
- 函數執行上下文可以有多個,並且函數每調用執行一次(即使是調用自身),就會生成一個新的函數執行上下文。
- Js是單線程,所以是同步執行,執行上下文棧中,永遠是處於棧頂的是執行狀態。
- VO或是AO只有一個,創建過程的順序是:參數聲明>函數聲明>變量聲明
- 每個EC可以抽象為一個對象,這個對象包含三個屬性:作用域鏈、VO/AO、this