問題:詞法作用域是什麼?怎麼才能在運行時“修改”(也可以說是欺騙)詞法作用域呢?
2.1 詞法作用域
簡單的說,詞法作用域就是定義在詞法階段的作用域。詞法作用域由你寫代碼時把變量和塊作用域寫在哪裏決定的。
因此,當詞法分析器處理代碼時會保持作用域不變(大部分情況下)。
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2);
這個例子中有三個逐級嵌套的作用域
- 全局作用域,只有一個標識符,foo
- foo所創建的作用域,三個標識符,a、bar和b
- bar所創建的作用域,只有一個標識符,c
查找
作用域查找會在找到第一個匹配的標識符時停止。
在多層的嵌套作用域中可以定義同名的標識符,這叫作“遮蔽效應”(內部標識符遮蔽了外部標識符)。
拋開遮蔽效應,作用域查找始終從運行時所處的最內部作用域開始逐級向外或者說向上,直到遇見第一個匹配的標識符爲止。
全局變量會自動成爲全局對象(比如瀏覽器中的Windows對象)的屬性,因此可以通過對全局對象屬性的引用來對其訪問(window.a)
通過這種技術可以訪問那些被遮蔽的全局變量。
詞法作用域查找只會查找一級標識符,比如 a、b、c。如果代碼使用了foo.bar.baz,詞法作用域只會試圖查找foo標識符,找到這個變量後,對象屬性訪問規則會分別接管bar和baz屬性的訪問。
2.2 欺騙詞法
怎麼才能在運行時“修改”(也可以說是欺騙)詞法作用域呢?JavaScript有兩種機制來實現,但是都會導致性能下降。
2.2.1 eval
function foo(str, a) {
eval(str); //欺騙
console.log(a, b);
}
var b = 2;
foo("var b=3;", 1) //1,3
eval(..)通常用來執行動態創建的代碼。
eval(..)調用的“var b=3”這段代碼會被當做本來就在那裏一樣處理。
事實上,這段代碼在foo(..)內部創建了一個變量b,並遮蔽了外部(全局)作用域中的同名變量。
在嚴格模式中,eval(..)有自己 的作用域,這意味着其中的聲明無法修改所在的作用域。
2.2.2 with
function foo(obj) {
with(obj) {
a = 2;
}
}
var o1 = {
a: 3
}
var o2 = {
b: 3
}
foo(o1);
console.log(o1.a); //2
foo(o2);
console.log(o2.a); //undefined
console.log(a); //2,a被泄露到全局作用域上了
with(obj)的作用實際上是一個LHS引用
找到obj,並把值賦給obj.a
- 當o1傳遞進去,a=2賦值操作找到了o1.a並賦值給它
- 當o2傳遞進去,o2並沒有a這個屬性,因此並不會(在with創造的作用域內)創建這個屬性,o2.a保持undefined,進行正常的LHS標識符查找,一直查找到全局作用域都沒有找到時,自動創建了一個全局變量(非嚴格模式)
with塊可以將一個對象處理爲一個完全隔離的詞法作用域,因此這個對象的屬性也會被處理爲定義在這個作用域中的詞法標識符
但是這個塊內部正常的var聲明並不會被限制在這個塊的作用域中,而是被添加到with所處的函數作用域中,也就是with(..){}中
區別:eval(..)函數會修改其所處的詞法作用域,而with聲明實際上是根據你傳遞的對象憑空建造了一個全新的詞法作用域。
2.2.3 性能
JavaScript引擎會在編譯階段進行數項的性能優化。其中有些優化依賴於根據代碼的詞法進行靜態分析,並預先確定所有變量和函數的定義位置,才能在執行過程中快速的找到標識符
但如果引擎在代碼中發現了eval(..)或者with,他無法知道會對作用域帶來怎麼樣的改變,所有優化可能都是沒用意義的,因此最簡單的方法是完全不做任何優化。
2.3 小結
詞法作用域意味着作用域是由書寫代碼的位置決定的。
編譯的詞法分析階段基本能夠知道全部標識符在哪裏以及是如何聲明的,從而能預測執行過程中如何進行查找
JavaScript有兩個機制可以“欺騙”詞法作用域:eval(..)和with。
前值可以對一段包含一個或多個聲明的“代碼”進行演算,並藉此修改以及存在詞法作用域(在運行時)
後者本質上是通過將一個對象的引用當作作用域來處理,將對象的屬性當作作用域中的標識符來處理,從而創建了一個新的詞法作用域(同樣是在運行時)
副作用就是引擎無法在編譯時對作用域進行優化,因此引擎只能謹慎的認爲這樣的優化是無效的。使用任何一個機制都將導致代碼運行變慢。不要使用他們!