1. 程式人生 > >【讀書筆記】你唔知JS 詞法作用域

【讀書筆記】你唔知JS 詞法作用域

詞法階段

 

簡單地說, 詞法作用域就是定義在詞法階段的作用域。

換句話說, 詞法作用域是由你在寫程式碼時將變數和塊作用域寫在哪裡來決定的, 因此當詞法分析器處理程式碼時會保持作用域不變。 

 

window.a

通過這種技術可以訪問那些被同名變數所遮蔽的全域性變數。

但非全域性的變數如果被遮蔽了, 無論如何都無法被訪問到。 

 


 

欺騙詞法

 

function foo(str, a) {
    eval(str);
    console.log(a, b);
}
var b = 2;
foo(
'var b = 3;', 1); // 1, 3

JavaScript 中的 eval(..) 函式可以接受一個字串為引數, 並將其中的內容視為好像在書寫時就存在於程式中這個位置的程式碼。 

 

eval(..) 通常被用來執行動態建立的程式碼, 因為像例子中這樣動態地執行一段固定字元所組成的程式碼, 並沒有比直接將程式碼寫在那裡更有好處。 

 

function foo(str) {
    'use strict';
    eval(str);
    console.log(a); // ReferenceError: a is not defined
} foo('var a = 2');

嚴格模式的程式中, eval(..) 在執行時有其自己的詞法作用域, 意味著其中的宣告無法修改所在的作用域。 

 

setTimeout(..) 和setInterval(..) 的第一個引數可以是字串, 字串的內容可以被解釋為一段動態生成的函式程式碼。 這些功能已經過時且並不被提倡。 不要使用它們! 

 

new Function(..) 函式的行為也很類似, 最後一個引數可以接受程式碼字串, 並將其轉化為動態生成的函式(前面的引數是這個新生成的函式的形參)。 

 

var
obj = { a: 1, b: 2, c: 3 }; // 單調乏味的重複'obj' obj.a = 2; obj.b = 3; obj.c = 4; // 簡單的快捷方式 with(obj) { a = 3; b = 4; c = 5; }

with 通常被當作重複引用同一個物件中的多個屬性的快捷方式, 可以不需要重複引用物件本身。 

 

儘管 with 塊可以將一個物件處理為詞法作用域, 但是這個塊內部正常的 var 宣告並不會被限制在這個塊的作用域中, 而是被新增到 with 所處的函式作用域中。 

o2 的作用域、 foo(..) 的作用域和全域性作用域中都沒有找到識別符號 a, 因此當 a=2 執行時, 自動建立了一個全域性變數(因為是非嚴格模式)。 

 


 

效能

 

1.eval(..) 和 with 會在執行時修改或建立新的作用域, 以此來欺騙其他在書寫時定義的詞法作用域。 

 

2.JavaScript 引擎會在編譯階段進行數項的效能優化。

其中有些優化依賴於能夠根據程式碼的詞法進行靜態分析, 並預先確定所有變數和函式的定義位置, 才能在執行過程中快速找到識別符號。

 

3.但如果引擎在程式碼中發現了 eval(..) 或 with, 它只能簡單地假設關於識別符號位置的判斷都是無效的, 因為無法在詞法分析階段明確知道 eval(..) 會接收到什麼程式碼, 這些程式碼會如何對作用域進行修改, 也無法知道傳遞給 with 用來建立新詞法作用域的物件的內容到底是什麼。

 

4.如果程式碼中大量使用 eval(..) 或 with, 那麼執行起來一定會變得非常

 


 

小結

 

1.詞法作用域意味著作用域是由書寫程式碼時函式宣告的位置來決定的。

編譯的詞法分析階段基本能夠知道全部識別符號在哪裡以及是如何宣告的, 從而能夠預測在執行過程中如何對它們進行查詢

 

2.JavaScript 中有兩個機制可以“欺騙” 詞法作用域: eval(..) 和 with。

前者可以對一段包含一個或多個宣告的“程式碼” 字串進行演算, 並藉此來修改已經存在的詞法作用域(在執行時)。

後者本質上是通過將一個物件的引用當作作用域來處理, 將物件的屬性當作作用域中的識別符號來處理,從而建立了一個新的詞法作用域(同樣是在執行時)。

 

3.這兩個機制的副作用是引擎無法在編譯時對作用域查詢進行優化, 因為引擎只能謹慎地認為這樣的優化是無效的,使用這其中任何一個機制都將導致程式碼執行變慢,不要使用它們。