作用域與詞法作用域

在《你不知道的JavaScript》:對於那些有一點 JavaScript 使用經驗但從未真正理解閉包概念的人來說,理解閉包可以看作是某種意義上的重生”。

作用域

我們先拋出一個概念:“詞法作用域是作用域的一種工作模型”,先不管這句話的深層次的意思,就但看表面,我們就應該可以得出一個結論,那就是沒有作用域的概念就沒有詞法作用域的概念。所以...接下來,你懂的...

什麼是作用域

一言以蔽之,“作用域就是一套規則,用於確定在何處以及如何查找變量(標識符)的規則”。在這句話中讀到一個關鍵點 查找變量(標識符),那麼久從查找變量說起吧。

先看一段及其簡單的代碼

 

function foo() {
    var a = 'iceman';
    console.log(a); // 輸出"iceman"
}
foo();

在foo函數執行的時候,輸出一個a變量,那麼這個a變量是哪裏來的嘞,有看到函數第一行有定義a變量的代碼var a = 'iceman'

再看一段同樣簡單的代碼

 

var b = 'programmer';
function foo() {
    console.log(b); // 輸出"programmer"
}
foo();

同樣的道理,在輸出b的時候,自己函數內部沒有找到變量b,那麼就在外層的全局中查找,找到了就停止查找並輸出了。

注意以上兩段代碼都有查找變量,第一段代碼是在函數中找到a變量,第二段代碼是在全局中找到b變量。現在閉上眼睛,我要給加粗的這兩個詞的後面加上幾個字了!

好了,打開眼睛,Duang,Duang --->函數作用域全局作用域,把這兩個詞換入到原來那句話中,第一段代碼是在函數作用域中找到a變量,第二段代碼是在全局作用域中找到b變量。

所以,懂了沒有呢?通俗的講,作用域就是查找變量的地方。在某函數中找到該變量,就可以說在該函數作用域中找到了該變量;在全局中找到該變量,就可以說在全局作用域中找到了該變量!

不知道各位同學有沒注意到一個細節,我們在查找b變量的時候,先在函數作用域中查找,沒有找到,再去全局作用域中查找,有一個往外層查找的過程。我們好像是順着一條鏈條從下往上查找變量,這條鏈條,我們就稱之爲作用域鏈

作用域嵌套

在還沒有接觸到ES6的let、const之前,只有函數作用域和全局作用域,函數作用域肯定是在全局作用域裏面的,而函數作用域中又可以繼續嵌套函數作用域,就像一個洋蔥圈一樣,如圖:

作用域嵌套.png

用代碼表示:

作用域嵌套.png

以上兩張圖可以很直觀的看出作用域的嵌套關係了吧。查找變量也是順着紅色的箭頭走的,從裏到外,這從裏到外的各層作用域就組成了作用域鏈。

作用域中變量(標識符)的查找規則

首先聲明一點,JavaScript是有編譯過程的,不要驚訝,真的有!也就是說var name = 'iceman'這段代碼,其實這是有兩個動作的:

  • 編譯器在當前作用域中聲明一個變量name

  • 運行時引擎在作用域中查找該變量,找到了name變量併爲其賦值

證明以上的說法:

 

console.log(name); // 輸出undefined
var name = 'iceman'; 

var name = 'iceman'的上一行輸出name變量,並沒有報錯,輸出undefined,說明輸出的時候該變量已經存在了,只是沒有賦值而已。

其實編譯器是這樣工作的,在代碼執行之前從上到下的進行編譯,當遇到某個用var聲明的變量的時候,先檢查在當前作用域下是否存在了該變量。如果存在,則忽略這個聲明;如果不存在,則在當前作用域中聲明該變量。

上面的這段簡單的代碼包含兩種查找類型:輸出變量的值的時候的查找類型是RHS,找到變量爲其賦值的查找類型是LHS。

我猜各位同學一定可以猜到“L”和“R”的含義,這裏的左側和右側指的是在賦值操作的左側和右側。也就是說,變量出現在賦值操作的左側時進行LHS查詢,出現在右側時進行RHS查詢。

用一句通俗的話來講,RHS就是取到它的源值。

注意:“賦值操作的左側和右側”,並不意味着只是“=”,實際上賦值操作還有好幾種形式。

在作用域中查找變量都是RHS,並且查找的規則是從當前作用域開始找,如果沒找到再到父級作用域中找,一層層往外找,如果在全局作用域如果還沒找到的話,就會報錯了:ReferenceError: 某變量 is not defined

所有的賦值操作中查找變量都是LHS。其中a=4這類賦值操作,也是會從當前作用域中查找,如果沒有找到再到外層作用域中找,如果到全局變量啊這個變量,在非嚴格模式下會創建一個全局變量a。不過,非常不建議這麼做,因爲輕則污染全局變量,重則造成內存泄漏(比如:a = 一個非常大的數組,a在全局變量中,一直用有引用,程序不會自動將其銷燬)。

詞法作用域

在上面的作用域介紹中,我們將作用域定義爲一套規則,這套規則來管理瀏覽器引擎如何在當前作用域以及嵌套的作用域中根據變量(標識符)進行變量查找。

我們在前面有拋出一個概念:“詞法作用域是作用域的一種工作模型”,作用域有兩種工作模型,在JavaScript中的詞法作用域是比較主流的一種,另一種動態作用域(比較少的語言在用)。

所謂的詞法作用域就是在你寫代碼時將變量和塊作用域寫在哪裏來決定,也就是詞法作用域是靜態的作用域,在你書寫代碼時就確定了

請看以下代碼:

 

function fn1(x) {
    var y = x + 4;
    function fn2(z) {
        console.log(x, y, z);
    }
    fn2(y * 5);
}
fn1(6); // 6 10 50

這個例子中有個三個嵌套的作用域,如圖:

  • A 爲全局作用域,有一個標識符:fn1

  • B 爲fn1所創建的作用域,有三個標識符:x、y、fn2

  • C爲fn2所創建的作用域,有一個標識符:z

作用域是由期代碼寫在哪裏決定的,並且是逐級包含的。

在此強調,詞法作用域就是作用域是由書寫代碼時函數聲明的位置來決定的。編譯階段就能夠知道全部標識符在哪裏以及是如何聲明的,所以詞法作用域是靜態的作用域,也就是詞法作用域能夠預測在執行代碼的過程中如何查找標識符。

注1:eval()和with可以通過其特殊性用來“欺騙”詞法作用域,不過正常情況下都不建議使用,會產生性能問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章