先說幾個概念:函數、執行環境、變量對象、做用域鏈、活動對象。這幾個東東之間有什麼關係呢,往下看~前端
函數你們都知道,我想說的是,js中,在函數內部有兩個特殊的對象:arguments
和 this
。 arguments
是一個類數組對象,包含着傳入函數中的全部參數。 this
引用的是函數據以執行的環境對象。web
在《js高級程序設計》中,是這樣定義的:數組
執行環境定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。瀏覽器
這裏要特別提到一個執行環境——全局執行環境。全局執行環境是最外圍的執行環境,在web瀏覽器中,全局執行環境被認爲是window對象。全局執行環境會一直存在於環境棧的最底端,直到關閉網頁或者瀏覽器。 執行環境也叫執行上下文。函數
《js高級程序設計》定義以下:this
每一個執行環境都有一個與之關聯的變量對象,環境中定義的全部變量和函數都保存在這個對象中。spa
由上能夠看出兩點:1.執行環境和變量對象是一一對應的。2.執行環境實際上是一個「虛」的概念,而變量對象是實際存在的對象,可以被解析器訪問到。不嚴謹的說,爲了訪問執行環境,就創造了變量對象這個東東,經過變量對象就能夠訪問執行環境中的全部變量和函數了,它倆實際上是一個東西,只不過一個是虛的,一個是真實存在的。設計
當一個執行環境中的全部代碼執行完畢後,該執行環境被銷燬,保存在其中的全部變量和函數定義也隨之銷燬。實際上是這個執行環境對應的變量對象被銷燬。發現不少地方都把執行環境和變量對象混談,你們彷佛不多提到變量對象,都用 「執行環境」 這四個字替代了,把執行環境說成了一個對象,沒辦法,誰讓說的是一個東西呢。code
當JS執行流進入函數時,JavaScript引擎在內部建立一個對象,叫作Variable Object。
對應函數的每個參數,在Variable Object上添加一個屬性,屬性的名字、值與參數的名字、值相同。
函數中每聲明一個變量,也會在Variable Object上添加一個屬性,名字就是變量名,所以爲變量賦值就是給Variable Object對應的屬性賦值。
在函數中訪問參數或者局部變量時,就是在variable Object上搜索相應的屬性,返回其值。
通常狀況下Variable Object是一個內部對象,JS代碼中沒法直接訪問。對象
做用域鏈的用途:保證對執行環境有權訪問的全部變量和函數的 有序 訪問。
當代碼在一個執行環境中執行時,就會建立變量對象的一個做用域鏈。
個人理解是,做用域鏈是由一個一個變量對象連接起來的一個鏈,整個做用域鏈構成了當前執行環境中變量和函數可訪問的範圍,即做用域。因爲變量對象是按必定順序連接在一塊兒的,因此就達到了對全部可訪問變量、函數有序訪問的效果。那麼它們是按怎樣的順序連接成做用域鏈的呢?這就要說到最後一個概念——活動對象。
當函數運行時就會爲其建立一個活動對象,其中包含形參和函數特殊的arguments對象。活動對象以後會作爲函數執行環境的變量對象來使用。
回到以前的問題,做用域鏈中的變量對象是如何排序的呢? 做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。但若是這個環境是函數,則將其活動對象做爲變量對象,放在其做用域鏈的前端。做用域鏈中的下一個變量對象來自包含環境,而再下一個變量對象來自下一個包含環境……這樣一直延續到全局執行環境。全局執行環境的變量對象始終是做用域鏈中的最後一個變量對象。
爲何執行環境是函數會有這樣特殊的規定呢? 《JS權威指南》中有一句很精闢的描述:
JavaScript中的函數運行在它們被定義的做用域裏,而不是它們被執行的做用域裏。
按照以前所說,在函數定義的做用域裏,當前執行環境的做用域鏈上是沒有該函數的活動對象的,爲了訪問函數內部的變量、函數,因此要將其活動對象插在當前做用域鏈的前端。
JS的語法風格和 C/C++ 相似, 但做用域的實現卻和 C/C++ 不一樣,並不是用「堆棧」方式,而是使用列表,具體過程以下(ECMA262中所述):
任何執行上下文時刻的做用域, 都是由做用域鏈(scope chain)來實現.
在執行func的定義語句的時候, 會建立一個這個函數對象的[[scope]]屬性(內部屬性,只有JS引擎能夠訪問),並將這個[[scope]]屬性連接到定義它的做用域鏈上。
在調用func的時候, 會建立一個活動對象,而後將調用參數賦值給形參數,對於缺乏的調用參數,賦值爲undefined。而後將這個活動對象作爲scope chain的最前端, 並將func的[[scope]]屬性所指向的,定義func時候的頂級活動對象,加入到scope chain.