二、詞法作用域 (學習筆記)—— 《你不知道的JavaScript》
目錄
詞法作用域
作用域工作模型:
- 詞法作用域(大多數程式語言採用)
- 動態作用域
詞法階段
大部分標準語言編譯器的第一個工作階段就是詞法化。
詞法化的過程:會對原始碼中的程式碼進行檢查,如果是有狀態的解析過程,還會賦予單詞語義。
詞法作用域:就是定義在詞法階段的作用域。在寫程式碼時,將變數和塊作用域寫在哪裡決定的。
function foo(a) { var b = a * 2; function bar(c) { console.log(a, b, c); } bar(b * 3); } foo(2);
上面demo有3個逐級巢狀的作用域。便於理解,可以看成是逐級巢狀的氣泡。
① 包含整個全域性作用域,其中只有一個識別符號:foo ② 包含 foo 建立的作用域,其中有三個識別符號:a b bar ③ 包含 bar 建立的作用域,其中只有一個識別符號:c
作用域氣泡由其對應的作用域塊程式碼寫在哪裡決定,是逐級包含的關係。
查詢
作用域氣泡的結構和互相之間的關係給引擎提供了足夠的位置資訊,引擎通過這些資訊來查詢識別符號的位置。
在上一個程式碼片段中, 引擎執行 console.log(..) 宣告, 並查詢 a、 b 和 c 三個變數的引 用。可參考下圖。
作用域查詢,會在找到第一個匹配的識別符號時終止。
遮蔽效應:在多層巢狀的作用域中定義多個同名的識別符號。(內部識別符號會“遮蔽”外部識別符號)
拋開遮蔽效應,作用域查詢,始終從執行時所處的最內部作用域開始查詢,逐級向上進行,直到找到第一個匹配的識別符號。
被同名變數遮蔽的全域性變數可以通過 window.a 來訪問。
詞法作用域只會查詢一級識別符號。如果引用 foo.bar.baz,只會查詢 foo, 找到這個變數,物件屬性訪問規則會接管對 bar baz 屬性的訪問。
欺騙詞法
詞法作用域完全由寫程式碼時函式定義的位置來定義,如何在執行時修改(欺騙)?
有兩種機制:
eval
還是先來看個栗子吧!
function foo(str, a) { eval(str); // 欺騙引擎 var b = 3; 原本就在這裡 console.log(a, b); } var b = 2; foo('var b = 3', 1); // 1 3
可以看到,通過 eval(str),將原本不在 foo 中的的 var b = 3; 欺騙成書寫時就程式碼就在那了,以此修改了詞法作用域。
在嚴格模式中,eval()在執行時有自己的詞法作用域,其中的宣告無法修改所在的作用域。
// eval 嚴格模式
function foo(str) {
'use strict';
eval(str);
console.log(a);
}
foo('var a = 3'); // Uncaught ReferenceError: a is not defined
with
先來一個 demo
function foo(obj) {
with(obj) {
a = 2;
}
}
var obj1 = { // obj1 有 a 屬性
a: 1
}
foo(obj1);
console.log(obj1.a); // 2
var obj2 = { // obj2 沒有 a 屬性
b: 1
}
foo(obj2);
console.log(obj2.a); // undefined
console.log(a); // 2,a 被掛在到全域性作用域
with 宣告會根據傳入的物件憑空建立一個全新的詞法作用域。
傳入 obj2 時,為什麼 a 被掛在到全域性作用域,可以按下圖來理解:
注意:使用 eval() 和 with() 會有效能問題。
效能
效能肯定是不好的,可參考下面圖片,具體的文字解說可參考《你不知道的JavaScript上卷》
注:以上所有的文字、程式碼都是本人一個字一個字敲上去的,圖片也是一張一張畫出來的,轉載請註明出處,謝謝!