前言
在上篇《JavaScript深入之執行上下文棧》中講到,當JavaScript程式碼執行一段可執行程式碼(executable code)時,會建立對應的執行上下文(execution context)。
對於每個執行上下文,都有三個重要屬性:
- 變數物件(Variable object,VO)
- 作用域鏈(Scope chain)
- this
今天重點講講建立變數物件的過程。
變數物件是與執行上下文相關的資料作用域,儲存了在上下文中定義的變數和函式宣告。
因為不同執行上下文下的變數物件稍有不同,所以我們來聊聊全域性上下文下的變數物件和函式上下文下的變數物件。
全域性上下文
我們先了解一個概念,叫全域性物件。在W3C school中也有介紹:
全域性物件是預定義的物件,作為 JavaScript 的全域性函式和全域性屬性的佔位符。通過使用全域性物件,可以訪問所有其他所有預定義的物件、函式和屬性。
在頂層 JavaScript 程式碼中,可以用關鍵字 this 引用全域性物件。因為全域性物件是作用域鏈的頭,這意味著所有非限定性的變數和函式名都會作為該物件的屬性來查詢。
例如,當JavaScript 程式碼引用 parseInt() 函式時,它引用的是全域性物件的 parseInt 屬性。全域性物件是作用域鏈的頭,還意味著在頂層 JavaScript 程式碼中宣告的所有變數都將成為全域性物件的屬性。
如果看的不是很懂的話,容我再來介紹下全域性物件:
1.可以通過this引用,在客戶端JavaScript中,全域性物件就是Window物件。
1 |
console.log(this); |
2.全域性物件是由Object建構函式例項化的一個物件。
1 |
console.log(this instanceof Object); |
3.預定義了一堆,嗯,一大堆函式和屬性。
1 2 3 |
// 都能生效 console.log(Math.random()); console.log(this.Math.random()); |
4.作為全域性變數的宿主。
1 2 |
var a = 1; console.log(this.a); |
5.客戶端JavaScript中,全域性物件有window屬性指向自身。
1 2 3 4 5 |
var a = 1; console.log(window.a); this.window.b = 2; console.log(this.b) |
花了一個大篇幅介紹全域性物件,其實就想說:
全域性上下文中的變數物件就是全域性物件吶!
函式上下文
在函式上下文中,我們用活動物件(activation object, AO)來表示變數物件。
活動物件是在進入函式上下文時刻被建立的,它通過函式的arguments屬性初始化。arguments屬性值是Arguments物件。
執行過程
執行上下文的程式碼會分成兩個階段進行處理:分析和執行,我們也可以叫做:
- 進入執行上下文
- 程式碼執行
進入執行上下文
當進入執行上下文時,這時候還沒有執行程式碼,
變數物件會包括:
- 函式的所有形參 (如果是函式上下文)
- 由名稱和對應值組成的一個變數物件的屬性被建立
- 沒有實參,屬性值設為undefined
- 函式宣告
- 由名稱和對應值(函式物件(function-object))組成一個變數物件的屬性被建立
- 如果變數物件已經存在相同名稱的屬性,則完全替換這個屬性
- 變數宣告
- 由名稱和對應值(undefined)組成一個變數物件的屬性被建立;
- 如果變數名稱跟已經宣告的形式引數或函式相同,則變數宣告不會干擾已經存在的這類屬性
舉個例子:
1 2 3 4 5 6 7 8 9 10 |
function foo(a) { var b = 2; function c() {} var d = function() {}; b = 3; } foo(1) |
在進入執行上下文後,這時候的AO是:
1 2 3 4 5 6 7 8 9 10 |
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined } |
程式碼執行
在程式碼執行階段,會順序執行程式碼,根據程式碼,修改變數物件的值
還是上面的例子,當程式碼執行完後,這時候的AO是:
1 2 3 4 5 6 7 8 9 10 |
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 3, c: reference to function c(){}, d: reference to FunctionExpression "d" } |
到這裡變數物件的建立過程就介紹完了,讓我們粗略的總結我們上述所說:
- 全域性上下文的變數物件初始化是全域性物件
- 函式上下文的變數物件初始化只包括Arguments物件
- 在進入執行上下文時會給變數物件新增形參、函式宣告、變數宣告等初始的屬性值
- 在程式碼執行階段,會再次修改變數物件的屬性值
思考題
最後讓我們看幾個例子:
1.第一題
1 2 3 4 5 6 7 8 9 10 11 12 |
function foo() { console.log(a); a = 1; } foo(); function bar() { a = 1; console.log(a); } bar(); |
第一段會報錯:Uncaught ReferenceError: a is not defined
第二段會列印1。
這是因為函式中的”a”並沒有通過var關鍵字宣告,所有不會被存放在AO中。
第一段執行console的時候,AO的值是:
1 2 3 4 5 |
AO = { arguments: { length: 0 } } |
沒有a的值,然後就會到全域性去找,全域性也沒有,所以會報錯。
當第二段執行console的時候,全域性物件已經被賦予了a屬性,這時候就可以從全域性找到a值,所以會列印1。
2.第二題
1 2 3 4 5 6 7 |
console.log(foo); function foo(){ console.log("foo"); } var foo = 1; |
會列印函式,而不是undefined。
這是因為在進入執行上下文時,首先會處理函式宣告,其次會處理變數宣告,如果如果變數名稱跟已經宣告的形式引數或函式相同,則變數宣告不會干擾已經存在的這類屬性。
深入系列
JavaScript深入系列預計寫十五篇左右,旨在幫大家捋順JavaScript底層知識,重點講解如原型、作用域、執行上下文、變數物件、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念,與羅列它們的用法不同,這個系列更注重通過寫demo,捋過程、模擬實現,結合ES規範等方法來講解。
所有文章和demo都可以在github上https://github.com/mqyqingfeng/Blog找到。如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。
本系列: