學習到javascript的運行機制時,有幾個概念常常出如今各類文章中且容易混淆。Execution Context(執行環境或執行上下文),Context Stack (執行棧),Variable Object(VO: 變量對象),Active Object(AO: 活動對象),LexicalEnvironment(詞法環境),VariableEnvironment(變量環境)等,特別是 VO,AO以及LexicalEnvironment,VariableEnvironment的區別不少文章都沒有涉及到。所以我查看了一些國內外的文章,結合自身理解寫下了下面的筆記。雖然由於自身不足致使理解上的誤差,可是依然相信讀完下文會對理解javascript的一些概念如變量提高,做用域和閉包有很大的幫助。javascript
瞭解javascript的運行機制,首先必須掌握兩個基本的概念。Execution Context(執行環境或執行上下文)和Context Stack (執行棧)java
咱們知道javascript是單線程語言,也就是同一時間只能執行一個任務。當javascript解釋器初始化代碼後,默認會進入全局的執行環境,以後每調用一個函數,javascript解釋器會建立一個新的執行環境。git
var a = 1; // 1.初始化默認進入全局執行環境
function b() { // 3.進入b 的執行環境
function c() { // 5. 進入c的執行環境
···
}
c() // 4.在b的執行環境裏調用c, 建立c的執行環境
}
b() // 2. 調用b 建立 b 的執行環境
複製代碼
執行環境的分類:es6
從一個簡單的例子開始講起github
function foo(i) {
if (i < 0) return;
console.log('begin:' + i);
foo(i - 1);
console.log('end:' + i);
}
foo(2);
複製代碼
如何存儲代碼運行時的執行環境(全局執行環境,函數執行環境)呢,答案是執行棧。而棧遵循的是先進後出的原理,javascript初始化完代碼後,首先會建立全局執行環境並推入當前的執行棧,當調用一個函數時,javascript引擎會建立新的執行環境並推到當前執行棧的頂端,在新的執行環境中,若是繼續發生一個新函數調用時,則繼續建立新的執行環境並推到當前執行棧的頂端,直到再無新函數調用。最上方的函數執行完成後,它的執行環境便從當前棧中彈出,並將控制權移交到當前執行棧的下一個執行環境,直到全局執行環境。當程序或瀏覽器關閉時,全局環境也將退出並銷燬。segmentfault
所以輸出的結果爲:瀏覽器
begin:2
begin:1
begin:0
end:0
end:1
end:2
複製代碼
咱們如今知道每次調用函數時,javascript 引擎都會建立一個新的執行環境,而如何建立這一系列的執行環境呢,答案是執行器會分爲兩個階段來完成, 分別是建立階段和激活(執行)階段。而即便步驟相同可是因爲規範的不一樣,每一個階段執行的過程有很大的不一樣。bash
建立階段:閉包
激活/執行階段:函數
建立階段:
激活/執行階段:
咱們從規範上能夠知道,ES3和ES5在執行環境的建立階段存在差別,固然他們都會在這個階段肯定this 的值 (關於this 的指向問題咱們之後會在專門的文章中分析各類this 的指向問題,這裏便不作深究)。咱們將圍繞這兩個規範不一樣點展開。儘管ES3的一些規範已經被拋棄,可是掌握ES3 建立執行環境的過程依然有助於咱們理解javascript深層次的概念。
VO 和 AO 是ES3規範中的概念,咱們知道在建立過程的第二個階段會建立變量對象,也就是VO,它是用來存放執行環境中可被訪問可是不能被 delete 的函數標識符,形參,變量聲明等,這個對象在js環境下是不可訪問的。而AO 和VO之間區別就是AO 是一個激活的VO,僅此而已。
變量對象(Variable) object)是說JS的執行上下文中都有個對象用來存放執行上下文中可被訪問可是不能被delete的函數標示符、形參、變量聲明等。它們會被掛在這個對象上,對象的屬性對應它們的名字對象屬性的值對應它們的值但這個對象是規範上或者說是引擎實現上的不可在JS環境中訪問到活動對象
激活對象(Activation object)有了變量對象存每一個上下文中的東西,可是它何時能被訪問到呢?就是每進入一個執行上下文時,這個執行上下文兒中的變量對象就被激活,也就是該上下文中的函數標示符、形參、變量聲明等就能夠被訪問到了
如何建立VO對象能夠大體分爲四步
注意: 整個過程能夠大概描述成: 函數的形參=>函數聲明=>變量聲明, 其中在建立函數聲明時,若是名字存在,則會被重寫,在建立變量時,若是變量名存在,則忽略不會進行任何操做。
一個簡單的例子
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
複製代碼
執行的僞代碼
// 建立階段
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
// 激活階段
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
複製代碼
詞法環境和變量環境是ES5之後提到的概念,官方對詞法環境的解釋以下。
詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關係。詞法環境由環境記錄(environment record)和可能爲空引用(null)的外部詞法環境組成。
簡單的理解,詞法環境是一個包含標識符變量映射的結構。(這裏的標識符表示變量/函數的名稱,變量是對實際對象【包括函數類型對象】或原始值的引用)。
ES3的VO,AO爲何能夠被拋棄?我的認爲有兩個緣由,第一個是在建立過程當中所執行的建立做用域鏈和建立變量對象(VO)均可以在建立詞法環境的過程當中完成。第二個是針對es6中存儲函數聲明和變量(let 和 const)以及存儲變量(var)的綁定,能夠經過兩個不一樣的過程(詞法環境,變量環境)區分開來。
詞法環境由兩個部分組成
對外部環境的引用關係到做用域鏈,以後再分析,咱們先來看看環境記錄的分類。
環境記錄分兩部分
僞代碼以下
// 全局環境
GlobalExectionContext = {
// 詞法環境
LexicalEnvironment: {
EnvironmentRecord: {
···
}
outer: <null>
}
}
// 函數環境
FunctionExectionContext = {
// 詞法環境
LexicalEnvironment: {
EnvironmentRecord: {
// 包含argument
}
outer: <Global or outer function environment reference>
}
}
複製代碼
變量環境也是個詞法環境,主要的區別在於lexicalEnviroment用於存儲函數聲明和變量( let 和 const )綁定,而ObjectEnviroment僅用於存儲變量( var )綁定。
ES5規範下的整個建立過程能夠參考下方的僞代碼
let a = 20;
const b = 30;
var c;
function d(e, f) {
var g = 20;
return e * f * g;
}
c = d(20, 30);
複製代碼
// 全局環境
GlobalExectionContext = {
this: <Global Object>,
// 詞法環境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object", // 環境記錄分類: 對象環境記錄
a: < uninitialized >, // 未初始化
b: < uninitialized >,
d: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object", // 環境記錄分類: 對象環境記錄
c: undefined, // undefined
}
outer: <null>
}
}
// 函數環境
FunctionExectionContext = {
this: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 環境記錄分類: 聲明環境記錄
Arguments: {0: 20, 1: 30, length: 2}, // 函數環境下,環境記錄比全局環境下的環境記錄多了argument對象
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 環境記錄分類: 聲明環境記錄
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
複製代碼
前面講建立過程當中,咱們留下了一個伏筆,ES3規範中有建立做用域鏈的過程,而ES5中在建立詞法環境或變量環境的過程當中,也有生成外部環境的引用的過程。那這個過程有什麼做用呢。咱們經過一個簡單的例子來講明。
function one() {
var a = 1;
two();
function two() {
var b = 2;
three();
function three() {
var c = 3;
alert(a + b + c); // 6
}
}
}
one();
複製代碼
當執行到three 的執行環境時,此時 a和b 都不在c 的變量內,所以做用域鏈則起到了引用外部執行環境變量的做用。ES3中建立的做用域鏈如圖:
當解釋器執行alert(a + b + c),他首先會找自身執行環境下是否有a這個變量的存在,若是不存在,則經過查看做用域鏈,判斷a是否在上一個執行環境內部。它檢查是否a存在於內部,若找不到,則沿着做用域鏈往上一個執行環境找,直到找到,或者到頂級的全局做用域。同理ES6規範中也能夠這樣分析。
所以這會引入一個javascript一個重要的概念,閉包。從上面對執行環境的解釋咱們能夠這樣理解,閉包就是內部環境經過做用域鏈訪問到上層環境的變量。所以也存在沒法進行變量回收的問題,只要函數的做用域鏈在,變量的值便由於閉包沒法被回收。
注意: 此做用域鏈和原型鏈的做用域鏈不是同一個概念。
經過對javascript運行機制的介紹,對一些javasript高級概念有了更深的認識,特別是對一些雲裏霧裏的概念區別有了更深入的認識。不一樣規範下,不一樣概念的解釋更有利於深挖javascript底層的執行思想。我相信這是理解javascipt語言最重要的一步。
參考資料:
本文爲博主原創文章,轉載請註明出處 juejin.im/post/5c2052…