什麼是作用域
編譯原理
在傳統編譯語言的流程中,程式中的一段原始碼在執行之前會經歷三個步驟,統稱為“編譯”。
-
分詞/詞法分析(Tokenizing/Lexing)
這個過程會將由字元組成的字串分解成(對程式語言來說)有意義的程式碼塊,這些程式碼塊被稱為詞法單元(token)。例如,考慮程式 var a = 2;。這段程式通常會被分解成為下面這些詞法單元:var、a、=、2 、;。空格是否會被當作詞法單元,取決於空格在這門語言中是否具有意義。
分詞(tokenizing)和詞法分析(Lexing)之間的區別是非常微妙、晦澀的,主要差異在於詞法單元的識別是通過有狀態還是無狀態的方式進行的。簡單來說,如果詞法單元生成器在判斷 a 是一個獨立的詞法單元還是其他詞法單元的一部分時,呼叫的是有狀態的解析規則,那麼這個過程就被稱為詞法分析。
-
解析/語法分析(Parsing)
這個過程是將詞法單元流(陣列)轉換成一個由元素逐級巢狀所組成的代表了程式語法結構的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree,AST)。var a = 2; 的抽象語法樹中可能會有一個叫作 VariableDeclaration 的頂級節點,接下來是一個叫作 Identifier(它的值是 a)的子節點,以及一個叫作 AssignmentExpression的子節點。AssignmentExpression 節點有一個叫作 NumericLiteral(它的值是 2)的子節點。
-
程式碼生成
將 AST 轉換為可執行程式碼的過程稱被稱為程式碼生成。這個過程與語言、目標平臺等息息相關。拋開具體細節,簡單來說就是有某種方法可以將 var a = 2;的 AST 轉化為一組機器指令,用來建立一個叫作 a 的變數(包括分配記憶體等),並將一個值儲存在 a 中。
這段解釋來源於《你不知道的JavaScript》。
瞭解這三個步驟對於我們理解接下來的作用域有很大的幫助。
什麼是作用域
定義:
作用域:負責收集並維護由所有宣告的識別符號(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權----《你不知道的JavaScript》。
- 引擎
從頭到尾負責整個 JavaScript 程式的編譯及執行過程。 - 編譯器
負責語法分析及程式碼生成等髒活累活。 - 作用域
負責收集並維護由所有宣告的識別符號(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權(《你不知道的JavaScript》)。
變數的賦值操作會執行兩個動作,首先編譯器會在當前作用域中宣告一個變數(如果之前沒有宣告過),然後在執行時引擎會在作用域中查詢該變數,如果能夠找到就會對它賦值.
而 JavaScript高階程式設計 一書中對於作用域(執行環境)的定義如下: 執行環境: 執行環境定義了變數或函式有權訪問的其他資料,決定了它們各自的行為。每個執行環境都有一個與之關聯的變數物件(variable object),環境中定義的所有變數和函式都儲存在這個物件中。《JavaScript高階程式設計》
全域性執行環境:全域性執行環境是最外圍的一個執行環境。根據 ECMAScript 實現所在的宿主環境不同,表示執行環境的物件也不一樣。在 Web 瀏覽器中,全域性執行環境被認為是 window 物件,因此所有全域性變數和函式都是作為 window 物件的屬性和方法建立的。某個執行環境中的所有程式碼執行完畢後,該環境被銷燬,儲存在其中的所有變數和函式定義也隨之銷燬(全域性執行環境直到應用程式退出——例如關閉網頁或瀏覽器——時才會被銷燬)。
函式執行環境:每個函式都有自己的執行環境。當執行流進入到函式時,會把這個函式的執行環境壓入執行棧中,等到執行結束後,再從棧中彈出,此時再把執行權交給外層的執行環境。
作用域鏈: 當程式碼在一個執行環境中執行時,會建立一個變數的作用域鏈,來保證執行環境對所有變數和函式的有序訪問。作用域的前端,始終是當前執行環境的變數物件,作用域的後端,始終是全域性變數物件(作用域巢狀意思一樣)。
var color = "blue";
function changeColor () {
var anotherColor = "red";
function swapColors () {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor; // 這裡可以訪問 color、anotherColor 和 tempColor
}
// 這裡可以訪問 color 和 anotherColor,但不能訪問 tempColor
swapColors();
}
// 這裡只能訪問 color
changeColor();
複製程式碼
分析:以上共存在三個執行環境:
全域性執行環境: 變數color、函式changeColor()
changeColor區域性環境:變數anotherColor、函式swapColors(),可以訪問的是變數color、anotherColor
swapColors區域性環境: 變數tempColor,可以訪問的是變數color、anotherColor、tempColor
函式引數也被當作變數來對待,因此其訪問規則與執行環境中的其他變數相同。
詞法作用域
定義:
詞法作用域:詞法作用域就是定義在詞法階段的作用域。換句話說,詞法作用域是由你在寫程式碼時將變數和塊作用域寫在哪裡來決定的,因此當詞法分析器處理程式碼時會保持作用域不變(大部分情況下是這樣的)。
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar(); // 2
複製程式碼
分析:bar呼叫,bar裡面foo被呼叫,foo函式需要查詢變數a.
由於javascript採用詞法作用域,foo被解析的時候是在全域性作用域,所以a是全域性作用域中的2,而非bar裡面的a。假設js採用的是動態作用域,foo是在bar中被呼叫的,所以a查詢到了bar作用域裡的3。
欺騙詞法
with
with 可以將一個沒有或有多個屬性的物件處理為一個完全隔離的詞法作用域,因此這個物件的屬性也會被處理為定義在這個作用域中的詞法識別符號。儘管 with 塊可以將一個物件處理為詞法作用域,但是這個塊內部正常的 var宣告並不會被限制在這個塊的作用域中,而是被新增到 with 所處的函式作用域中。
function buildUrl () {
var qs = "? debug = true";
with (location) {
var url = href + qs;
}
return url;
}
複製程式碼
eval()
JavaScript 中的 eval(..) 函式可以接受一個字串為引數,並將其中的內容視為好像在書寫時就存在於程式中這個位置的程式碼。個人覺得,相當於在eval()的位置又重新把這個字串變數宣告瞭,所以原本屬於父級執行環境的變數在當前執行環境中也可以找到。
function foo(str, a) {
eval( str ); // 欺騙!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
複製程式碼
塊級作用域
塊級作用域:從ES6開始,JavaScript具有了塊級作用域。 為什麼要有塊級作用域?1、防止內層變數修改外層變數。2、用來計數的迴圈變數洩漏為全域性變數。
var a = 1;
function f() {
console.log(a);
if (false) {
var a = 2;
}
}
f(); // undefined
複製程式碼
分析:這段程式碼的本意是if外使用全域性變數,內部使用內層的a變數。但是由於變數提升,最後輸出了undefined。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
複製程式碼
分析:變數i只是為了控制迴圈,但是洩漏成了全域性變數,如果另外一個for迴圈繼續使用i變數控制迴圈,就會出錯。
程式碼塊:在ES6標準中,用{}包裹的程式碼。塊級作用域是可以巢狀的。
- with:with語句也會常見一個程式碼塊。
- try/catch():catch分句會建立一個塊級作用域。
- let、const:ES6中規定,let、const宣告的變數在未定義前都不能使用。
作用域提升
- 變數和函式都可以提升。
console.log(a); // undefined
var a = 1;
function foo () {
console.log(b); // undefined
var b = 2;
return a + b; // 3
}
foo()
複製程式碼
- 函式宣告的函式體可以提升,但是函式表示式不能提升。
console.log(a); // undefined
console.log(foo);// foo(){...}
console.log(bar); // undefined
var a = 1;
function foo () {
console.log(b); // undefined
var b = 2;
return a + b; // 3
}
var bar = function bar () {
console.log(d); // undefined
var d = 2;
return a + d; // 3
}
foo()
bar()
複製程式碼
- 函式的提升要先於變數的提升。
foo(); // 1
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 1 );
}
複製程式碼
- 用let、const宣告的變數不會被提升,let和const宣告的變數存在“暫時性死區”:在變數未被宣告之前,不能使用該變數。