1. 程式人生 > >JS的解析原理和變數作用域

JS的解析原理和變數作用域

當我們在談變數作用域的時候,我們在談什麼?

實際上,變數作用域指的是變數的生命週期與作用範圍。比如說:

  • 全域性作用域全域性都可以訪問;
  • 區域性作用域只有在區域性才能夠訪問。
    在JS中一個函式作用域就是一個區域性作用域。

這裡講的變數作用域,主要包含兩個方面:

  • 一個方面是我們看到一個變數定義的時候,我們要知道這個變數的生命週期和作用範圍是什麼
  • 另一方面,當我們看到一個變數時,我們要知道引用的是哪裡定義的變數,也就是說我們要找到這個變數的值。

一、變數作用域

1、靜態作用域

靜態作用域又稱為詞法作用域,詞法作用域在編譯階段就可以決定變數的引用,它只跟程式定義的位置有關係,而跟執行的順序無關。程式碼執行順序可能是一個函式裡面呼叫另一個函式,也就是有巢狀關係,但是靜態作用域是不考慮是是在哪裡呼叫這些函式,它只跟程式定義的原始位置有關。執行時會回到函式被定義的地方執行,由內往外尋找變數。

對於靜態作用域,當代碼執行到foo函式,就會招到foo函式被定義的地方,alert出x變數的時候,就會順著這條作用域的鏈從函式內部往外找,找到x變數,在圖中可以很明顯地看到,會順著foo函式這條作用域鏈找到全域性作用域下的x。這個就是靜態作用域的解析方式。

這裡寫圖片描述

2、動態作用域

動態作用域在程式執行的時候才能決定引用了哪些變數,在函式執行的地方開始執行,在函式執行的地方開始有內往外尋找變數。動態作用域一般是由動態棧來管理。

程式碼執行的時候,先定義一個棧,依次把x變數、foo函式、bar函式放到棧裡面,當呼叫bar函式的時候,發現有個x變數,就把x變數放到棧裡面,然後呼叫foo函式,執行到foo函式裡面,要alert出x的值,就在棧裡面找出最近的x的值,這個值就是20。

這裡寫圖片描述

二、詞法環境

1、詞法環境的初始化

JS使用的是靜態作用域的管理方式,但是JS是沒有塊級作用域的,在JS裡面只有函式作用域,也就是隻有函式才會建立一個新的作用域,而JS裡面的if語塊和for產生的語塊等都不會產生新的作用域(像C語言等其它語言是這樣的)。另外在ES5中使用詞法環境來管理靜態作用域。

詞法環境是一種描述靜態作用域的資料結構,本質上它定義了一種資料結構,這種資料結構可以用來管理靜態作用域。

詞法環境是由以下幾部分組成:

  • 環境記錄(形參、變數、函式等)
  • 對外部詞法環境的引用(outer)

也就是說這是一種巢狀的結構,在當前的詞法環境裡,一定有一個對外層引用的指標,稱為outer,當然最外層的詞法環境指向的是null。

那什麼時候會建立詞法環境呢?答案是一段程式碼開始執行前,就會先初始化詞法環境。因為JS裡沒有塊級作用域,所以JS裡面只有全域性程式碼,或者是函式程式碼開始執行前會初始化詞法環境。那麼有哪些東西會被初始化到詞法環境呢?

  1. 第一個是形參,形參就是說如果呼叫了一個函式,函式裡面有一些引數,那麼這些形參的值是需要初始化到詞法環境裡的,這個是程式碼執行前就需要初始化好的。

  2. 第二個就是變數宣告,我們使用var宣告的變數也是需要寫到環境記錄裡的。就是說這一段程式碼還沒有執行前就需要先將這些東西寫到環境記錄裡,這個是執行前要做的初始化的工作。一個var宣告的變數記錄到詞法環境裡的時候是什麼值呢?所有的var宣告的變數初始化到環境記錄裡的時候都是undefined,只是宣告而未賦值,就是說在這一段程式碼裡聲明瞭很多變數,有的在前有的在後,我們在程式碼的前面都可以訪問到這些變數,但是這些變數的值都是undefined的,只有真正執行到語句的時候,才能夠得到變數的定義和賦值。

  3. 第三個是函式宣告,這些程式碼有一些函式宣告也是需要寫到詞法環境的環境記錄裡的。而且函式宣告在初始化詞法環境的時候,在引擎內部會建立一個函式物件,這個函式物件會把函式的形參、函式體放到這個物件裡面,還會儲存當前的作用域,也就是函式宣告的時候,要把當前的作用域儲存到儲存到這個函式物件裡面。

這裡寫圖片描述

提示:其實這裡可以解析變數宣告和函式宣告提升的問題!

2、詞法環境的構建過程

前面已經說了,當我們要開始執行一段程式碼的時候,要先初始化詞法環境,全域性程式碼也是一樣,會先建立一個全域性詞法環境,全域性詞法環境建立之後,就要初始化裡面的一些環境記錄。

var x=10;
function foo(y){
    var z=30;
    function bar(q){
        return x + y + z + q;
    }
    return bar;
}
var bar = foo(20);
bar(40);

全域性初始化

這裡寫圖片描述

程式碼執行:
執行var x=10

這裡寫圖片描述

執行var bar=foo(20)
這裡要執行foo函式,所以會建立foo函式的詞法環境
對於傳入的引數,回到執行時的環境中找。

這裡寫圖片描述

執行var z=30

這裡寫圖片描述

執行return bar
返回一個bar函式,這時全域性環境中的bar變數就變成了一個函式

這裡寫圖片描述

這裡要執行bar函式,所以會建立bar函式的詞法作用域

這裡寫圖片描述

執行bar()
順著作用域鏈找到x、y、z的值返回x+y+z+q的值

三、詞法環境的相關問題

1、函式定義、形參、變數定義名稱衝突

優先順序:函式定義>形參>變數定義

2、arguments物件

argument物件是一個在函式裡面定義好的物件,通過arguments物件我們可以訪問函式傳過來的實參,實際上arguments也是算到環境記錄裡的。其實上面需要記錄到環境記錄裡的東西並沒有全部討論,但主要的就是這些。

3、函式表示式

如果在一個函式作用域裡遇到函式表示式會怎樣?

函式的詞法環境是在函式執行前。但是其實函式宣告的outer在宣告這個函式的時候就已經儲存了這個scope。前面說到把一個函式定義放到一個環境記錄的時候,會建立一個函式物件,那麼在這個函式物件裡面會儲存當前的詞法環境,為什麼呢?實際上當我們初始化這個函式的詞法環境的時候就可以把這個詞法環境的outer設定成宣告函式時儲存的當前的詞法環境,其實在一段程式碼開始執行之前就會把這部分初始化工作完成。

函式表示式是不一樣的,函式表示式的函式物件是在執行到函式表示式這一條語句的時候才建立的函式物件,才把函式物件裡面的作用域設定成當前的作用域,函式宣告的函式物件是在程式碼執行之前就已經建立了的。也許你會問這樣不是一樣嗎?難道函式在執行過程中詞法環境還會改變嗎?答案是還真的會,在函式執行的過程中,函式的詞法環境的確是可能發生改變的,例如with和try…catch

這裡寫圖片描述

這裡寫圖片描述

提示: 關於with和try…catch的相關知識請自行了解。