JavaScript中的作用域是js中比較重要的一部分,也是大多數面試中必考的內容,我們有必要更加深入的瞭解下js中作用域。
看一個栗子
仔細閱讀以下JavaScript程式碼,你覺得執行結果會是什麼呢?是 1 還是2?
不是1,也不是2,答案卻是是undefined.
為什麼會產生這個讓人意外的結果呢?我們得來看下js中的預解析。
JavaScript預解析
JavaScript在瀏覽器中執行的過程分為兩個階段預解析階段 執行階段,在JavaScript引擎對JavaScript程式碼進行執行之前,需要進行預先處理,然後再對處理後的程式碼進行執行。
我們平時書寫的JavaScript程式碼並不是JavaScript執行的程式碼(V8引擎讀取一行執行一行這種理解是錯誤的),它需要預解釋後,再由引擎進行執行.
具體的解釋過程涉及到瀏覽器核心的技術不屬於前端領域,不過我們可以淺顯的理解一下V8在處理JavaScript的一般過程:
以上例中的var a = 2;為例,我們一般人的理解為宣告瞭一個值為2的變數a,但是在JavaScript引擎處理時卻分為了兩個步驟:
1. 讀取var a後,在當前作用域中查詢是否有相同宣告,如果沒有就在當前作用域集合中建立一個名為a的變數,否則忽略此宣告繼續進行解析.
2. 接下來,V8引擎會處理a = 2的賦值操作,首先會詢問當前作用域中是否有名為a的變數,如果有進行賦值,否則繼續向上級作用域詢問.
JavaScript執行環境
我們上面提到的所謂javascript預解釋正是建立函式的執行環境(又稱“執行上下文”),只有搞定了javascript的執行環境我們才能搞清楚一段程式碼在執行過後為什麼產生這樣的結果。
我們用一段虛擬碼表示創立的執行環境
作用域鏈(scopeChain)包括下面提到的變數物件(variableObject)和所有父級執行上下文中的變數物件.
變數物件(variableObject)是與執行上下文相關的資料作用域,一個與上下文相關的特殊物件,其中儲存了在上下文中定義的變數和函式宣告:
· 變數
· 函式宣告
· 函式的形參
在有了這些基板概念之後我們可以梳理一下js引擎建立執行的過程:
· 建立階段
· 建立Scope chain
· 建立variableObject
· 設定this
· 執行階段
· 變數的值、函式的引用
· 執行程式碼
而變數物件的建立細節如下:
· 根據函式的引數,建立並初始化arguments object
· 掃描函式內部程式碼,查詢函式宣告(Function declaration)
· 對於所有找到的函式宣告,將函式名和函式引用存入變數物件中
· 如果變數物件中已經有同名的函式,那麼就進行覆蓋
· 掃描函式內部程式碼,查詢變數宣告(Variable declaration)
· 對於所有找到的變數宣告,將變數名存入變數物件中,並初始化為"undefined"
· 如果變數名稱跟已經宣告的形式引數或函式相同,則變數宣告不會干擾已經存在的這類屬性
變數提升
正是由於以上的處理,產生了大家熟知的JavaScript中的變數提升,具體以上程式碼的執行過程如以下虛擬碼所示:
我們可以明顯看到,a變數在預解釋階段已經被賦值undefined,在執行階段js是自上而下單線執行,當console.log(a)執行之時,a=2還沒有被執行,a變數的值便是預處理階段被賦予的undefined,
函式宣告與函式表示式
我們看到,在編譯器處理階段,除了被var宣告的變數會有變數提升這一特性之外,函式也會產生這一特性,但是函式宣告與函式表示式兩種正規化建立的函式卻表現出不同的結果.
我們先看一個例項,執行以下程式碼
f成功被列印出來,而g函式出現了型別錯誤,這是什麼原因呢?
我們看到,在預解釋階段函式宣告的f是被指向了正確的函式得以執行,而函式表示式g被賦予undefined,undefined無法被當作函式執行因此報錯g is not a function.
衝突處理
通常情況下我們不會將同一變數變數重複宣告,但是出現了類似情況後,編譯器會如何處理這些衝突呢?
1. 變數之間衝突
執行以下函式:
結果顯而易見,後宣告變數值覆蓋前者的值
1. 函式之間衝突
結果同變數衝突,後者覆蓋前者.
2. 函式與變數之間衝突
結果如下,函式宣告將覆蓋變數宣告
[Function: f]
ES6中的let
在ES6中出現了兩個最新的宣告語法let與const,我們以let為例,進行測試看看與var的區別.
這段程式碼直接報錯顯示未定義,let與const擁有類似的特性,阻止了變數提升,當程式碼執行到console.log(a)時,執行換將中a還從未被定義,因此產生了錯誤.