本文摘抄自冴羽的博客 JavaScript深入系列15篇正式完結](https://github.com/mqyqingfeng/Blog)
本文將先從 JavaScript作用域 聊起,然后從執行上下文的創建過程分析 JavaScript 作用域鏈 以及 相關的活動變量。
靜態作用域
作用域
作用域規定了如何查找變量,也就是確定當前執行代碼對變量的訪問權限。
JavaScript 采用詞法作用域(lexical scoping),也就是靜態作用域。
因為 JavaScript 采用的是詞法作用域,函數的作用域在函數定義的時候就決定了。
而與詞法作用域相對的是動態作用域,函數的作用域是在函數調用的時候才決定的。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 結果是 ???
總之一句話:
函數的作用域在函數定義的時候就決定了
函數會沿著作用域鏈去查找變量。
那么為什么 JavaScript 是靜態作用域呢?JavaScript 引擎是怎么處理函數的呢?
以下將進行詳解。
執行上下文棧
如果要問到 JavaScript 代碼執行順序的話,想必寫過 JavaScript 的開發者都會有個直觀的印象,那就是順序執行,畢竟:
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
然而去看這段代碼:
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
打印的結果卻是兩個 foo2。
刷過面試題的都知道這是因為 JavaScript 引擎并非一行一行地分析和執行程序,而是一段一段地分析執行。當執行一段代碼的時候,會進行一個“準備工作”,比如第一個例子中的變量提升,和第二個例子中的函數提升。
但是本文真正想讓大家思考的是:這個“一段一段”中的“段”究竟是怎么劃分的呢?
其實是以函數為段。
每當 JavaScript引擎 執行到一個函數的時候,就會創建一個執行空間(執行上下文)。
接下來問題來了,我們寫的函數多了去了,如何管理創建的那么多執行上下文呢?
所以 JavaScript 引擎 創建了執行上下文棧(Execution context stack,ECS)來管理執行上下文
試想當 JavaScript 開始要解釋執行代碼的時候,最先遇到的就是全局代碼,所以初始化的時候首先就會向執行上下文棧壓入一個全局執行上下文。當執行一個函數的時候,就會創建一個執行上下文,并且壓入執行上下文棧,當函數執行完畢的時候,就會將函數的執行上下文從棧中彈出。
那么 執行上下文 有什么用?
執行上下文
執行代碼計算時,總需要得知 變量 的來源及值,那么怎么獲取該變量?當然是要從代碼執行的上下文查找。
對于每個執行上下文,都有三個重要屬性:
- 變量對象(Variable object,VO)
- 作用域鏈(Scope chain)
- this
JavaScript 引擎會沿著作用域鏈去查找變量。
作用域鏈是如何產生?我們應該先了解 JavaScript 引擎 在創建 執行上下文 的具體處理過程。
作用域鏈
具體分析產生
我們分析第一段代碼:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
執行過程如下:
1.執行全局代碼,創建全局執行上下文,全局上下文被壓入執行上下文棧
ECStack = [
globalContext
];
2.全局上下文初始化
globalContext = {
VO: [global, scope, checkscope],
Scope: [globalContext.VO],
this: globalContext.VO
}
2.初始化的同時,checkscope 函數被創建,保存作用域鏈到函數的內部屬性[[scope]]。
每個函數有一個內部屬性 [[scope]],當函數創建的時候,就會保存所有父變量對象到其中,你可以理解 [[scope]] 就是所有父變量對象的層級鏈,但是注意:[[scope]] 并不代表完整的作用域鏈!
checkscope.[[scope]] = [
globalContext.VO
];
3.執行 checkscope 函數,創建 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧
ECStack = [
checkscopeContext,
globalContext
];
4.checkscope 函數執行上下文初始化:
- 復制函數 [[scope]] 屬性創建作用域鏈,
- 用 arguments 創建活動對象,
- 初始化活動對象,即加入形參、函數聲明、變量聲明,
- 將活動對象壓入 checkscope 作用域鏈頂端(執行至此,該函數的作用域鏈才完整)。
- 同時 f 函數被創建,保存作用域鏈到 f 函數的內部屬性[[scope]]
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
5.執行 f 函數,創建 f 函數執行上下文,f 函數執行上下文被壓入執行上下文棧
ECStack = [
fContext,
checkscopeContext,
globalContext
];
6.f 函數執行上下文初始化, 以下跟第 4 步相同:
- 復制函數 [[scope]] 屬性創建作用域鏈
- 用 arguments 創建活動對象
- 初始化活動對象,即加入形參、函數聲明、變量聲明
- 將活動對象壓入 f 作用域鏈頂端
fContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, checkscopeContext.AO, globalContext.VO],
this: undefined
}
7.f 函數執行,沿著作用域鏈查找 scope 值,返回 scope 值
8.f 函數執行完畢,f 函數上下文從執行上下文棧中彈出
ECStack = [
checkscopeContext,
globalContext
];
9.checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出
ECStack = [
globalContext
];
總結
當查找變量的時候,會先從當前上下文的變量對象中查找,如果沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈。
由上可知,作用域鏈是由 變量對象/活動對象 構成的。
活動變量
變量對象是與執行上下文相關的數據作用域,存儲了在上下文中定義的變量和函數聲明。因為不同執行上下文下的變量對象稍有不同,全局上下文中的變量對象就是全局對象。
在函數上下文中,我們用活動對象(activation object, AO)來表示變量對象。
活動對象和變量對象其實是一個東西,只是變量對象是規范上的或者說是引擎實現上的,不可在 JavaScript 環境中訪問,只有到當進入一個執行上下文中,這個執行上下文的變量對象才會被激活,所以才叫 activation object 吶,而只有被激活的變量對象,也就是活動對象上的各種屬性才能被訪問。
活動對象是在進入函數上下文時刻被創建的,它通過函數的 arguments 屬性初始化。arguments 屬性值是 Arguments 對象。
函數執行產生活動變量過程
執行上下文的代碼會分成兩個階段進行處理:分析和執行,我們也可以叫做:
- 進入執行上下文
- 代碼執行
當進入執行上下文時,這時候還沒有執行代碼,
變量對象會包括:
-
函數的所有形參 (如果是函數上下文)
- 由名稱和對應值組成的一個變量對象的屬性被創建
- 沒有實參,屬性值設為 undefined
-
函數聲明
- 由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建
- 如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性
-
變量聲明
- 由名稱和對應值(undefined)組成一個變量對象的屬性被創建;
- 如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性
舉個例子:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在進入執行上下文后,這時候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
在代碼執行階段,會順序執行代碼,根據代碼,修改變量對象的值
還是上面的例子,當代碼執行完后,這時候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
活動變量 總結
到這里變量對象的創建過程就介紹完了,讓我們簡潔的總結我們上述所說:
全局上下文的變量對象初始化是全局對象
函數上下文的變量對象初始化只包括 Arguments 對象
在進入執行上下文時會給變量對象添加形參、函數聲明、變量聲明等初始的屬性值
在代碼執行階段,會再次修改變量對象的屬性值