JS學習系列 02 - 詞法做用域

1. 兩種做用域

「做用域」咱們知道是一套規則,用來管理引擎如何在當前做用域以及嵌套的子做用域中根據標識符名稱進行變量查找。前端

做用域有兩種主要工做模型:詞法做用域動態做用域性能優化

大多數語言採用的都是詞法做用域,少數語言採用動態做用域(例如 Bash 腳本),這裏咱們主要討論詞法做用域。微信

2. 詞法

大部分標準語言編譯器的第一個工做階段叫做詞法化
簡單地說,詞法做用域是由你在寫代碼時將變量和函數(塊)做用域寫在哪裏來決定的。固然,也會有一些方法來動態修改做用域,後邊我會介紹。函數

舉個例子:性能

var a = 2;

function foo1 () {
   console.log(a);
}

function foo2 () {
   var a = 10;

   foo1();
}

foo2();

這裏輸出結果是多少呢?測試

注意,這裏結果打印的是 2優化

可能會有一些同窗認爲是 10,那就是沒有搞清楚詞法做用域的概念。
前邊介紹了,詞法做用域只取決於代碼書寫時的位置,那麼在這個例子中,函數 foo1 定義時的位置決定了它的做用域,經過下圖理解:spa

詞法做用域

foo1 和 foo2 都是分別定義在全局做用域中的函數,它們是並列的,因此在 foo1 的做用域鏈中並不包含 foo2 的做用域,雖然在 foo2 中調用了 foo1,可是 foo1 對變量 a 進行 RHS 查詢時,在本身的做用域沒有找到,引擎會去 foo1 的上級做用域(也就是全局做用域)中查找,而並不會去 foo2 的做用域中查找,最終在全局做用域中找到 a 的值爲 2。code

總結來講,不管函數在哪裏被調用,也不管它如何被調用,它的詞法做用域都只由函數被聲明時所處的位置決定。對象

3. 欺騙詞法

JavaScript 中有 3 種方式能夠用來「欺騙詞法」,動態改變做用域。

第一種: eval

JavaScript 中 eval(...) 函數能夠接受一個字符串做爲參數,並將其中的內容視爲好像在書寫時就存在於程序中這個位置的代碼。

在執行 eval(...) 以後的代碼時,引擎並不知道或在乎前面的代碼是以動態形式插入進來並對詞法做用域環境進行修改的,引擎只會像往常同樣正常進行詞法做用域的查找。

舉個例子:

function foo (str) {
   eval(str);        // "欺騙"詞法

   console.log(a);
}

var a = 2;

foo("var a = 10;");

如你們所想,輸出結果爲 10。
由於 eval("var a = 10;") 在 foo 的做用域中新建立了一個同名變量 a,引擎在 foo 做用域中對 a 進行 RHS 查詢,找到了新定義的 a,值爲 10,因此再也不向上查找全局做用域中的 a,因此致使輸出結果爲 10,這就是 eval(...) 的做用。

嚴格模式下,eval(...) 在運行時有本身的詞法做用域,意味着其中的聲明沒法修改所在的做用域。

'use strict;'

function foo (str) {
   eval(str);        // eval() 有本身的做用域,因此並不會修改 foo 的詞法做用域

   console.log(a);
}

var a = 2;

foo("var a = 10;");

這裏輸出結果爲 2。

JavaScript 中還有一些功能和 eval(...) 相似的函數,例如 setTimeout(...) 和 setInterval(...) 的第一個參數能夠是一個字符串,字符串的內容能夠解釋爲一段動態生成的代碼。這些功能已通過時而且不被提倡,最好不要使用它們。new Function(...) 函數的最後一個參數也能夠接受代碼字符串,並將其轉化爲動態生成的函數,也儘可能避免使用。

在程序中動態生成代碼的使用場景很是罕見,由於它所帶來的好處沒法抵消性能上的損失。

第二種: with
with 一般被當作重複引用同一個對象中的多個屬性的快捷方式,能夠不須要重複引用對象自己。

舉個例子:

var obj = {
   a: 2,
   b: 3
};

with (obj) {
   console.log(a);      // 2
   console.log(b);      // 3
   c = 4;         
};

console.log(c);          // 4, c 被泄露到全局做用域上

如上所示,咱們對 c 進行 LHS 查詢,由於在 with 引入的新做用域中沒有找到 c,因此向上一級做用域(這裏是全局做用域)查找,也沒有找到,在非嚴格模式下,在全局對象中新建了一個屬性 c 並賦值爲 4。

with 能夠將一個沒有或有多個屬性的對象處理爲一個徹底隔離的詞法做用域,所以這個對象的屬性也會被處理爲定義在這個做用域中的詞法標識符。

儘管 with 塊能夠將一個對象處理爲詞法做用域,可是這個塊內部正常的 var 聲明並不會限制在這個塊做用域中,而是被添加到 with 所處的函數做用域中。

嚴格模式下,with 被徹底禁止使用。

'use strict';

var obj = {
   a: 2,
   b: 3
};

with (obj) {
   console.log(a);     
   console.log(b);      
   c = 4;         
};

console.log(c);

嚴格模式下禁止使用with

第三種: try...catch
try...catch 能夠測試代碼中的錯誤。try 部分包含須要運行的代碼,而 catch 部分包含錯誤發生時運行的代碼。

舉個例子:

try {
   foo();
} catch (err) {
   console.log(err);   

   var a = 2; 
// 打印出 "ReferenceError: foo is not defined at <anonymous>:2:4"
}

console.log(a);      // 2

當 try 中的代碼出現錯誤時,就會進入 catch 塊,此時會把異常對象添加到做用域鏈的最前端,相似於 with 同樣,catch 中定義的局部變量也都會添加到包含 try...catch 的函數做用域(或全局做用域)中。

4. 性能

JavaScript 引擎會在編譯階段進行數項性能優化。其中有些優化依賴於可以根據代碼的詞法進行靜態分析,並預先肯定全部變量和函數定義的位置,才能在執行過程當中快速找到標識符。

但若是引擎在代碼中發現了 eval(...)、with 和 try...catch ,它只能簡單的假設關於標識符位置的判斷都是無效的,由於沒法在詞法分析階段明確知道 eval(...) 會接受到什麼代碼,這些代碼會如何對做用域進行修改,也沒法知道傳遞給 with 用來建立新詞法做用域的對象的內容究竟是什麼。

最悲觀的狀況是若是出現了這些動態添加做用域的代碼,全部的優化可能都是無心義的,所以最簡單的作法就是徹底不進行任何優化。

若是代碼中大量使用 eval(...) 和 with,那麼運行起來必定會變得很是緩慢。

5. 結論

不少時候咱們對代碼的分析出錯,就是源於對詞法做用域的忽略,因此讓咱們從新審視代碼,繼續努力!

歡迎關注個人公衆號

微信公衆號

相關文章
相關標籤/搜索