很久沒更新文章了,這一憋就是一個大的。
提及js中的概念,執行上下文和做用域應該是你們最容易混淆的,你說混淆就混淆吧,其實大多數人在開發的時候不是很關注這兩個名詞,可是這裏面恰恰還夾雜好多其餘的概念--變量提高啊,閉包啊,this啊!
所以,搞明白這二者的關係對深刻javascript相當重要javascript
JavaScript代碼的整個執行過程,分爲兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段做用域規則會肯定。執行階段由引擎完成, 主要任務是執行可執行代碼,執行上下文在這個階段建立。
上面提到的可執行代碼,那麼什麼是可執行代碼呢?
其實很簡單,就三種,全局代碼、函數代碼、eval代碼。
其中eval代碼你們能夠忽略,畢竟實際開發中處於性能考慮基本不會用到,因此接下來咱們重點關注的就是全局代碼、函數代碼前端
在龐大的代碼裏必然不會只有一兩個函數,那麼如何管理每次執行函數時候建立的上下文呢java
js引擎建立了執行上下文棧(Execution context stack,ECS)來管理執行上下文面試
爲了模擬執行上下文棧的行爲,讓咱們定義執行上下文棧是一個數組:數組
ECStack = [];
試想當js開始要解釋執行代碼的時候,最早遇到的就是全局代碼,因此初始化的時候首先就會向執行上下文棧壓入一個全局執行上下文,咱們用 globalContext 表示它,而且只有當整個應用程序結束的時候,ECStack 纔會被清空,因此 ECStack 最底部永遠有個 globalContext:閉包
ECStack = [ globalContext ];
舉個?:函數
function out(){ function inner(){} inner() } out()
那麼這個函數的執行上下文棧會經歷如下過程:性能
ECStack.push(globalContext) ECStack.push(outContext) ECStack.push(innerContext) ECStack.pop(innerContext) ECStack.pop(outContext) ECStack.pop(globalContext)
再來看一個閉包的?:this
function f1(){ var n=999; function f2(){ console.log(n) } return f2; } var result=f1(); result(); // 999
該函數的執行上下文棧會經歷如下過程:翻譯
ECStack.push(globalContext) ECStack.push(f1Context) ECStack.pop(f1Context) ECStack.push(resultContext) ECStack.pop(resultContext) ECStack.pop(globalContext)
你們自行感覺一下對比,必定要記住上下文是在函數調用的時候纔會生產
既然調用一個函數時一個新的執行上下文會被建立。那執行上下文的生命週期一樣能夠分爲兩個階段。
變量對象
在函數上下文中,咱們用活動對象(activation object, AO)來表示變量對象。
活動對象其實就是被激活的變量對象,只是變量對象是規範上的或者說是引擎實現上的,不可在 JavaScript 環境中訪問,只有到當進入一個執行上下文中,這個執行上下文的變量對象纔會被激活,因此才叫 activation object,而只有活動對象上的各類屬性才能被訪問。
執行上下文的代碼會分紅兩個階段進行處理:分析(進入)和執行
當進入執行上下文時,這時候尚未執行代碼,
變量對象會包括:
a.由名稱和對應值組成的一個變量對象的屬性被建立
b.沒有實參,屬性值設爲 undefined
a.由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被建立
b.若是變量對象已經存在相同名稱的屬性,則徹底替換這個屬性
a.由名稱和對應值(undefined)組成一個變量對象的屬性被建立;
b.若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性
根據這個規則,理解變量提高就變得十分簡單了
舉個?分析下,看下面的代碼:
function foo(a) { console.log(b) console.log(c) 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" }
所以,這個例子代碼執行順序就是這樣的
function foo(a) { var b function c() {} var d console.log(b) console.log(c) b = 2; function c() {} d = function() {}; b = 3; }
做用域規定了如何查找變量,也就是肯定當前執行代碼對變量的訪問權限。
JavaScript 採用詞法做用域(lexical scoping),也就是靜態做用域。
由於 JavaScript 採用的是詞法做用域,函數的做用域在函數定義的時候就決定了。
而與詞法做用域相對的是動態做用域,函數的做用域是在函數調用的時候才決定的。
經典的一道面試題
var a = 1 function out(){ var a = 2 inner() } function inner(){ console.log(a) } out() //====> 1
當查找變量的時候,會先從當前上下文的變量對象中查找,若是沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫作做用域鏈
下面,讓咱們以一個函數的建立和激活兩個時期來說解做用域鏈是如何建立和變化的。
上面講到函數做用域是在建立的階段肯定
這是由於函數有一個內部屬性 [[scope]],當函數建立的時候,就會保存全部父變量對象到其中,你能夠理解 [[scope]] 就是全部父變量對象的層級鏈,可是注意:[[scope]] 並不表明完整的做用域鏈!
舉個?
function out() { function inner() { ... } }
函數建立時,各自的[[scope]]爲:
out.[[scope]] = [ globalContext.VO ]; inner.[[scope]] = [ outContext.AO, globalContext.VO ];
當函數激活時,進入函數上下文,建立 AO 後,就會將活動對象添加到做用鏈的前端。
這時候執行上下文的做用域鏈,咱們命名爲 Scope:
Scope = [AO].concat([[Scope]]);
至此,做用域鏈建立完畢。
最後咱們用一個代碼完整的說明下整個過程
var scope = "global scope"; function checkscope(){ var scope2 = 'local scope'; return scope2; } checkscope();
執行過程以下:
1.checkscope 函數被建立,保存做用域鏈到 內部屬性[[scope]]
checkscope.[[scope]] = [ globalContext.VO ];
2.執行 checkscope 函數,建立 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧
ECStack = [ checkscopeContext, globalContext ];
3.checkscope 函數並不馬上執行,開始作準備工做,第一步:複製函數[[scope]]屬性建立做用域鏈
checkscopeContext = { Scope: checkscope.[[scope]], }
4.第二步:用 arguments 建立活動對象,隨後初始化活動對象,加入形參、函數聲明、變量聲明
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]], }
5.第三步:將活動對象壓入 checkscope 做用域鏈頂端
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] }
6.準備工做作完,開始執行函數,隨着函數的執行,修改 AO 的屬性值
ECStack = [ globalContext ];
至此,關於做用域和執行上下文的介紹就到這裏,但願你們多消化,有問題請在評論中及時指出