說到 Javascript 中的做用域,一般一同出現的還有一個執行上下文(execution context)的概念,之前我在網上搜索相關的內容老是理不清這二者的關係。彷佛函數,做用域,執行上下文這三者天生就是糾纏在一塊兒的。爲了得到一手的資料我翻看了 ES6 規範,把他們究竟是什麼梳理了一下:javascript
首先咱們來講下做用域,簡單來講做用域就是一個區域,包含了其中變量,常量,函數等等定義信息和賦值信息,以及這個區域內代碼書寫的結構信息。做用域能夠嵌套。咱們一般知道 js 中函數的定義能夠產生做用域,下面咱們用具體代碼來示例下:java
全局做用域(global scope)裏面定義了兩個變量,一個函數。walk 函數生成的做用域裏面定義了一個變量,兩個函數。innerFunc 和 anotherInnerFunc 這兩個函數生成的做用域裏面分別定義了一個變量。在規範中做用域更官方的叫法是詞法環境(Lexical Environments)。什麼意思?就是做用域包含哪些內容取決於你代碼怎麼寫,你把定義 go 變量寫在了 walk 函數裏面,那麼 go 變量就屬於 walk 函數做用域。函數
做用域其實由兩部分組成:this
記錄做用域內變量信息(咱們假設變量,常量,函數等統稱爲變量)和代碼結構信息的東西,稱之爲 Environment Record。lua
一個引用 __outer__
,這個引用指向當前做用域的父做用域。拿上面代碼爲例。innerFunc 的函數做用域有一個引用指向 walk 函數做用域,walk 函數做用域有一個引用指向全局做用域。全局做用域的 __outer__
爲 null。spa
規範中定義了查找一個變量的過程:先查看當前做用域裏面的 Environment Record 是否有此變量的信息,若是找到了,則返回當前做用域內的這個變量。若是沒有查找到,則順着 __outer__
到父做用域裏面的 Environment Record 查找,以此遞歸。因此咱們一般所說的函數內同名變量遮蔽全局變量就是這麼回事。不過若是你在變量查找的時候指定某個做用域中的 Environment Record,那麼也是能夠的,譬如:window.name
【其實 window 對象就是全局做用域的 Environment Record 對象,可是普通函數做用域的 Environment Record 對象是獲取不到的】。code
函數聲明orm
function f() { var inner = 'inner'; console.log( inner ); } f(); // inner; console.log( inner ); // Uncaught ReferenceError: inner is not defined
catch 語句對象
try { throw new Error( 'customized error' ); } catch( err ) { var iamnoterror = 'not error'; console.log( iamnoterror ); // not error console.log( err ); // Error: customized error } console.log( iamnoterror ); // not error console.log( err ); // Uncaught ReferenceError: e is not defined
這裏特別指出的是 catch 語句生成的做用域只會框住參數部分的變量(err),使其不能在外面訪問。對於 catch 語句體裏面聲明的變量並不起做用。咱們看規範裏面怎麼說:遞歸
For each element argName of the BoundNames of CatchParameter, do
Perform catchEnv.CreateMutableBinding(argName).
catchEvn 就是 catch 語句生成的做用域,可是這個做用域只保存參數列表中的變量(CreateMutableBinding(argName))。
語句塊
if ( true ) { let bv = 'bv'; const B_C = 'BC'; let blockFunc = function() {} function notBlockFunc() {} console.log( bv ); // bv console.log( B_C ); // BC console.log( notBlockFunc ); // function notBlockFunc() {} console.log( blockFunc ); // function () {} } console.log( bv ); // Uncaught ReferenceError: bv is not defined console.log( B_C ); // Uncaught ReferenceError: B_C is not defined console.log( notBlockFunc ); // function notBlockFunc() {} console.log( blockFunc ); // ReferenceError: blockFunc is not defined
語句塊 {}
會生成一個新的做用域,可是這個做用域只綁定塊級變量,常量等,即 let,const 聲明的屬於塊級做用域,而 var 聲明的仍是屬於塊級做用域的父做用域。
接下來咱們說下執行上下文(execution context),執行上下文是用於跟蹤代碼的運行狀況,其特徵以下:
一段代碼塊對應一個執行上下文,被封裝成函數的代碼被視做一段代碼塊,或者「全局做用域」也被視做一段代碼塊。
當程序運行,進入到某段代碼塊時,一個新的執行上下文被建立,並被放入一個 stack 中。當程序運行到這段代碼塊結尾後,對應的執行上下文被彈出 stack。
當程序在某段代碼塊中運行到某個點須要轉到了另外一個代碼塊時(調用了另外一個函數),那麼當前的可執行上下文的狀態會被置爲掛起,而後生成一個新的可執行上下文放入 stack 的頂部。
stack 最頂部的可執行上下文被稱爲 running execution context。當頂部的可執行上下文被彈出後,上一個掛起的可執行上下文繼續執行。
咱們用代碼來示例下(從 outer 調用到 level1 調用,再逐層返回):
執行上下文對象的內部屬性:
[[code evaluation]]
:當前代碼塊執行的狀態:prerform,suspend,resume。
[[Function]]
:若是當前執行上下文對應的是一個函數,那麼這個屬性就保存的這個函數對象。若是對應的是全局環境(能夠是一個 script 或者 module),屬性值是 null。
[[Real]]
:相似與沙箱的概念?(我尚未看懂,不過不太影響此篇的內容)。
若是程序執行到某個點拋出異常了,那麼咱們能夠用這個記錄執行上下文的 stack 來追蹤到底哪裏出錯了,能夠看到整個調用棧,此時內部屬性 [[Function]]
就起到做用了:
其實你們看下做用域和執行上下文各自的職責,你會發現他們幾乎是沒有啥交集的。那麼爲啥一般二者會被同時提到呢?由於在一個函數被執行時,建立的執行上下文對象除了保存了些代碼執行的信息,還會把當前的做用域保存在執行上下文中。因此它們的關係只是存儲關係。
結合做用域和執行上下文,咱們再來看下變量查找的過程。其實第一步不是到做用域裏面找 Environment Record,而是先從當前的執行上下文中找保存的做用域(對象),而後再是經過做用域鏈向上查找變量。並且同一個執行上下文保存的做用域(對象)是可變的,當代碼在同一個執行上下文中執行的時候,若是碰到有必要生成一個新做用域的時候,這個新的做用域會被添加到做用域鏈的頭部,而後執行上下文就保存的做用域對象就更新成這個新的做用域。等這個新的做用域生命週期完成後,做用域鏈又會恢復到以前的情況,而後執行上下文保存的做用域也會恢復成以前的。示例:
稍微提下,我看到網上有把執行上下文等同於 this 的文章,其實 this 的值是經過當前執行上下文中保存的做用域(對象)來獲取到的,規範以下。
ResolveThisBinding ( )
The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context. ResolveThisBinding performs the following steps:
Let envRec be GetThisEnvironment( ).
Return envRec.GetThisBinding().
我接下來會要總結函數做爲普通函數和做爲構造函數被調用時的區別,那個時候應該會對 this 有更深刻的解釋。