跟大家聊聊js的執行上下文
一,相關概念
EC : 執行上下文
ECS : 執行環境棧
VO : 變數物件
AO : 活動物件
scope chain :作用域鏈
二,執行上下文
javascript執行的程式碼環境有三種:
全域性程式碼:程式碼預設執行的環境,最先會進入到全域性環境中
函式程式碼:在函式的區域性環境中執行的程式碼
Eval程式碼:在Eval()函式中執行的程式碼
複製程式碼
全域性上下文是最外圍的一個執行環境,web瀏覽器中被認為是window物件。在初始化程式碼時會先進入全域性上下文中,每當一個函式被呼叫時就會為該函式建立一個執行上下文,每個函式都有自己的執行上下文。來看一段程式碼:
function f1() {
var f1Context = 'f1 context';
function f2() {
var f2Context = 'f2 context';
function f3() {
var f3Context = 'f3 context';
console.log(f3Context);
}
f3();
console.log(f2Context);
}
f2();
console.log(f1Context);
}
f1();
複製程式碼
這段程式碼有4個執行上下文:全域性上下文和f1(),f2(),f3()屬於自己的執行上下文。
全域性上下文擁有變數f1(),f1()的上下文中有變數f1Context和f2(),f2()的上下文有變數f2Context和f3(),f3()上下文有變數f3Context。
在這我們瞭解下執行環境棧ECS,一段程式碼所有的執行上下文都會被推入棧中等待被執行,因為js是單執行緒,任務都為同步任務的情況下某一時間只能執行一個任務,執行一段程式碼首先會進入全域性上下文中,並將其壓入ECS中,執行f1()會為其建立執行上下文壓入棧頂,f1()中有f2(),再為f2()建立f2()的執行上下文,依次,最終全域性上下文被壓入到棧底,f3()的執行上下文在棧頂,函式執行完後,ECS就會彈出其上下文,f3()上下文彈出後,f2()上下文來到棧頂,開始執行f2(),依次,最後ECS中只剩下全域性上下文,它等到應用程式退出,例如瀏覽器關閉時銷燬。
總結:(執行上下文就用EC替代)
1. 全域性上下文壓入棧頂
2. 執行某一函式就為其建立一個EC,並壓入棧頂
3. 棧頂的函式執行完之後它的EC就會從ECS中彈出,並且變數物件(VO)隨之銷燬
4. 所有函式執行完之後ECS中只剩下全域性上下文,在應用關閉時銷燬
複製程式碼
大家再看一道道題:
function foo(i) {
if(i == 3) {
return;
}
foo(i+1);
console.log(i);
}
foo(0);
複製程式碼
大家明白執行上下文的進棧出棧就應該知道結果為什麼是2,1,0
ECS棧頂為foo(3)的的上下文,直接return彈出後,棧頂變成foo(2)的上下文,執行foo(2),輸出2並彈出,執行foo(1),輸出1並彈出,執行foo(0),輸出0並彈出,關閉瀏覽器後全域性EC彈出,所以結果為2,1,0
剛才提到VO,我們來了解什麼是VO
三,VO/AO
VO(變數物件)
建立執行上下文時與之關聯的會有一個變數物件,該上下文中的所有變數和函式全都儲存在這個物件中。
AO(活動物件)
進入到一個執行上下文時,此執行上下文中的變數和函式都可以被訪問到,可以理解為被啟用
談到了上下文的建立和執行,我們來看看EC建立的過程:
建立階段:(函式被呼叫,但是還未執行函式中的程式碼)
1. 建立變數,引數,函式,arguments物件
2. 建立作用域鏈
3. 確定this的值
執行階段:變數賦值,函式引用,執行程式碼
複製程式碼
執行上下文為一個物件,包含VO,作用域鏈和this
executionContextObj = {
variableObject: { /* 函式中的arguments物件, 引數, 內部的變數以及函式宣告 */ },
scopeChain: { /* variableObject 以及所有父執行上下文中的variableObject */ },
this: {}
}
複製程式碼
具體過程:
1. 找到當前上下文呼叫函式的程式碼
2. 執行程式碼之前,先建立執行上下文
3. 建立階段:
3-1. 建立變數物件(VO):
1. 建立arguments物件,檢查當前上下文的引數,建立該物件下的屬性和屬性值
2. 掃描上下文的函式申明:
1. 每掃描到一個函式什麼就會在VO裡面用函式名建立一個屬性,
為一個指標,指向該函式在記憶體中的地址
2. 如果函式名在VO中已經存在,對應的屬性值會被新的引用覆蓋
3. 掃描上下文的變數申明:
1. 每掃描到一個變數就會用變數名作為屬性名,其值初始化為undefined
2. 如果該變數名在VO中已經存在,則直接跳過繼續掃描
3-2. 初始化作用域鏈
3-3. 確定上下文中this的指向
4. 程式碼執行階段
4-1. 執行函式體中的程式碼,給VO中的變數賦值
複製程式碼
看程式碼理解:
function foo(i) {
var a = 'hello';
var b = function privateB() {};
function c() {}
}
foo(22);
複製程式碼
呼叫foo(22)時建立上下文包括VO,作用域鏈,this值
以函式名作為屬性值,指向該函式在記憶體中的地址;變數名作為屬性名,其初始化值為undefined
注意:函式申明先於變數申明
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c(),
a: undefined,
b: undefined
},
scopeChain: { ... },
this: { ... }
}
複製程式碼
建立階段結束後就會進入程式碼執行階段,給VO中的變數賦值
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c(),
a: 'hello',
b: pointer to function privateB()
},
scopeChain: { ... },
this: { ... }
}
複製程式碼
四,變數提升
function foo() {
console.log(f1); //f1() {}
console.log(f2); //undefined
var f1 = 'hosting';
var f2 = function() {}
function f1() {}
}
foo();
複製程式碼
呼叫foo()時會建立VO,初始VO中變數值等有一系列的過程,所有變數初始化值為undefined,所以console.log(f2)的值為undefined。並且函式申明先於變數申明,所以console.log(f1)的值為f1()函式而不為hosting
五,總結
1. 呼叫函式時會為其建立執行上下文,並壓入執行環境棧的棧頂,執行完畢
彈出,執行上下文被銷燬,隨之VO也被銷燬
2. EC建立階段分建立階段和程式碼執行階段
3. 建立階段初始變數值為undefined,執行階段才為變數賦值
4. 函式申明先於變數申明
複製程式碼