文章目錄
複製代碼 代碼如下:var classA = function(){
this.prop1 = 1;
}
classA.prototype.func1 = function(){
var that = this,
var1 = 2;
function a(){
return function(){
alert(var1);
alert(this.prop1);
}.apply(that);
};
a();
}
var objA = new ClassA();
objA.func1();
大家應該寫過上面類似的代碼吧,其實這裡我想要表達的是有時候一個方法定義的地方和使用的地方會相隔十萬八千裡,那方法執行時,它能訪問哪些變數,不能訪問哪些變數,這個怎麼判斷呢?這個就是我們這次需要分析的問題—詞法範圍
詞法範圍:變數的範圍是在定義時決定而不是執行時決定,也就是說詞法範圍取決於源碼,通過靜態分析就能確定,因此詞法範圍也叫做靜態範圍。 with和eval除外,所以只能說JS的範圍機制非常接近詞法範圍(Lexical scope)。
下面通過幾個小小的案例,開始深入的瞭解對理解詞法範圍和閉包必不可少的,JS執行時底層的一些概念和理論知識。
經典案列重現
1、經典案例一 複製代碼 代碼如下:/*全域(window)域下的一段代碼*/
function a(i) {
var i;
alert(i);
};
a(10);
疑問:上面的代碼會輸出什麼呢?
答案:沒錯,就是彈出10。具體執行過程應該是這樣的
a 函數有一個形參 i,調用 a 函數時傳入實參 10,形參 i=10
接著定義一個同名的局部變數 i,未賦值
alert 輸出 10
思考:局部變數 i 和形參 i 是同一個儲存空間嗎?
2、經典案例二
複製代碼 代碼如下:/*全域(window)域下的一段代碼*/
function a(i) {
alert(i);
alert(arguments[0]); //arguments[0]應該就是形參 i
var i = 2;
alert(i);
alert(arguments[0]);
};
a(10);
疑問:上面的代碼又會輸出什麼呢?(( 10,10,2,10 10,10,2,2 ))
答案:在FireBug中的運行結果是第二個10,10,2,2,猜對了… ,下面簡單說一下具體執行過程
a 函數有一個形參i,調用 a 函數時傳入實參 10,形參 i=10
第一個 alert 把形參 i 的值 10 輸出
第二個 alert 把 arguments[0] 輸出,應該也是 i
接著定義個局部變數 i 並賦值為2,這時候局部變數 i=2
第三個 alert 就把局部變數 i 的值 2 輸出
第四個alert再次把 arguments[0] 輸出
思考:這裡能說明局部變數 i 和形參 i 的值相同嗎?
3、經典案例三
複製代碼 代碼如下:/*全域(window)域下的一段代碼*/
function a(i) {
var i = i;
alert(i);
};
a(10);
疑問:上面的代碼又又會輸出什麼呢?(( undefined 10 ))
答案:在FireBug中的運行結果是 10,下面簡單說一下具體執行過程
第一句聲明一個與形參 i 同名的局部變數 i,根據結果我們知道,後一個 i 是指向了
形參 i,所以這裡就等於把形參 i 的值 10 賦了局部變數 i
第二個 alert 當然就輸出 10
思考:結合案列二,這裡基本能說明局部變數 i 和形參 i 指向了同一個儲存地址!
4、經典案例四 複製代碼 代碼如下:/*全域(window)域下的一段代碼*/
var i=10;
function a() {
alert(i);
var i = 2;
alert(i);
};
a();
疑問:上面的代碼又會輸出什麼呢?(小子,看這回整不死你!哇哈哈,就不給你選項)
答案:在FireBug中的運行結果是 undefined, 2,下面簡單說一下具體執行過程
第一個alert輸出undefined
第二個alert輸出 2
思考:到底怎麼回事兒?
5、經典案例五…………..N
看到上面的幾個例子,你可能會想,怎麼可能,我寫了幾年的 js 了,怎麼這麼簡單例子也會猶豫,結果可能還答錯了。其實可能原因是:我們能很快的寫出一個方法,但到底方法內部是怎麼執行的呢?執行的細節又是怎麼樣的呢?你可能沒有進行過深入的學習和瞭解。要瞭解這些細節,那就需要瞭解 JS 引擎的工作方式,所以下面我們就把 JS 引擎對一個方法的解析過程進行一個稍微深入一些的介紹
解析過程
1、執行順序
- 編譯型語言,編譯步驟分為:詞法分析、文法分析、語義檢查、代碼最佳化和位元組產生。
- 解釋型語言,通過詞法分析和文法分析得到文法分析樹後,就可以開始解釋執行了。這裡是一個簡單原始的關於解析過程的原理,僅作為參考,詳細的解析過程(各種JS引擎還有不同)還需要更深一步的研究
JavaScript執行過程,如果一個文檔流中包含多個script程式碼片段(用script標籤分隔的js代碼或引入的js檔案),它們的運行順序是:
- 步驟1. 讀入第一個程式碼片段(js執行引擎並非一行一行地執行程式,而是一段一段地分析執行的)
- 步驟2. 做詞法分析和文法分析,有錯則報語法錯誤(比如括弧不匹配等),並跳轉到步驟5
- 步驟3. 對【var】變數和【function】定義做“預解析“(永遠不會報錯的,因為只解析正確的聲明)
- 步驟4. 執行程式碼片段,有錯則報錯(比如變數未定義)
- 步驟5. 如果還有下一個程式碼片段,則讀入下一個程式碼片段,重複步驟2
- 步驟6. 結束
2、特殊說明
全域域(window)域下所有JS代碼可以被看成是一個“匿名方法“,它會被自動執行,而此“匿名方法“內的其它方法則是在被顯示調用的時候才被執行
3、關鍵步驟
上面的過程,我們主要是分成兩個階段
- 解析:就是通過文法分析和預解析構造合法的文法分析樹。
- 執行:執行具體的某個function,JS引擎在執行每個函數執行個體時,都會建立一個執行環境(ExecutionContext)和使用中的物件(activeObject)(它們屬於宿主對象,與函數執行個體的生命週期保持一致)
3、關鍵概念
到這裡,我們再更強調以下一些概念,這些概念都會在下面用一個一個的實體來表示,便於大家理解
- 文法分析樹(SyntaxTree)可以直觀地表示出這段代碼的相關資訊,具體的實現就是JS引擎建立了一些表,用來記錄每個方法內的變數集(variables),方法集(functions)和範圍(scope)等
- 執行環境(ExecutionContext)可理解為一個記錄當前執行的方法【外部描述資訊】的對象,記錄所執行方法的類型,名稱,參數和使用中的物件(activeObject)
- 使用中的物件(activeObject)可理解為一個記錄當前執行的方法【內部執行資訊】的對象,記錄內部變數集(variables)、內嵌函數集(functions)、實參(arguments)、範圍鏈(scopeChain)等執行所需資訊,其中內部變數集(variables)、內嵌函數集(functions)是直接從第一步建立的文法分析樹複製過來的
- 詞法範圍:變數的範圍是在定義時決定而不是執行時決定,也就是說詞法範圍取決於源碼,通過靜態分析就能確定,因此詞法範圍也叫做靜態範圍。 with和eval除外,所以只能說JS的範圍機制非常接近詞法範圍(Lexical scope)
- 範圍鏈:詞法範圍的實現機制就是範圍鏈(scopeChain)。範圍鏈是一套按名稱尋找(Name Lookup)的機制,首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著範圍鏈到父 ActiveObject 中尋找,一直找到全域調用對象(Global Object)
4、實體表示
解析類比
估計,看到這兒,大家還是很朦朧吧,什麼是文法分析樹,文法分析樹到底長什麼樣子,範圍鏈又怎麼實現的,使用中的物件又有什麼內容等等,還是不是太清晰,下面我們就通過一段實際的代碼來類比整個解析過程,我們就把文法分析樹,使用中的物件實實在在的建立出來,理解範圍,範圍鏈的到底是怎麼實現的
1、類比代碼 複製代碼 代碼如下:/*全域(window)域下的一段代碼*/
var i = 1,j = 2,k = 3;
function a(o,p,x,q){
var x = 4;
alert(i);
function b(r,s) {
var i = 11,y = 5;
alert(i);
function c(t){
var z = 6;
alert(i);
};
//函數運算式
var d = function(){
alert(y);
};
c(60);
d();
};
b(40,50);
}
a(10,20,30);
2、文法分析樹
上面的代碼很簡單,就是先定義了一些全域變數和全域方法,接著在方法內再定義局部變數和局部方法,現在JS解譯器讀入這段代碼開始解析,前面提到 JS 引擎會先通過文法分析和預解析得到文法分析樹,至於文法分析樹長什麼樣兒,都有些什麼資訊,下面我們以一種簡單的結構:一個 JS 對象(為了清晰表示個各種對象間的參考關聯性,這裡的只是偽對象表示,可能無法運行)來描述文法分析樹(這是我們比較熟悉的,實際結構我們不去深究,肯定複雜得多,這裡是為了協助理解解析過程而特意簡化) 複製代碼 代碼如下:/**
* 類比建立一棵文法分析樹,儲存function內的變數和方法
*/
var SyntaxTree = {
// 全域對象在文法分析樹中的表示
window: {
variables:{
i:{ value:1},
j:{ value:2},
k:{ value:3}
},
functions:{
a: this.a
}
},
a:{
variables:{
x:"undefined"
},
functions:{
b: this.b
},
scope: this.window
},
b:{
variables:{
y:"undefined"
},
functions:{
c: this.c,
d: this.d
},
scope: this.a
},
c:{
variables:{
z:"undefined"
},
functions:{},
scope: this.b
},
d:{
variables:{},
functions:{},
scope: {
myname:d,
scope: this.b
}
}
};
上面就是關於文法分析樹的一個簡單表示,正如我們前面分析的,文法分析樹主要記錄了每個 function 中的變數集(variables),方法集(functions)和範圍(scope)
文法分析樹關鍵點
1變數集(variables)中,只有變數定義,沒有變數值,這時候的變數值全部為“undefined”
2範圍(scope),根據詞法範圍的特點,這個時候每個變數的範圍就已經明確了,而不會隨執行時的環境而改變。【什麼意思呢?就是我們經常將一個方法 return 回去,然後在另外一個方法中去執行,執行時,方法中變數的範圍是按照方法定義時的範圍走。其實這裡想表達的意思就是不管你在多麼複雜,多麼遠的地方執行該方法,最終判斷方法中變數能否被訪問還是得回到方法定義時的地方查證】
3範圍(scope)建立規則
a對於函式宣告和匿名函數運算式來說,[scope]就是它建立時的範圍
b對於有名字的函數運算式,[scope]頂端是一個新的JS對象(也就是繼承了Object.prototype),這個對象有兩個屬性,第一個是自身的名稱,第二個是定義的範圍,第一個函數名稱是為了確保函數內部的代碼可以無誤地訪問自己的函數名進行遞迴。
3、執行環境與使用中的物件
文法分析完成,開始執行代碼。我們調用每一個方法的時候,JS 引擎都會自動為其建立一個執行環境和一個使用中的物件,它們和方法執行個體的生命週期保持一致,為方法執行提供必要的執行支援,針對上面的幾個方法,我們這裡統一為其建立了使用中的物件(按道理是在執行方法的時候才會組建活動對象,為了便於示範,這裡一下子定義了所有方法的使用中的物件),具體如下:
執行環境 複製代碼 代碼如下:/**
* 執行環境:函數執行時建立的執行環境
*/
var ExecutionContext = {
window: {
type: "global",
name: "global",
body: ActiveObject.window
},
a:{
type: "function",
name: "a",
body: ActiveObject.a,
scopeChain: this.window.body
},
b:{
type: "function",
name: "b",
body: ActiveObject.b,
scopeChain: this.a.body
},
c:{
type: "function",
name: "c",
body: ActiveObject.c,
scopeChain: this.b.body
},
d:{
type: "function",
name: "d",
body: ActiveObject.d,
scopeChain: this.b.body
}
}
上面每一個方法的執行環境都儲存了相應方法的類型(function)、方法名稱(funcName)、使用中的物件(ActiveObject)、範圍鏈(scopeChain)等資訊,其關鍵點如下:
body屬性,直接指向當前方法的使用中的物件
scopeChain屬性,範圍鏈,它是一個鏈表結構,根據文法分析樹中當前方法對應的scope屬性,它指向scope對應的方法的使用中的物件(ActivceObject),變數尋找就是跟著這條鏈條尋找的
使用中的物件 複製代碼 代碼如下:/**
* 使用中的物件:函數執行時建立的使用中的物件列表
*/
var ActiveObject = {
window: {
variables:{
i: { value:1},
j: { value:2},
k: { value:3}
},
functions:{
a: this.a
}
},
a:{
variables:{
x: {value:4}
},
functions:{
b: SyntaxTree.b
},
parameters:{
o: {value: 10},
p: {value: 20},
x: this.variables.x,
q: "undefined"
},
arguments:[this.parameters.o,this.parameters.p,this.parameters.x]
},
b:{
variables:{
y:{ value:5}
},
functions:{
c: SyntaxTree.c,
d: SyntaxTree.d
},
parameters:{
r:{value:40},
s:{value:50}
},
arguments:[this.parameters.r,this.parameters.s]
},
c:{
variables:{
z:{ value:6}
},
functions:{},
parameters:{
u:{value:70}
},
arguments:[this.parameters.u]
},
d:{
variables:{},
functions:{},
parameters:{},
arguments:[]
}
}
上面每一個使用中的物件都儲存了相應方法的內部變數集(variables)、內嵌函數集(functions)、形參(parameters)、實參(arguments)等執行所需資訊,使用中的物件關鍵點
建立使用中的物件,從文法分析樹複製方法的內部變數集(variables)和內嵌函數集(functions)
方法開始執行,使用中的物件裡的內部變數集全部被重設為 undefined
建立形參(parameters)和實參(arguments)對象,同名的實參,形參和變數之間是【引用】關係
執行方法內的指派陳述式,這才會對變數集中的變數進行賦值處理
變數尋找規則是首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著執行環境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)
方法執行完成後,內部變數值不會被重設,至於變數什麼時候被銷毀,請參考下面一條
方法內變數的生存周期取決於方法執行個體是否存在活動引用,如沒有就銷毀使用中的物件
6和7 是使閉包能訪問到外部變數的根本原因
重釋經典案例
案列一二三
根據【在一個方法中,同名的實參,形參和變數之間是參考關聯性,也就是JS引擎的處理是同名變數和形參都引用同一個記憶體位址】,所以才會有二中的修改arguments會影響到局部變數的情況出現
案例四
根據【JS引擎變數尋找規則,首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著執行環境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)】,所以在四中,因為在當前的ActiveObject中找到了有變數 i 的定義,只是值為 “undefined”,所以直接輸出 “undefined” 了
總結
以上是我在學習和使用了JS一段時間後,為了更深入的瞭解它, 也為了更好的把握對它的應用, 從而在對閉包的學習過程中,自己對於詞法範圍的一些理解和總結,中間可能有一些地方和真實的JS解釋引擎有差異,因為我只是站在一個剛入門的前端開發人員而不是系統設計者的角度上去分析這個問題,希望能對JS開發人員理解此法範圍帶來一些協助!