首發地址: https://github.com/jeuino/Blo...
在上一篇《JavaScript 之執行上下文》中介紹了什麼是執行上下文與執行上下文棧,本篇文章主要總結了:javascript
在《 JavaScript 引擎(V8)是如何工做的》中說到過,做用域是由 V8 的 Parser 解析器肯定的。那麼什麼是做用域呢,咱們下面來聊一聊。java
在軟件設計中,有一個公共的原則——最小受權(暴露)原則。這個原則是指在軟件設計中, 應該最小限度地暴露必要內容, 而將其餘內容都「隱藏」 起來。git
這樣作的優勢是:github
在 JavaScript 中就是經過做用域來實現最小受權原則的。編程
做用域規定了變量和函數的可訪問性。瀏覽器
做用域實施了一套嚴格的規則,用於規定在 JavaScript 運行時如何查找變量和函數,也就是肯定當前執行代碼對變量和函數的訪問權限。bash
做用域共有兩種主要的工做模型。 第一種是最爲廣泛的, 被大多數編程語言所採用的詞法做用域。 另一種叫做動態做用域。session
JavaScript 採用的是詞法做用域,也稱爲靜態做用域。編程語言
詞法做用域是由你在寫代碼時變量和函數聲明的位置來決定的。函數
詞法做用域的「父子關係」,取決於代碼書寫時的嵌套關係。
咱們來分析下面這段代碼:
var a = 1; // foo 函數聲明在全局執行上下文中 function foo() { console.log(a); } // bar 函數聲明在全局執行上下文中 function bar() { var a = 2; foo(); } bar();
這裏補充一個小知識,就是上圖描述文字中出現的 RHS 是什麼意思,這涉及到了 JavaScript 執行過程當中引擎是如何查找變量的。
引擎在執行代碼時,會經過查找標識符來判斷它是否已經聲明過。查找的過程由做用域進行協助,可是引擎是怎麼查找的呢?引擎查找變量有兩種方式,分別是:
若是查找的目的是對變量進行賦值,則使用 LHS 查詢(告訴做用域我須要對 a 變量進行 LHS 引用,你見過它嘛?)
不成功的 LHS 引用會致使自動隱式建立一個全局變量(非嚴格模式),嚴格模式下拋出ReferenceError 異常
若是查找的目的是獲取變量的值,則使用 RHS 查詢(告訴做用域我須要對 a 變量進行 RHS 引用,你見過它嘛?)
不成功的 RHS 查詢會拋出 ReferenceError 異常,不會隱式建立一個全局變量。
請看下面這個例子,其中 RHS 共使用了三次,LHS 共使用了兩次,你能找到都是在哪裏使用了 RHS 和 LHS 嗎?
function add(a, b) { return a + b; } add(1, 2)
好了,咱們再回到詞法做用域上。看了上述分析,可能你仍是不太明白什麼是詞法做用域,下面咱們再來看下什麼是動態做用域,經過與動態做用域進行對比,你應該會有一個更清晰的認知。
詞法做用域是在寫代碼或者說定義時肯定的,而動態做用域是在運行時肯定的。
動態做用域不關心函數和變量是在何處聲明的,只關心它們是從何處調用的;
動態做用域是基於調用棧 的,而不是代碼中的做用域嵌套。
咱們從動態做用域的角度,再來分析上面那段代碼:
var a = 1; function foo() { console.log(a); } function bar() { var a = 2; // 在 bar 函數內調用 foo foo(); } bar(); // 1
若是 bar 函數中也沒有找到 a,則會順着調用棧到全局環境中查找,此時輸出結果爲 1。
bash 採用了動態做用域
如今應該對這兩個概念有個清晰的認知了吧。
JavaScript 中的做用域類型分爲:
局部做用域(Local Scope)
全局做用域就是最頂層的做用域,只有一個,而且能夠由程序中的任何函數訪問。
在 JavaScript 中,如下兩種狀況聲明的變量和函數會處於全局做用域內:
var a = 1; // window.a function foo() {} // window.foo
function foo () { a = 1; // window.a }
全局做用域中的數據,均可以經過 window 對象的屬性來訪問。
每一個函數都有本身的做用域。函數做用域有權訪問全局做用域,反之不行。
// Global Scope function fn() { // Local Scope #1 function someOtherFunction() { // Local Scope #2 } }
塊做用域是 ES6 的新特性,它指的是變量不只能夠屬於所處的做用域,也能夠屬於某個代碼塊( { .. } 內部)。
只有使用 let 和 const 關鍵字聲明的變量纔會產生塊級做用域。
if (true) { // if 條件語句不會建立一個做用域 // a 處於全局做用域中 var a = 'a'; // b 處於當前塊級做用域內 let b = 'b'; // c 也處於當前塊級做用域內 const c = 'c'; } console.log(a); // a console.log(b); // Uncaught ReferenceError: b is not defined console.log(c); // Uncaught ReferenceError: c is not defined
咱們都知道,局部做用域有權訪問自身做用域和全局做用域;若是一個函數內部嵌套了一個函數,則嵌套的函數也是有權訪問自身做用域、聲明所在函數做用域以及全局做用域的。
每一個做用域都存在一條由可訪問的做用域造成的做用域鏈(Scope Chain)。
舉個例子:
var a = 1 function foo () { var b = a + 1; function bar () { var c = b + a } bar () } foo ()
當咱們開始執行上述代碼時,首先會建立一個全局執行上下文。在上一篇《JavaScript 之執行上下文》文尾,咱們說明了,每一個執行上下文都包含三個重要的屬性:
這裏咱們先不關注變量對象和
this
,後面會有單獨的文章進行介紹。
執行上下文的僞代碼能夠表示以下:
global_EC = { scopeChain: { // current scope + scopes of all its parents global_scope }, variableObject: { // All the variables including inner variables & functions, function arguments }, this: {} }
在做用域鏈(scopeChain)中按照"從大到小"的順序依次存放着當前做用域和它的全部父級做用域。
在全局執行上下文中,它的做用域鏈只包含一個做用域,即全局做用域。
scopeChain = [global_scope]
當執行 foo 函數時,foo 執行上下文的做用域鏈以下所示:
scopeChain = [global_scope, foo_scope]
當執行 bar 函數時,bar 執行上下文的做用域鏈以下所示:
scopeChain = [global_scope, foo_scope, bar_scope]
做用域鏈的查詢:
當解釋器在執行代碼遇到一個變量時,它首先會在當前做用域內查找其值;若是找不到,它會遍歷做用域鏈,繼續從上一級做用域查找;依此類推,直到找到變量或到達做用域鏈的末尾(全局做用域)時結束。
原來下篇文章是想寫執行上下文中的變量對象的,可是想在介紹變量和函數是如何引用的以前,先總結一下它們是如何存儲的。因此調整了一下發文順序。
參考:
JavaScript深刻之詞法做用域和動態做用域
How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time
Understanding Scope and Scope Chain in JavaScript
Understanding Scope in JavaScript