開年之後工作熱情一直不是很高,這幾天一直處於消極怠工狀態。早上不想起床,起床了不想上班。明明放假之前工作熱情還一直很高,一直心心念唸的想把小程式專案懟出來,結果休假回來之後畫風完全不一樣了。我感覺自己得了嚴重了節後綜合徵。還好擼了幾篇文章,勉強表示這一週的時間沒有完全浪費。這篇文章要給大家介紹的是變數物件。
在JavaScript中,我們肯定不可避免的需要宣告變數和函式,可是JS解析器是如何找到這些變數的呢?我們還得對執行上下文有一個進一步的瞭解。
在上一篇文章中,我們已經知道,當呼叫一個函式時(啟用),一個新的執行上下文就會被建立。而一個執行上下文的生命週期可以分為兩個階段。
- 建立階段
在這個階段中,執行上下文會分別建立變數物件,建立作用域鏈,以及確定this的指向 - 程式碼執行階段
建立完成之後,就會開始執行程式碼,這個時候,會完成變數賦值,函式引用,以及執行其他程式碼。
從這裡我們就可以看出詳細瞭解執行上下文極為重要,因為其中涉及到了變數物件,作用域鏈,this等很多人沒有怎麼弄明白,但是卻極為重要的概念,因此它關係到我們能不能真正理解JavaScript。在後面的文章中我們會一一詳細總結,這裡我們先重點了解變數物件。
變數物件(Variable Object)
變數物件的建立,依次經歷了以下幾個過程。
- 建立arguments物件。檢查當前上下文中的引數,建立該物件下的屬性與屬性值。
- 檢查當前上下文的函式宣告,也就是使用function關鍵字宣告的函式。在變數物件中以函式名建立一個屬性,屬性值為指向該函式所在記憶體地址的引用。如果函式名的屬性已經存在,那麼該屬性將會被新的引用所覆蓋。
- 檢查當前上下文中的變數宣告,每找到一個變數宣告,就在變數物件中以變數名建立一個屬性,屬性值為undefined。如果該變數名的屬性已經存在,為了防止同名的函式被修改為undefined,則會直接跳過,原屬性值不會被修改。
根據這個規則,理解變數提升就變得十分簡單了。在很多文章中雖然提到了變數提升,但是具體是怎麼回事還真的很多人都說不出來,以後在面試中用變數物件的建立過程跟面試官解釋變數提升,保證瞬間提升逼格。
在上面的規則中我們看出,function宣告會比var宣告優先順序更高一點。為了幫助大家更好的理解變數物件,我們結合一些簡單的例子來進行探討。
1 2 3 4 5 6 7 8 9 10 11 12 |
// demo01 function test() { console.log(a); console.log(foo()); var a = 1; function foo() { return 2; } } test(); |
在上例中,我們直接從test()的執行上下文開始理解。全域性作用域中執行test()
時,test()的執行上下文開始建立。為了便於理解,我們用如下的形式來表示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
建立過程 testEC = { // 變數物件 VO: {}, scopeChain: {}, this: {} } // 因為本文暫時不詳細解釋作用域鏈和this,所以把變數物件專門提出來說明 // VO 為 Variable Object的縮寫,即變數物件 VO = { arguments: {...}, //注:在瀏覽器的展示中,函式的引數可能並不是放在arguments物件中,這裡為了方便理解,我做了這樣的處理 foo: <foo reference> // 表示foo的地址引用 a: undefined } |
未進入執行階段之前,變數物件中的屬性都不能訪問!但是進入執行階段之後,變數物件轉變為了活動物件,裡面的屬性都能被訪問了,然後開始進行執行階段的操作。
這樣,如果再面試的時候被問到變數物件和活動物件有什麼區別,就又可以自如的應答了,他們其實都是同一個物件,只是處於執行上下文的不同生命週期。
1 2 3 4 5 6 7 |
// 執行階段 VO -> AO // Active Object AO = { arguments: {...}, foo: <foo reference>, a: 1 } |
因此,上面的例子demo1,執行順序就變成了這樣
1 2 3 4 5 6 7 8 9 10 11 |
function test() { function foo() { return 2; } var a; console.log(a); console.log(foo()); a = 1; } test(); |
再來一個例子,鞏固一下我們的理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// demo2 function test() { console.log(foo); console.log(bar); var foo = 'Hello'; console.log(foo); var bar = function () { return 'world'; } function foo() { return 'hello'; } } test(); |
1 2 3 4 5 6 7 |
// 建立階段 VO = { arguments: {...}, foo: <foo reference>, bar: undefined } // 這裡有一個需要注意的地方,因為var宣告的變數當遇到同名的屬性時,會跳過而不會覆蓋 |
1 2 3 4 5 6 7 |
// 執行階段 VO -> AO VO = { arguments: {...}, foo: 'Hello', bar: <bar reference> } |
需要結合上面的知識,仔細對比這個例子中變數物件從建立階段到執行階段的變化,如果你已經理解了,說明變數物件相關的東西都已經難不倒你了。
全域性上下文的變數物件
以瀏覽器中為例,全域性物件為window。
全域性上下文有一個特殊的地方,它的變數物件,就是window物件。而這個特殊,在this指向上也同樣適用,this也是指向window。
1 2 3 4 5 6 7 |
// 以瀏覽器中為例,全域性物件為window // 全域性上下文 windowEC = { VO: window, scopeChain: {}, this: window } |
除此之外,全域性上下文的生命週期,與程式的生命週期一致,只要程式執行不結束,比如關掉瀏覽器視窗,全域性上下文就會一直存在。其他所有的上下文環境,都能直接訪問全域性上下文的屬性。
前端基礎進階系列我會持續更新,歡迎大家關注我公眾號isreact,新的文章更新了我會在公眾號裡第一時間通知大家。也歡迎大家來簡書關注我。