首先,回顧下上篇博文中js基礎梳理-究竟什麼是執行上下文棧(執行棧),執行上下文(可執行代碼)?的執行上下文的生命週期:html
3.執行上下文的生命週期
3.1 建立階段
- 生成變量對象(Variable object, VO)
- 創建做用域鏈(Scope chain)
- 肯定this指向
3.2 執行階段
- 變量賦值
- 函數引用
- 執行其餘代碼
在寫程序的時候會定義不少變量和函數,那js解析器是如何找到這些變量和函數的?函數
變量對象是與執行上下文對應的概念,在執行上下文的建立階段,它依次存儲着在上下文中定義的如下內容:this
創建arguments對象。檢查當前上下文中的參數,創建該對象下的屬性與屬性值。沒有實參的話,屬性值爲undefined。code
檢查當前上下文的函數聲明,也就是使用function關鍵字聲明的函數。在變量對象中以函數名創建一個屬性,屬性值爲指向該函數所在內存地址的引用。若是變量對象已經存在相同名稱的屬性,則徹底替換這個屬性。htm
檢查當前上下文中的變量聲明,每找到一個變量聲明,就在變量對象中以變量名創建一個屬性,屬性值爲undefined。若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。對象
只有全局上下文的變量對象容許經過VO的屬性名稱來間接訪問,在其餘上下文(後面乾脆直接講函數上下文吧,咱們並無分析eval上下文)中是不能直接訪問VO對象的。blog
在函數上下文中,VO是不能直接訪問的,此時由活動對象AO繼續扮演VO的角色。生命週期
未進入執行階段前,變量對象中的屬性都不能訪問!可是進入到執行階段以後,變量對象轉變成了活動對象,裏面的屬性都能被訪問了,而後開始進行執行階段的操做。內存
所以,對於函數上下文來說,活動對象與變量對象其實都是同一個對象,只是處於執行上下文的不一樣生命週期。不過只有處於執行上下文棧棧頂的函數執行上下文中的變量對象,纔會變成活動對象。作用域
說了一堆概念,有點懵,對嗎?請看這個例子:
var a = 10; function b () { console.log('全局的b函數') }; function bar(a, b) { console.log('1', a, b) var a = 1 function b() { console.log('bar下的b函數') } console.log('2', a, b) } bar(2, 3) console.log('3', a, b)
要想知道爲何會這樣打印,首先,從執行上下文的建立階段來分析變量對象:
// 建立階段: // 第一步,遇到了全局代碼,進入全局上下文,此時的執行上下文棧是這樣 ECStack = [ globalContext: { VO: { // 根據1.2,會優先處理全局下的b函數聲明,值爲該函數所在內存地址的引用 b: <reference to function>, // 緊接着,按順序再處理bar函數聲明,此時根據1.1,由於是在全局上下文中,並不會分析bar函數的參數 bar: <refernce to function>, // 根據1.3,再處理變量,並賦值爲undefined a: undefined } } ]; // 第二步,發現bar函數被調用,就又建立了一個函數上下文,此時的執行上下文棧是這樣 ECStack = [ globalContext: { VO: { b: <reference to function b() {}>, bar: <refernce to function bar() {}>, a: undefined } }, <bar>functionContext: { VO: { // 根據1.1,優先分析函數的形參 arguments: { 0: 2, 1: 3, length: 2, callee: bar }, a: 2, // b: 3, // 根據1.2, 再分析bar函數中的函數聲明b,而且賦值爲b函數所在內存地址的引用, 它發現VO中已經有b:3了,就會覆蓋掉它。所以上面一行中的b:3實際上不存在了。 b: <refernce to function b() {}> // 根據1.3,接着分析bar函數中的變量聲明a,而且賦值爲undefined, 可是發現VO中已經有a:2了,所以下面一行中的a:undefined也是會不存在的。 // a: undefined } } ]
以上就是執行上下文中的代碼分析階段,也就是執行上下文的建立階段。再看看執行上下文的代碼執行階又發生了什麼。
// 執行階段: // 第三步:首先,執行了bar(2, 3)函數,緊接着,在bar函數裏執行了console.log('1', a, b)。全局上下文中依然仍是VO,可是函數上下文中VO就變成了AO。而且代碼執行到這,就已經修改了全局上下文中的變量a. ECStack = [ globalContext: { VO: { b: <reference to function b() {}>, bar: <refernce to function bar() {}>, a: 10, } }, <bar>functionContext: { AO: { arguments: { 0: 2, 1: 3, length: 2, callee: bar }, a: 2, b: <refernce to function b() {}> } } ] // 所以會輸出結果: '1', 2, function b() {console.log('bar下的b函數')}; // 第四步:執行console.log('2', a, b)的時候, 發現裏面的變量a被從新賦值爲1了。 ECStack = [ globalContext: { VO: { b: <reference to function b() {}>, bar: <refernce to function bar() {}>, a: 10, } }, <bar>functionContext: { AO: { arguments: { 0: 2, 1: 3, length: 2, callee: bar }, a: 1, b: <refernce to function b() {}> } } ] // 所以會輸出結果: '2', 1, function b() {console.log('bar下的b函數')}; // 第五步,執行到console.log('3', a, b)的時候,ECStack發現bar函數已經執行完了,就把bar從ECStack給彈出去了。此時的執行上下文棧是這樣的。 ECStack = [ globalContext: { VO: { b: <reference to function b() {}>, bar: <refernce to function bar() {}>, a: 10, } } ] // 所以會輸出結果: '3', 10, function b() {console.log('全局的b函數')}
總結一下,變量對象會有如下四種特性:
理解了這些,是否是發現再有一些函數提高,變量提高什麼的是否是都很簡單了。例如,你能夠思考下這三段代碼分別發生了什麼。
foo() var foo = function() {console.log(1)} function foo() {console.log(2)}
foo() function foo() {console.log(2)} var foo = function() {console.log(1)}
var foo = function() {console.log(1)} function foo() {console.log(2)} foo()