JavaScript 之做用域與做用域鏈

首發地址: https://github.com/jeuino/Blo...

概述

在上一篇《JavaScript 之執行上下文》中介紹了什麼是執行上下文與執行上下文棧,本篇文章主要總結了:javascript

  • 什麼是做用域?
  • 什麼是詞法做用域和動態做用域?它們的區別是什麼?
  • JavaScript 採用了什麼類型的做用域?
  • JavaScript 中做用域的類型?
  • 執行上下文中的做用域鏈?

做用域 Scope

《 JavaScript 引擎(V8)是如何工做的》中說到過,做用域是由 V8 的 Parser 解析器肯定的。那麼什麼是做用域呢,咱們下面來聊一聊。java

在軟件設計中,有一個公共的原則——最小受權(暴露)原則。這個原則是指在軟件設計中, 應該最小限度地暴露必要內容, 而將其餘內容都「隱藏」 起來。git

這樣作的優勢是:github

  1. 能夠下降多文件引入時,變量或函數命名出現衝突的機率;
  2. 若是將全部內容都暴露給全局環境,那麼會佔用不少無用內存,只有當關掉瀏覽器或當前窗口時,全局變量纔會被回收;
  3. 若是程序出現錯誤,能夠更小範圍的肯定出錯區域;

在 JavaScript 中就是經過做用域來實現最小受權原則的。編程

做用域規定了變量和函數的可訪問性。瀏覽器

做用域實施了一套嚴格的規則,用於規定在 JavaScript 運行時如何查找變量和函數,也就是肯定當前執行代碼對變量和函數的訪問權限。bash

做用域共有兩種主要的工做模型。 第一種是最爲廣泛的, 被大多數編程語言所採用的詞法做用域。 另一種叫做動態做用域session

JavaScript 採用的是詞法做用域,也稱爲靜態做用域。編程語言

詞法做用域

詞法做用域是由你在寫代碼時變量和函數聲明的位置來決定的。函數

詞法做用域的「父子關係」,取決於代碼書寫時的嵌套關係。

咱們來分析下面這段代碼:

var a = 1;

// foo 函數聲明在全局執行上下文中
function foo() {
    console.log(a);
}
// bar 函數聲明在全局執行上下文中
function bar() {
    var a = 2;
    foo();
}

bar();

Image  2

這裏補充一個小知識,就是上圖描述文字中出現的 RHS 是什麼意思,這涉及到了 JavaScript 執行過程當中引擎是如何查找變量的。

引擎在執行代碼時,會經過查找標識符來判斷它是否已經聲明過。查找的過程由做用域進行協助,可是引擎是怎麼查找的呢?引擎查找變量有兩種方式,分別是:

  • LHS 查詢

若是查找的目的是對變量進行賦值,則使用 LHS 查詢(告訴做用域我須要對 a 變量進行 LHS 引用,你見過它嘛?)
不成功的 LHS 引用會致使自動隱式建立一個全局變量(非嚴格模式),嚴格模式下拋出ReferenceError 異常

  • RHS 查詢

若是查找的目的是獲取變量的值,則使用 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

Image  6

若是 bar 函數中也沒有找到 a,則會順着調用棧到全局環境中查找,此時輸出結果爲 1。

bash 採用了動態做用域

如今應該對這兩個概念有個清晰的認知了吧。

JavaScript 中的做用域

JavaScript 中的做用域類型分爲:

  • 全局做用域(Global Scope)
  • 局部做用域(Local Scope)

    • 函數做用域
    • 塊級做用域(ES6)

全局做用域

全局做用域就是最頂層的做用域,只有一個,而且能夠由程序中的任何函數訪問。
在 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 之執行上下文》文尾,咱們說明了,每一個執行上下文都包含三個重要的屬性:

  • 變量對象(Variable Object,VO)
  • 做用域鏈(Scope Chain)
  • this指向
這裏咱們先不關注變量對象和 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 以內存空間》

參考:

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
相關文章
相關標籤/搜索