文章目錄
- 壹 ❀ 引(出現現象)
- 貳 ❀ JS執行上下文
-
- 1.全局執行上下文
- 2.函數執行上下文
- 叁 ❀ 執行上下文棧(執行棧)
- 肆 ❀ 執行上下文建立階段
-
- 1.确定this
- 2.詞法環境元件
- 3.變量環境元件
- 伍 ❀ 關于變量對象與活動對象
- 陸 ❀ 總結
壹 ❀ 引(出現現象)
我們都知道,JS代碼的執行順序總是與代碼先後順序有所差異,當先抛開異步問題你會發現就算是同步代碼,它的執行也與你的預期不一緻,比如:
function f1() {
console.log('聽風是風');
};
f1(); //echo
function f1() {
console.log('echo');
};
f1(); //echo
按照代碼書寫順序,應該先輸出 聽風是風,再輸出 echo才對,很遺憾,兩次輸出均為 echo;如果我們将上述代碼中的函數聲明改為函數表達式,結果又不太一樣:
var f1 = function () {
console.log('聽風是風');
};
f1(); //聽風是風
var f1 = function() {
console.log('echo');
};
f1(); //echo
這說明代碼在執行前一定發生了某些微妙的變化,JS引擎究竟做了什麼呢?這就不得不提JS執行上下文的了。
貳 ❀ JS執行上下文
JS代碼在執行前,JS引擎總要做一番準備工作,這份工作其實就是建立對應的執行上下文;
執行上下文有且隻有三類,全局執行上下文,函數上下文,與eval上下文;由于eval一般不會使用,這裡不做讨論。
1.全局執行上下文
- 全局執行上下文隻有一個,在用戶端中一般由浏覽器建立,也就是我們熟知的window對象,我們能通過this直接通路到它。
- 全局對象window上預定義了大量的方法和屬性,我們在全局環境的任意處都能直接通路這些屬性方法,同時window對象還是var聲明的全局變量的載體。我們通過var建立的全局對象,都可以通過window直接通路。
2.函數執行上下文
函數執行上下文可存在無數個,每當一個函數被調用時都會建立一個函數上下文;需要注意的是,同一個函數被多次調用,都會建立一個新的上下文。
說到這你是否會想,上下文種類不同,而且建立的數量還這麼多,它們之間的關系是怎麼樣的,又是誰來管理這些上下文呢,這就不得不說說執行上下文棧了。
叁 ❀ 執行上下文棧(執行棧)
-
執行上下文棧(下文簡稱執行棧)也叫調用棧,執行棧用于存儲代碼執行期間建立的所有上下文,具有LIFO(Last In First Out後進先出,也就是先進後出)的特性。
JS代碼首次運作,都會先建立一個全局執行上下文并壓入到執行棧中,之後每當有函數被調用,都會建立一個新的函數執行上下文并壓入棧内;由于執行棧LIFO的特性,是以可以了解為,JS代碼執行完畢前在執行棧底部永遠有個全局執行上下文。
function f1() {
f2();
console.log(1);
};
function f2() {
f3();
console.log(2);
};
function f3() {
console.log(3);
};
f1();//3 2 1
我們通過執行棧與上下文的關系來解釋上述代碼的執行過程,為了友善了解,我們假象執行棧是一個數組,在代碼執行初期一定會建立全局執行上下文并壓入棧,是以過程大緻如下:
//代碼執行前建立全局執行上下文
ECStack = [globalContext];
// f1調用
ECStack.push('f1 functionContext');
// f1又調用了f2,f2執行完畢之前無法console 1
ECStack.push('f2 functionContext');
// f2又調用了f3,f3執行完畢之前無法console 2
ECStack.push('f3 functionContext');
// f3執行完畢,輸出3并出棧
ECStack.pop();
// f2執行完畢,輸出2并出棧
ECStack.pop();
// f1執行完畢,輸出1并出棧
ECStack.pop();
// 此時執行棧中隻剩下一個全局執行上下文
那麼到這裡,我們解釋了執行棧與執行上下文的存儲規則;還記得我在前文提到代碼執行前JS引擎會做準備建立執行上下文嗎,具體怎麼建立呢,我們接着說。
肆 ❀ 執行上下文建立階段
執行上下文建立分為建立階段與執行階段兩個階段,較為難了解應該是建立階段,我們先說建立階段。
JS執行上下文的建立階段主要負責三件事:确定this—建立詞法環境元件(LexicalEnvironment)—建立變量環境元件(VariableEnvironment)
這裡我就直接借鑒了他人翻譯資料的僞代碼,來表示這個建立過程:
ExecutionContext = {
// 确定this的值
ThisBinding = <this value>,
// 建立詞法環境元件
LexicalEnvironment = {},
// 建立變量環境元件
VariableEnvironment = {},
};
如果你有閱讀其它關于執行上下文的文章讀到這裡一定有疑問,執行上下文建立過程不是應該解釋this,作用域與變量對象/活動對象才對嗎,怎麼跟别的地方說的不一樣,這點我後面解釋。
1.确定this
官方的稱呼為This Binding,在全局執行上下文中,this總是指向全局對象,例如浏覽器環境下this指向window對象。
而在函數執行上下文中,this的值取決于函數的調用方式,如果被一個對象調用,那麼this指向這個對象。否則this一般指向全局對象window或者undefined(嚴格模式)。
2.詞法環境元件
詞法環境是一個包含辨別符變量映射的結構,這裡的辨別符表示變量/函數的名稱,變量是對實際對象【包括函數類型對象】或原始值的引用。
詞法環境由環境記錄與對外部環境引入記錄兩個部分組成。
其中環境記錄用于存儲目前環境中的變量和函數聲明的實際位置;外部環境引入記錄很好了解,它用于儲存自身環境可以通路的其它外部環境,那麼說到這個,是不是有點作用域鍊的意思?
我們在前文提到了全局執行上下文與函數執行上下文,是以這也導緻了詞法環境分為全局詞法環境與函數詞法環境兩種。
全局詞法環境元件:
對外部環境的引入記錄為null,因為它本身就是最外層環境,除此之外它還記錄了目前環境下的所有屬性、方法位置。
函數詞法環境元件:
包含了使用者在函數中定義的所有屬性方法外,還包含了一個arguments對象。函數詞法環境的外部環境引入可以是全局環境,也可以是其它函數環境,這個根據實際代碼而來。
這裡借用譯文中的僞代碼(環境記錄在全局和函數中也不同,全局中的環境記錄叫對象環境記錄,函數中環境記錄叫聲明性環境記錄,說多了糊塗,下方有展示):
// 全局環境
GlobalExectionContext = {
// 全局詞法環境
LexicalEnvironment: {
// 環境記錄
EnvironmentRecord: {
Type: "Object", //類型為對象環境記錄
// 辨別符綁定在這裡
},
outer: < null >
}
};
// 函數環境
FunctionExectionContext = {
// 函數詞法環境
LexicalEnvironment: {
// 環境紀錄
EnvironmentRecord: {
Type: "Declarative", //類型為聲明性環境記錄
// 辨別符綁定在這裡
},
outer: < Global or outerfunction environment reference >
}
};
3.變量環境元件
變量環境可以說也是詞法環境,它具備詞法環境所有屬性,一樣有環境記錄與外部環境引入。在ES6中唯一的差別在于詞法環境用于存儲函數聲明與let const聲明的變量,而變量環境僅僅存儲var聲明的變量。
我們通過一串僞代碼來了解它們:
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
我們用僞代碼來描述上述代碼中執行上下文的建立過程:
//全局執行上下文
GlobalExectionContext = {
// this綁定為全局對象
ThisBinding: <Global Object>,
// 詞法環境
LexicalEnvironment: {
//環境記錄
EnvironmentRecord: {
Type: "Object", // 對象環境記錄
// 辨別符綁定在這裡 let const建立的變量a b在這
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
// 全局環境外部環境引入為null
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object", // 對象環境記錄
// 辨別符綁定在這裡 var建立的c在這
c: undefined,
}
// 全局環境外部環境引入為null
outer: <null>
}
}
// 函數執行上下文
FunctionExectionContext = {
//由于函數是預設調用 this綁定同樣是全局對象
ThisBinding: <Global Object>,
// 詞法環境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 聲明性環境記錄
// 辨別符綁定在這裡 arguments對象在這
Arguments: {0: 20, 1: 30, length: 2},
},
// 外部環境引入記錄為</Global>
outer: <GlobalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 聲明性環境記錄
// 辨別符綁定在這裡 var建立的g在這
g: undefined
},
// 外部環境引入記錄為</Global>
outer: <GlobalEnvironment>
}
}
不知道你有沒有發現,在執行上下文建立階段,函數聲明與var聲明的變量在建立階段已經被賦予了一個值,var聲明被設定為了undefined,函數被設定為了自身函數,而let const被設定為未初始化。
現在你總知道變量提升與函數聲明提前是怎麼回事了吧,以及為什麼let const為什麼有暫時性死域,這是因為作用域建立階段JS引擎對兩者初始化指派不同。
上下文除了建立階段外,還有執行階段,這點大家應該好了解,代碼執行時根據之前的環境記錄對應指派,比如早期var在建立階段為undefined,如果有值就對應指派,像let const值為未初始化,如果有值就指派,無值則賦予undefined。
伍 ❀ 關于變量對象與活動對象
回答前面的問題,為什麼别人的博文介紹上下文都是談作用域,變量對象和活動對象,我這就成了詞法環境,變量環境了。
我在閱讀相關資料也産生了這個疑問,一番查閱可以确定的是,變量對象與活動對象的概念是ES3提出的老概念,從ES5開始就用詞法環境和變量環境替代了,因為更好解釋。
在上文中,我們通過介紹詞法環境與變量環境解釋了為什麼var會存在變量提升,為什麼let const沒有,而通過變量對象與活動對象是很難解釋的,由其是在JavaScript在更新中不斷在彌補當初設計的坑。
其次,詞法環境的概念與變量對象這類概念也是可以對應上的。
我們知道變量對象與活動對象其實都是變量對象,變量對象是與執行上下文相關的資料作用域,存儲了在上下文中定義的變量和函數聲明。而在函數上下文中,我們用活動對象(activation object, AO)來表示變量對象。
那這不正好對應到了全局詞法記錄與函數詞法記錄了嗎。而且由于ES6新增的let const不存在變量提升,于是正好有了詞法環境與變量環境的概念來解釋這個問題。
是以說到這,你也不用為詞法環境,變量對象的概念鬧沖突了。
我們來總結下上面提到的概念。
陸 ❀ 總結
1.全局執行上下文一般由浏覽器建立,代碼執行時就會建立;函數執行上下文隻有函數被調用時才會建立,調用多少次函數就會建立多少上下文。
2.調用棧用于存放所有執行上下文,滿足FILO規則。
3.執行上下文建立階段分為綁定this,建立詞法環境,變量環境三步,兩者差別在于詞法環境存放函數聲明與const let聲明的變量,而變量環境隻存儲var聲明的變量。
4.詞法環境主要由環境記錄與外部環境引入記錄兩個部分組成,全局上下文與函數上下文的外部環境引入記錄不一樣,全局為null,函數為全局環境或者其它函數環境。環境記錄也不一樣,全局叫對象環境記錄,函數叫聲明性環境記錄。
5.你應該明白了為什麼會存在變量提升,函數提升,而let const沒有。
6.ES3之前的變量對象與活動對象的概念在ES5之後由詞法環境,變量環境來解釋,兩者概念不沖突,後者了解更為通俗易懂。