在之前我們講過,將作用域定義成“一套規(guī)則”,這套規(guī)則用于管理引擎如何在當前作用域以及嵌套的子作用域中根據(jù)標識符名稱來進行變量的訪問。作用域就我知道的有兩種,一種是詞法作用域,也可以叫做靜態(tài)作用域,js的作用域就是詞法作用域(靜態(tài)作用域),我會盡我所能去討論這個詞法作用域;還有一種就是動態(tài)作用域,比較少用(比如Bash腳本等)。
簡單地說,詞法作用域就是定義在詞法階段的作用域。換句話說,詞法作用域是由你在寫代碼時將變量和塊作用域?qū)懺谀睦飦頉Q定的,因此當詞法分析器處理代碼時會保持作用域不變(大部分情況下是這樣的,特殊情況有幾種,下面我們一起來討論下)。
function fun1(a) {
var b = a * 2;
function fun2(c) {
console.log( a, b, c );
}
fun2( b * 3 );
}
fun1( 2 ); // 2, 4, 12
在這個例子中有三個逐級嵌套的作用域,
1.是全局作用域,里面有一個標識符 fun1;
2.包含fun1所創(chuàng)建的作用域,其中有三個標識符 :a 、b、fun2
3.包含fun2所創(chuàng)建的作用域,其中三個標識符:a 、b、c
一、查找
引擎會根據(jù)作用域的來查找標識符的位置,引擎執(zhí)行console.log(..) 聲明,并查找a、b 和c 三個變量的引用。它首先從最內(nèi)部的作用域,也就是fun2(..) 函數(shù)的作用域氣泡開始查找。引擎無法在這里找到a,因此會去上一級到所嵌套的fun1(..) 的作用域中繼續(xù)查找。在這里找到了a,因此引擎使用了這個引用。對b 來講也是一樣的。而對c 來說,引擎在fun2(..) 中就找到了它。如果a、c 都存在于fun1(..) 和fun2(..) 的內(nèi)部,console.log(..) 就可以直接使用fun2(..)中的變量,而無需到外面的fun1(..) 中查找。
作用域查找會在找到第一個匹配的標識符時停止。在多層的嵌套作用域中可以定義同名的標識符,這叫作“遮蔽效應(yīng)”(內(nèi)部的標識符“遮蔽”了外部的標識符)。
拋開遮蔽效應(yīng),作用域查找始終從運行時所處的最內(nèi)部作用域開始,逐級向外或者說向上進行,直到遇見第一個匹配的標識符為止。
全局變量會自動成為全局對象(比如瀏覽器中的window 對象)的屬性,因此可以不直接通過全局對象的詞法名稱,而是間接地通過對全局對象屬性的引用來對其進行訪問。window.a通過這種技術(shù)可以訪問那些被同名變量所遮蔽的全局變量。但非全局的變量如果被遮蔽了,無論如何都無法被訪問到。
無論函數(shù)在哪里被調(diào)用,也無論它如何被調(diào)用,它的詞法作用域都只由函數(shù)被聲明時所處的位置決定。詞法作用域只會查找一級標識符,何為一級標識符呢?比如 上面代碼的 a 、b、c,如果查找的是obj.a.b;那么它只會試圖查obj 標識符,找到這個變量后,對象屬性訪問規(guī)則會分別接管對a和b屬性的訪問。
二、欺騙詞法(改變詞法作用域)
前面我們講過,詞法作用域是有變量和快作用域的位置決定的,但是在某些特殊情況下,這種情況會發(fā)生改變,也就是欺騙詞法,以下就是特殊的情況,我們繼續(xù)探討:
1、eval()
2、width()語句
eval()函數(shù)可以接受一個字符串,并執(zhí)行其中的的 JavaScript 代碼。函數(shù)的返回值是通過通過計算 string 得到的值(如果有的話)。換句話說就是可以讓你在寫代碼中用程序動態(tài)生成的代碼并運行,就相當于代碼真的是寫在了那個位置了。
在執(zhí)行代碼的時候,引擎是不知道或是不在意你寫在eval()函數(shù)內(nèi)的代碼的,也就是說引擎會一如既往的按照詞法作用域來查找代碼。比如
var b=1;
function foo(srt,a){
eval(str); //欺騙
console.log(a+‘,’+b);
}
foo('var b=2'); //3,2
通過這個例子我們可以發(fā)現(xiàn),eval('var b=2');這段代碼會被當作本來就在那里一樣來處理。由于那段代碼聲明了一個新的變量b,因此它對已經(jīng)存在的foo(..) 的詞法作用域進行了修改。這個操作對foo()里面的詞法作用域進行欺騙,從而屏蔽了foo()對外部變量b(全局變量)進行查找。所有foo()最后輸出的永遠是3,2而不會是正常情況下的1,3.
請一定要注意,這個不是正常的情況下。并且這是特殊的,當然也是不值得推薦的實踐方式,因為eval()所帶來的好處,無法抵消它對性能的損耗。
在這個例子中,我們傳遞eval()的是不變的"var b=2";而實際情況,我們非常可能會根據(jù)業(yè)務(wù)邏輯需要而傳遞進去動態(tài)生成的代碼拼接而成的字符,也就是說非常有可能代碼包含有一個或多個聲明(無論是變量還是函
數(shù)),就會對eval(..) 所處的詞法作用域進行修改。但無論何種情況,eval(..) 都可以在運行期修改書寫期的詞法作用域。或許有人會想,這個功能不錯啊,可能以后需要這個業(yè)務(wù)需求就可以用這個eval()了,但是現(xiàn)實是殘酷的,eval()有很大的弊端。具體是什么弊端,等講完with再一起總結(jié)。
在嚴格模式的程序中,eval(..) 在運行時有其自己的詞法作用域,意味著其中的聲明無法修改所在的作用域。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
with
另一個能欺騙詞法的是with關(guān)鍵字。with語句在《JavaScript》權(quán)威指南的定義是:一個可以按序檢索對象列表,通過它可以進行變量名解析。with語句用于臨時拓展作用域鏈。
這句話看的我云里霧里的,在這里我通過一些實例來理解with
var obj = {
a: 1,
b: 2,
c: 3
};
obj.a=3;
obj.b=4;
obj.c=5;
console.log(obj.a+" "+obj.b+" "+obj.c); //3 4 5
結(jié)果是毫無疑問的,with在這個情況下可以把它當成一種快捷方式:
var obj = {
a: 1,
b: 2,
c: 3
};
with(obj){
a=3;
b=4;
c=5;
}
console.log(obj.a+" "+obj.b+" "+obj.c); //3 4 5
輸出結(jié)果也毫無疑問,實際上with不僅僅是為了方便的訪問對象屬性。
function foo(obj) {
with (obj) {
a = 2;
}
}
var obj1 = {
a: 3
};
var obj2 = {
b: 3
};
foo( obj1 );
console.log( obj1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 ——不好,a 被泄漏到全局作用域上了!相當于內(nèi)存泄漏了!!!
上面我們定義了兩個對象,obj1和obj2,obj1定義一個a屬性,而obj2沒有定義a屬性,但是有b屬性,foo(..) 函數(shù)接受一個obj 參數(shù),該參數(shù)是一個對象引用,并對這個對象引用執(zhí)行了with(obj) {..}。在with 塊內(nèi)部,看起來只是對變量a 進行簡單的詞法引用,實際上就是一個LHS(不理解的話可以參考上一篇文章。) 引用,并將2 賦值給它。當我們將obj1 傳遞進去,a=2 賦值操作找到了obj1.a 并將2 賦值給它,這在后面的console.log(obj1.a) 中可以體現(xiàn)。而當obj2 傳遞進去,obj2 并沒有a 屬性,因此不會創(chuàng)建這個屬性,obj2.a 保持undefined。但是可以注意到一個奇怪的副作用,實際上a = 2 賦值操作創(chuàng)建了一個全局的變量a。這是怎么回事?
with 可以將一個沒有或有多個屬性的對象處理為一個完全隔離的詞法作用域,因此這個對象的屬性也會被處理為定義在這個作用域中的詞法標識符。
盡管with 塊可以將一個對象處理為詞法作用域,但是這個塊內(nèi)部正常的var聲明并不會被限制在這個塊的作用域中,而是被添加到with 所處的函數(shù)作用域中。
eval(..) 函數(shù)如果接受了含有一個或多個聲明的代碼,就會修改其所處的詞法作用域,而with 聲明實際上是根據(jù)你傳遞給它的對象憑空創(chuàng)建了一個全新的詞法作用域。可以這樣理解,當我們傳遞obj1 給with 時,with 所聲明的作用域是obj1,而這個作用域中含有一個同obj1.a 屬性相符的標識符。但當我們將obj2 作為作用域時,其中并沒有a 標識符,因此進行了正常的LHS 標識符查找
o2 的作用域、foo(..) 的作用域和全局作用域中都沒有找到標識符a,因此當a=2 執(zhí)行時,自動創(chuàng)建了一個全局變量(因為是非嚴格模式)。with 這種將對象及其屬性放進一個作用域并同時分配標識符的行為很讓人費解。
理論上,我們通過一些技巧可以實現(xiàn)動態(tài)代碼去修改詞法作用域(欺騙詞法),但是這種情況有個很大的弊端就是欺騙:詞法作用域會導(dǎo)致性能下降。怎么會這樣呢?前面我們有討論過引擎,JavaScript 引擎要復(fù)雜得多,JavaScript 引擎會在編譯階段進行數(shù)項的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置,才能在執(zhí)行過程中快速找到標識符。
但如果引擎在代碼中發(fā)現(xiàn)了eval(..) 或with,它只能簡單地假設(shè)關(guān)于標識符位置的判斷都是無效的,因為無法在詞法分析階段明確知道eval(..) 會接收到什么代碼,這些代碼會如何對作用域進行修改,也無法知道傳遞給with 用來創(chuàng)建新詞法作用域的對象的內(nèi)容到底是什么。最悲觀的情況是如果出現(xiàn)了eval(..) 或with,所有的優(yōu)化可能都是無意義的,因此最簡單的做法就是完全不做任何優(yōu)化。如果代碼中大量使用eval(..) 或with,那么運行起來一定會變得非常慢。
無論引擎多聰明,試圖將這些悲觀情況的副作用限制在最小范圍內(nèi),也無法避免如果沒有這些優(yōu)化,代碼會運行得更慢這個事實。
歡迎訪問我的個人網(wǎng)站zhengyepan
學(xué)習(xí)筆記,互相學(xué)習(xí)~