在講正文以前,咱們須要先了解幾個角色和概念:segmentfault
若是想深刻學習做用域的相關知識請查看《JavaScript深刻之做用域》
咱們常常說 JavaScript 是一門解釋型語言,區別於編譯型語言,可是實際上 JavaScript 是一門編譯語言。但與傳統的編譯語言不一樣, 它不是提早編譯的, 編譯結果也不能在分佈式系統中進行移植。分佈式
事實上,任何 JavaScript 代碼片斷在執行前都要進行編譯,而後作好執行它的準備,一般編譯後就會立刻執行。函數
咱們如下面這段代碼爲例來講明 JavaScript 究竟是如何執行代碼的。學習
var a = 2; function m() { console.log('m'); } m();
上面咱們說了 JavaScript 在執行代碼前是先進行編譯的,上述代碼雖然只有三塊(變量聲明、函數聲明、函數調用),可是對於引擎來講卻至關於四個指令:spa
var a
和 function m() {}
:編譯階段執行;(全部的變量和函數聲明都是在編譯階段執行的)a = 2
和 m();
:執行階段執行;下面咱們將詳細介紹一下在編譯和執行階段具體是如何處理的。code
若是不瞭解編譯原理的相關知識(詞法分析、語法分析、AST、代碼生成),請查看 《編譯原理之基礎篇》
var a
時,編譯器會詢問做用域是否已經存在一個該名稱的標識符,若是存在,編譯器則忽略該指令,繼續編譯;不然它會要求做用域在當前做用域的集合中聲明一個命名爲 a 的標識符(變量)。function m() {}
時,也會去詢問做用域是否存在命名爲 m 的函數聲明,若是存在,編譯器則忽略該指令,繼續編譯;不然它會要求做用域在當前做用域的集合中聲明一個命名爲 m 的函數。引擎在執行代碼時:blog
遇到 a = 2
時,會詢問做用域,在當前做用域是否存在一個叫做 a 的標識符,若是是,做用域就會將其返還給引擎,引擎則會使用這個標識符;若是否,引擎會繼續查找該變量(詢問當前做用域的父級做用域,依次類推,直到頂層(全局做用域))。ip
若是引擎最終找到了 a ,就會將 2 賦值給它。作用域
不然在嚴格模式下,引擎會拋出一個 ReferenceError 異常(同做用域判別失敗相關,做用域中沒有找到想要的標識符);get
在非嚴格模式下,若是在頂層(全局做用域) 中也沒法找到目標標識符,全局做用域中就會隱式建立一個具備該名稱的標識符, 並將其返還給引擎。
總結:變量的賦值操做會執行兩個動做,首先編譯器會在當前做用域中聲明一個變量(若是以前沒有聲明過),而後再運行時引擎會在做用域中查找該變量,若是可以找到就會對它賦值。
當引擎遇到m();
時,查找 m 標識符的過程同上。不一樣點是:在找不到 m 是,在非嚴格模式下,不會隱式建立一個具備該名稱的標識符,在下面【執行過程當中引擎是如何查找變量的】中會解釋緣由。
還有一個須要注意的點是,當找到 m 標識符後,引擎會開始執行該函數。此時若是該標識符表明的是一個函數,那麼函數能夠正常執行;可是若是標識符表明的是一個變量,那麼就會拋出TypeError 異常 (表明做用域判別成功了, 可是對結果的操做是非法或不合理的),以下圖所示:
此時 a 是一個變量,因此當嘗試執行它時,會報錯 a 不是一個函數。~~~~
上面咱們留了一個疑問,就是爲何在找不到函數標識符的時候,全局做用域不會像變量聲明同樣也隱式建立一個,下面咱們就來看看是爲何。
引擎在執行代碼時,會經過查找標識符 a 和 m 來判斷它是否已經聲明過。查找的過程由做用域進行協助,可是引擎是怎麼查找的呢?引擎查找變量有兩種方式,分別是:
請看下面這個例子,其中 RHS 共使用了三次,LHS 共使用了兩次,你能找到都是在哪裏使用了 RHS 和 LHS 嗎?
function add(a, b) { return a + b; } add(1, 2)
參考資料:《你不知道的JavaScript》 上篇