你不知道的JavaScript-2.詞法作用域
考慮以下代碼:
function foo(a) { var b = a * 2; function bar(c) { console.log( a, b, c ); } bar( b * 3 ); } foo( 2 ); // 2, 4, 12
在這個例子中有三個逐級嵌套的作用域。
- 包含著整個全局作用域,其中只有一個標識符: foo 。
- 包含著 foo 所創建的作用域,其中有三個標識符: a 、 bar 和 b 。
- 包含著 bar 所創建的作用域,其中只有一個標識符: c 。
全局變量會自動成為全局對象(比如瀏覽器中的 window 對象)的屬性,因此可以不直接通過全局對象的詞法名稱,而是間接地通過對全局對象屬性的引用來對其進行訪問。
window.a
通過方式可以訪問那些被同名局部變量所遮蔽的全局變量。但非全局的變量如果被遮蔽了,無論如何都無法被訪問到。
eval
JavaScript 中的 eval(..) 函數可以接受一個字符串為參數,並將其中的內容視為好像在書寫時就存在於程序中這個位置的代碼。換句話說,可以在你寫的代碼中用程序生成代碼並運行,就好像代碼是寫在那個位置的一樣。
考慮以下代碼:
function foo(str, a) { eval( str ); // 欺騙! console.log( a, b ); } var b = 2; foo( "var b = 3;", 1 ); // 1, 3
eval(..) 調用中的 "var b = 3;" 這段代碼會被當作本來就在那裏一樣來處理。
由於那段代碼聲明了一個新的變量 b ,因此它對已經存在的 foo(..) 的詞法作用域進行了修改。事實上,和前面提到的原理一樣,這段代碼實際上在 foo(..) 內部創建了一個變量 b ,並遮蔽了外部(全局)作用域中的同名變量。
with
with 通常被當作重復引用同一個對象中的多個屬性的快捷方式,可以不需要重復引用對象本身。
// 單調乏味的重復 "obj" obj.a = 2; obj.b = 3; obj.c = 4; // 簡單的快捷方式 with (obj) { a = 3; b = 4; c = 5; }
但實際上這不僅僅是為了方便地訪問對象屬性。考慮如下代碼:
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 被泄漏到全局作用域上了!
可以註意到一個奇怪的副作用,實際上 a = 2 賦值操作創建了一個全局的變量 a 。這是怎麽回事?
可以這樣理解,當我們傳遞 o1 給 with 時, with 所聲明的作用域是 o1 ,而這個作用域中含有一個同 o1.a 屬性相符的標識符。但當我們將 o2 作為作用域時,其中並沒有 a 標識符,因此進行了正常的 LHS 標識符查找.
o2 的作用域、 foo(..) 的作用域和全局作用域中都沒有找到標識符 a ,因此當 a=2 執行時,自動創建了一個全局變量(因為是非嚴格模式)。
eval、with 會導致引擎無法在編譯時對作用域查找進行優化,從而影響性能。
你不知道的JavaScript-2.詞法作用域