javascript 詞法作用域和閉包分析說(shuō)明
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();
大家應(yīng)該寫過(guò)上面類似的代碼吧,其實(shí)這里我想要表達(dá)的是有時(shí)候一個(gè)方法定義的地方和使用的地方會(huì)相隔十萬(wàn)八千里,那方法執(zhí)行時(shí),它能訪問(wèn)哪些變量,不能訪問(wèn)哪些變量,這個(gè)怎么判斷呢?這個(gè)就是我們這次需要分析的問(wèn)題—詞法作用域
詞法作用域:變量的作用域是在定義時(shí)決定而不是執(zhí)行時(shí)決定,也就是說(shuō)詞法作用域取決于源碼,通過(guò)靜態(tài)分析就能確定,因此詞法作用域也叫做靜態(tài)作用域。 with和eval除外,所以只能說(shuō)JS的作用域機(jī)制非常接近詞法作用域(Lexical scope)。
下面通過(guò)幾個(gè)小小的案例,開始深入的了解對(duì)理解詞法作用域和閉包必不可少的,JS執(zhí)行時(shí)底層的一些概念和理論知識(shí)。
經(jīng)典案列重現(xiàn)
1、經(jīng)典案例一
/*全局(window)域下的一段代碼*/
function a(i) {
var i;
alert(i);
};
a(10);
疑問(wèn):上面的代碼會(huì)輸出什么呢?
答案:沒(méi)錯(cuò),就是彈出10。具體執(zhí)行過(guò)程應(yīng)該是這樣的
a 函數(shù)有一個(gè)形參 i,調(diào)用 a 函數(shù)時(shí)傳入實(shí)參 10,形參 i=10
接著定義一個(gè)同名的局部變量 i,未賦值
alert 輸出 10
思考:局部變量 i 和形參 i 是同一個(gè)存儲(chǔ)空間嗎?
2、經(jīng)典案例二
/*全局(window)域下的一段代碼*/
function a(i) {
alert(i);
alert(arguments[0]); //arguments[0]應(yīng)該就是形參 i
var i = 2;
alert(i);
alert(arguments[0]);
};
a(10);
疑問(wèn):上面的代碼又會(huì)輸出什么呢?(( 10,10,2,10 10,10,2,2 ))
答案:在FireBug中的運(yùn)行結(jié)果是第二個(gè)10,10,2,2,猜對(duì)了… ,下面簡(jiǎn)單說(shuō)一下具體執(zhí)行過(guò)程
a 函數(shù)有一個(gè)形參i,調(diào)用 a 函數(shù)時(shí)傳入實(shí)參 10,形參 i=10
第一個(gè) alert 把形參 i 的值 10 輸出
第二個(gè) alert 把 arguments[0] 輸出,應(yīng)該也是 i
接著定義個(gè)局部變量 i 并賦值為2,這時(shí)候局部變量 i=2
第三個(gè) alert 就把局部變量 i 的值 2 輸出
第四個(gè)alert再次把 arguments[0] 輸出
思考:這里能說(shuō)明局部變量 i 和形參 i 的值相同嗎?
3、經(jīng)典案例三
/*全局(window)域下的一段代碼*/
function a(i) {
var i = i;
alert(i);
};
a(10);
疑問(wèn):上面的代碼又又會(huì)輸出什么呢?(( undefined 10 ))
答案:在FireBug中的運(yùn)行結(jié)果是 10,下面簡(jiǎn)單說(shuō)一下具體執(zhí)行過(guò)程
第一句聲明一個(gè)與形參 i 同名的局部變量 i,根據(jù)結(jié)果我們知道,后一個(gè) i 是指向了
形參 i,所以這里就等于把形參 i 的值 10 賦了局部變量 i
第二個(gè) alert 當(dāng)然就輸出 10
思考:結(jié)合案列二,這里基本能說(shuō)明局部變量 i 和形參 i 指向了同一個(gè)存儲(chǔ)地址!
4、經(jīng)典案例四
/*全局(window)域下的一段代碼*/
var i=10;
function a() {
alert(i);
var i = 2;
alert(i);
};
a();
疑問(wèn):上面的代碼又會(huì)輸出什么呢?(小子,看這回整不死你!哇哈哈,就不給你選項(xiàng))
答案:在FireBug中的運(yùn)行結(jié)果是 undefined, 2,下面簡(jiǎn)單說(shuō)一下具體執(zhí)行過(guò)程
第一個(gè)alert輸出undefined
第二個(gè)alert輸出 2
思考:到底怎么回事兒?
5、經(jīng)典案例五…………..N
看到上面的幾個(gè)例子,你可能會(huì)想,怎么可能,我寫了幾年的 js 了,怎么這么簡(jiǎn)單例子也會(huì)猶豫,結(jié)果可能還答錯(cuò)了。其實(shí)可能原因是:我們能很快的寫出一個(gè)方法,但到底方法內(nèi)部是怎么執(zhí)行的呢?執(zhí)行的細(xì)節(jié)又是怎么樣的呢?你可能沒(méi)有進(jìn)行過(guò)深入的學(xué)習(xí)和了解。要了解這些細(xì)節(jié),那就需要了解 JS 引擎的工作方式,所以下面我們就把 JS 引擎對(duì)一個(gè)方法的解析過(guò)程進(jìn)行一個(gè)稍微深入一些的介紹
解析過(guò)程
1、執(zhí)行順序
- 編譯型語(yǔ)言,編譯步驟分為:詞法分析、語(yǔ)法分析、語(yǔ)義檢查、代碼優(yōu)化和字節(jié)生成。
- 解釋型語(yǔ)言,通過(guò)詞法分析和語(yǔ)法分析得到語(yǔ)法分析樹后,就可以開始解釋執(zhí)行了。這里是一個(gè)簡(jiǎn)單原始的關(guān)于解析過(guò)程的原理,僅作為參考,詳細(xì)的解析過(guò)程(各種JS引擎還有不同)還需要更深一步的研究
JavaScript執(zhí)行過(guò)程,如果一個(gè)文檔流中包含多個(gè)script代碼段(用script標(biāo)簽分隔的js代碼或引入的js文件),它們的運(yùn)行順序是:
- 步驟1. 讀入第一個(gè)代碼段(js執(zhí)行引擎并非一行一行地執(zhí)行程序,而是一段一段地分析執(zhí)行的)
- 步驟2. 做詞法分析和語(yǔ)法分析,有錯(cuò)則報(bào)語(yǔ)法錯(cuò)誤(比如括號(hào)不匹配等),并跳轉(zhuǎn)到步驟5
- 步驟3. 對(duì)【var】變量和【function】定義做“預(yù)解析“(永遠(yuǎn)不會(huì)報(bào)錯(cuò)的,因?yàn)橹唤馕稣_的聲明)
- 步驟4. 執(zhí)行代碼段,有錯(cuò)則報(bào)錯(cuò)(比如變量未定義)
- 步驟5. 如果還有下一個(gè)代碼段,則讀入下一個(gè)代碼段,重復(fù)步驟2
- 步驟6. 結(jié)束
2、特殊說(shuō)明
全局域(window)域下所有JS代碼可以被看成是一個(gè)“匿名方法“,它會(huì)被自動(dòng)執(zhí)行,而此“匿名方法“內(nèi)的其它方法則是在被顯示調(diào)用的時(shí)候才被執(zhí)行
3、關(guān)鍵步驟
上面的過(guò)程,我們主要是分成兩個(gè)階段
- 解析:就是通過(guò)語(yǔ)法分析和預(yù)解析構(gòu)造合法的語(yǔ)法分析樹。
- 執(zhí)行:執(zhí)行具體的某個(gè)function,JS引擎在執(zhí)行每個(gè)函數(shù)實(shí)例時(shí),都會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境(ExecutionContext)和活動(dòng)對(duì)象(activeObject)(它們屬于宿主對(duì)象,與函數(shù)實(shí)例的生命周期保持一致)
3、關(guān)鍵概念
到這里,我們?cè)俑鼜?qiáng)調(diào)以下一些概念,這些概念都會(huì)在下面用一個(gè)一個(gè)的實(shí)體來(lái)表示,便于大家理解
- 語(yǔ)法分析樹(SyntaxTree)可以直觀地表示出這段代碼的相關(guān)信息,具體的實(shí)現(xiàn)就是JS引擎創(chuàng)建了一些表,用來(lái)記錄每個(gè)方法內(nèi)的變量集(variables),方法集(functions)和作用域(scope)等
- 執(zhí)行環(huán)境(ExecutionContext)可理解為一個(gè)記錄當(dāng)前執(zhí)行的方法【外部描述信息】的對(duì)象,記錄所執(zhí)行方法的類型,名稱,參數(shù)和活動(dòng)對(duì)象(activeObject)
- 活動(dòng)對(duì)象(activeObject)可理解為一個(gè)記錄當(dāng)前執(zhí)行的方法【內(nèi)部執(zhí)行信息】的對(duì)象,記錄內(nèi)部變量集(variables)、內(nèi)嵌函數(shù)集(functions)、實(shí)參(arguments)、作用域鏈(scopeChain)等執(zhí)行所需信息,其中內(nèi)部變量集(variables)、內(nèi)嵌函數(shù)集(functions)是直接從第一步建立的語(yǔ)法分析樹復(fù)制過(guò)來(lái)的
- 詞法作用域:變量的作用域是在定義時(shí)決定而不是執(zhí)行時(shí)決定,也就是說(shuō)詞法作用域取決于源碼,通過(guò)靜態(tài)分析就能確定,因此詞法作用域也叫做靜態(tài)作用域。 with和eval除外,所以只能說(shuō)JS的作用域機(jī)制非常接近詞法作用域(Lexical scope)
- 作用域鏈:詞法作用域的實(shí)現(xiàn)機(jī)制就是作用域鏈(scopeChain)。作用域鏈?zhǔn)且惶装疵Q查找(Name Lookup)的機(jī)制,首先在當(dāng)前執(zhí)行環(huán)境的 ActiveObject 中尋找,沒(méi)找到,則順著作用域鏈到父 ActiveObject 中尋找,一直找到全局調(diào)用對(duì)象(Global Object)
4、實(shí)體表示
解析模擬
估計(jì),看到這兒,大家還是很朦朧吧,什么是語(yǔ)法分析樹,語(yǔ)法分析樹到底長(zhǎng)什么樣子,作用域鏈又怎么實(shí)現(xiàn)的,活動(dòng)對(duì)象又有什么內(nèi)容等等,還是不是太清晰,下面我們就通過(guò)一段實(shí)際的代碼來(lái)模擬整個(gè)解析過(guò)程,我們就把語(yǔ)法分析樹,活動(dòng)對(duì)象實(shí)實(shí)在在的創(chuàng)建出來(lái),理解作用域,作用域鏈的到底是怎么實(shí)現(xiàn)的
/*全局(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);
};
//函數(shù)表達(dá)式
var d = function(){
alert(y);
};
c(60);
d();
};
b(40,50);
}
a(10,20,30);
2、語(yǔ)法分析樹
上面的代碼很簡(jiǎn)單,就是先定義了一些全局變量和全局方法,接著在方法內(nèi)再定義局部變量和局部方法,現(xiàn)在JS解釋器讀入這段代碼開始解析,前面提到 JS 引擎會(huì)先通過(guò)語(yǔ)法分析和預(yù)解析得到語(yǔ)法分析樹,至于語(yǔ)法分析樹長(zhǎng)什么樣兒,都有些什么信息,下面我們以一種簡(jiǎn)單的結(jié)構(gòu):一個(gè) JS 對(duì)象(為了清晰表示個(gè)各種對(duì)象間的引用關(guān)系,這里的只是偽對(duì)象表示,可能無(wú)法運(yùn)行)來(lái)描述語(yǔ)法分析樹(這是我們比較熟悉的,實(shí)際結(jié)構(gòu)我們不去深究,肯定復(fù)雜得多,這里是為了幫助理解解析過(guò)程而特意簡(jiǎn)化)
/**
* 模擬建立一棵語(yǔ)法分析樹,存儲(chǔ)function內(nèi)的變量和方法
*/
var SyntaxTree = {
// 全局對(duì)象在語(yǔ)法分析樹中的表示
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
}
}
};
上面就是關(guān)于語(yǔ)法分析樹的一個(gè)簡(jiǎn)單表示,正如我們前面分析的,語(yǔ)法分析樹主要記錄了每個(gè) function 中的變量集(variables),方法集(functions)和作用域(scope)
語(yǔ)法分析樹關(guān)鍵點(diǎn)
1變量集(variables)中,只有變量定義,沒(méi)有變量值,這時(shí)候的變量值全部為“undefined”
2作用域(scope),根據(jù)詞法作用域的特點(diǎn),這個(gè)時(shí)候每個(gè)變量的作用域就已經(jīng)明確了,而不會(huì)隨執(zhí)行時(shí)的環(huán)境而改變。【什么意思呢?就是我們經(jīng)常將一個(gè)方法 return 回去,然后在另外一個(gè)方法中去執(zhí)行,執(zhí)行時(shí),方法中變量的作用域是按照方法定義時(shí)的作用域走。其實(shí)這里想表達(dá)的意思就是不管你在多么復(fù)雜,多么遠(yuǎn)的地方執(zhí)行該方法,最終判斷方法中變量能否被訪問(wèn)還是得回到方法定義時(shí)的地方查證】
3作用域(scope)建立規(guī)則
a對(duì)于函數(shù)聲明和匿名函數(shù)表達(dá)式來(lái)說(shuō),[scope]就是它創(chuàng)建時(shí)的作用域
b對(duì)于有名字的函數(shù)表達(dá)式,[scope]頂端是一個(gè)新的JS對(duì)象(也就是繼承了Object.prototype),這個(gè)對(duì)象有兩個(gè)屬性,第一個(gè)是自身的名稱,第二個(gè)是定義的作用域,第一個(gè)函數(shù)名稱是為了確保函數(shù)內(nèi)部的代碼可以無(wú)誤地訪問(wèn)自己的函數(shù)名進(jìn)行遞歸。
3、執(zhí)行環(huán)境與活動(dòng)對(duì)象
語(yǔ)法分析完成,開始執(zhí)行代碼。我們調(diào)用每一個(gè)方法的時(shí)候,JS 引擎都會(huì)自動(dòng)為其建立一個(gè)執(zhí)行環(huán)境和一個(gè)活動(dòng)對(duì)象,它們和方法實(shí)例的生命周期保持一致,為方法執(zhí)行提供必要的執(zhí)行支持,針對(duì)上面的幾個(gè)方法,我們這里統(tǒng)一為其建立了活動(dòng)對(duì)象(按道理是在執(zhí)行方法的時(shí)候才會(huì)生成活動(dòng)對(duì)象,為了便于演示,這里一下子定義了所有方法的活動(dòng)對(duì)象),具體如下:
執(zhí)行環(huán)境
/**
* 執(zhí)行環(huán)境:函數(shù)執(zhí)行時(shí)創(chuàng)建的執(zhí)行環(huán)境
*/
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
}
}
上面每一個(gè)方法的執(zhí)行環(huán)境都存儲(chǔ)了相應(yīng)方法的類型(function)、方法名稱(funcName)、活動(dòng)對(duì)象(ActiveObject)、作用域鏈(scopeChain)等信息,其關(guān)鍵點(diǎn)如下:
body屬性,直接指向當(dāng)前方法的活動(dòng)對(duì)象
scopeChain屬性,作用域鏈,它是一個(gè)鏈表結(jié)構(gòu),根據(jù)語(yǔ)法分析樹中當(dāng)前方法對(duì)應(yīng)的scope屬性,它指向scope對(duì)應(yīng)的方法的活動(dòng)對(duì)象(ActivceObject),變量查找就是跟著這條鏈條查找的
活動(dòng)對(duì)象
/**
* 活動(dòng)對(duì)象:函數(shù)執(zhí)行時(shí)創(chuàng)建的活動(dòng)對(duì)象列表
*/
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:[]
}
}
上面每一個(gè)活動(dòng)對(duì)象都存儲(chǔ)了相應(yīng)方法的內(nèi)部變量集(variables)、內(nèi)嵌函數(shù)集(functions)、形參(parameters)、實(shí)參(arguments)等執(zhí)行所需信息,活動(dòng)對(duì)象關(guān)鍵點(diǎn)
創(chuàng)建活動(dòng)對(duì)象,從語(yǔ)法分析樹復(fù)制方法的內(nèi)部變量集(variables)和內(nèi)嵌函數(shù)集(functions)
方法開始執(zhí)行,活動(dòng)對(duì)象里的內(nèi)部變量集全部被重置為 undefined
創(chuàng)建形參(parameters)和實(shí)參(arguments)對(duì)象,同名的實(shí)參,形參和變量之間是【引用】關(guān)系
執(zhí)行方法內(nèi)的賦值語(yǔ)句,這才會(huì)對(duì)變量集中的變量進(jìn)行賦值處理
變量查找規(guī)則是首先在當(dāng)前執(zhí)行環(huán)境的 ActiveObject 中尋找,沒(méi)找到,則順著執(zhí)行環(huán)境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)
方法執(zhí)行完成后,內(nèi)部變量值不會(huì)被重置,至于變量什么時(shí)候被銷毀,請(qǐng)參考下面一條
方法內(nèi)變量的生存周期取決于方法實(shí)例是否存在活動(dòng)引用,如沒(méi)有就銷毀活動(dòng)對(duì)象
6和7 是使閉包能訪問(wèn)到外部變量的根本原因
重釋經(jīng)典案例
案列一二三
根據(jù)【在一個(gè)方法中,同名的實(shí)參,形參和變量之間是引用關(guān)系,也就是JS引擎的處理是同名變量和形參都引用同一個(gè)內(nèi)存地址】,所以才會(huì)有二中的修改arguments會(huì)影響到局部變量的情況出現(xiàn)
案例四
根據(jù)【JS引擎變量查找規(guī)則,首先在當(dāng)前執(zhí)行環(huán)境的 ActiveObject 中尋找,沒(méi)找到,則順著執(zhí)行環(huán)境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)】,所以在四中,因?yàn)樵诋?dāng)前的ActiveObject中找到了有變量 i 的定義,只是值為 “undefined”,所以直接輸出 “undefined” 了
總結(jié)
以上是我在學(xué)習(xí)和使用了JS一段時(shí)間后,為了更深入的了解它, 也為了更好的把握對(duì)它的應(yīng)用, 從而在對(duì)閉包的學(xué)習(xí)過(guò)程中,自己對(duì)于詞法作用域的一些理解和總結(jié),中間可能有一些地方和真實(shí)的JS解釋引擎有差異,因?yàn)槲抑皇钦驹谝粋€(gè)剛?cè)腴T的前端開發(fā)人員而不是系統(tǒng)設(shè)計(jì)者的角度上去分析這個(gè)問(wèn)題,希望能對(duì)JS開發(fā)者理解此法作用域帶來(lái)一些幫助!
相關(guān)文章
淺談JavaScript中小數(shù)和大整數(shù)的精度丟失
下面小編就為大家?guī)?lái)一篇淺談JavaScript中小數(shù)和大整數(shù)的精度丟失。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-05-05用js實(shí)現(xiàn)隨機(jī)返回?cái)?shù)組的一個(gè)元素
js實(shí)現(xiàn)隨機(jī)返回?cái)?shù)組的一個(gè)元素,這是個(gè)奇妙的方法。適合做標(biāo)題性質(zhì)文字的隨機(jī)輪換顯示2007-08-08JavaScript實(shí)現(xiàn)微信小程序打卡時(shí)鐘項(xiàng)目實(shí)例
這篇文章主要為大家介紹了JavaScript實(shí)現(xiàn)微信小程序打卡時(shí)鐘項(xiàng)目實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04JS實(shí)現(xiàn)登錄頁(yè)密碼的顯示和隱藏功能
在登錄頁(yè)經(jīng)常會(huì)用到通過(guò)點(diǎn)擊文本框的類似小眼睛圖片來(lái)實(shí)現(xiàn)隱藏顯示密碼的功能。下面給大家介紹基于JS實(shí)現(xiàn)登錄頁(yè)密碼的顯示和隱藏功能,需要的朋友參考下吧2017-12-12JavaScript監(jiān)聽和禁用瀏覽器回車事件實(shí)例
這篇文章主要介紹了JavaScript監(jiān)聽和禁用瀏覽器回車事件實(shí)例,本文直接給出示例代碼,需要的朋友可以參考下2015-01-01HTML頁(yè)面,測(cè)試JS對(duì)C函數(shù)的調(diào)用簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇HTML頁(yè)面,測(cè)試JS對(duì)C函數(shù)的調(diào)用簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08