1. 程式人生 > >二、詞法作用域 (學習筆記)—— 《你不知道的JavaScript》

二、詞法作用域 (學習筆記)—— 《你不知道的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上卷》

注:以上所有的文字、程式碼都是本人一個字一個字敲上去的,圖片也是一張一張畫出來的,轉載請註明出處,謝謝!