深入 JavaScript
請支持這本書:購買捐款
(廣告,請不要阻擋。)

5 深入探討全域變數



在本章中,我們將深入探討 JavaScript 的全域變數如何運作。有幾個有趣的現象會發揮作用:腳本的範圍、所謂的全域物件等等。

5.1 範圍

變數的詞法範圍(簡稱:範圍)是程式中可以存取它的區域。JavaScript 的範圍是靜態的(它們在執行時不會改變),而且它們可以巢狀,例如

function func() { // (A)
  const aVariable = 1;
  if (true) { // (B)
    const anotherVariable = 2;
  }
}

if 陳述式(B 行)引入的範圍巢狀在函式 func()(A 行)的範圍內。

範圍 S 最內層的周圍範圍稱為 S 的外部範圍。在範例中,funcif 的外部範圍。

5.2 詞法環境

在 JavaScript 語言規範中,範圍是透過詞法環境「實作」的。它們包含兩個組成部分

因此,巢狀範圍的樹狀結構由透過外部環境參考連結的環境樹狀結構表示。

5.3 全域物件

全域物件是一個物件,其屬性會變成全域變數。(我們很快就會探討它如何精確地融入環境樹狀結構中。)它可以透過下列全域變數存取

5.4 在瀏覽器中,globalThis 沒有直接指向全域物件

在瀏覽器中,globalThis 沒有直接指向全域,而是透過間接方式。例如,考慮網頁中的 iframe

檔案 parent.html

<iframe src="iframe.html?first"></iframe>
<script>
  const iframe = document.querySelector('iframe');
  const icw = iframe.contentWindow; // `globalThis` of iframe

  iframe.onload = () => {
    // Access properties of global object of iframe
    const firstGlobalThis = icw.globalThis;
    const firstArray = icw.Array;
    console.log(icw.iframeName); // 'first'

    iframe.onload = () => {
      const secondGlobalThis = icw.globalThis;
      const secondArray = icw.Array;

      // The global object is different
      console.log(icw.iframeName); // 'second'
      console.log(secondArray === firstArray); // false

      // But globalThis is still the same
      console.log(firstGlobalThis === secondGlobalThis); // true
    };
    iframe.src = 'iframe.html?second';
  };
</script>

檔案 iframe.html

<script>
  globalThis.iframeName = location.search.slice(1);
</script>

瀏覽器如何確保 globalThis 在此情況下不會變更?它們在內部區分兩個物件

在瀏覽器中,globalThis 指稱 WindowProxy;在其他所有地方,它直接指稱全域物件。

5.5 全域環境

全域範圍是「最外層」範圍,沒有外層範圍。它的環境是全域環境。每個環境都透過由外層環境參考連結的環境鏈,與全域環境連接。全域環境的外層環境參考為 null

全域環境記錄使用兩個環境記錄來管理其變數

何時使用這兩個記錄中的哪一個記錄,將在稍後說明。

5.5.1 指令碼範圍和模組範圍

在 JavaScript 中,我們只會在指令碼的最上層處於全域範圍。相對地,每個模組都有自己的範圍,是指令碼範圍的子範圍。

如果我們忽略變數繫結如何新增至全域環境的相對複雜規則,則全域範圍和模組範圍會像嵌套的程式碼區塊一樣運作

{ // Global scope (scope of *all* scripts)

  // (Global variables)

  { // Scope of module 1
    ···
  }
  { // Scope of module 2
    ···
  }
  // (More module scopes)
}

5.5.2 建立變數:宣告式記錄與物件記錄

為了建立一個真正全域的變數,我們必須在全域範圍中,這只會發生在指令碼的最上層

<script>
  const one = 1;
  var two = 2;
</script>
<script>
  // All scripts share the same top-level scope:
  console.log(one); // 1
  console.log(two); // 2
  
  // Not all declarations create properties of the global object:
  console.log(globalThis.one); // undefined
  console.log(globalThis.two); // 2
</script>

5.5.3 取得或設定變數

當我們取得或設定變數,且兩個環境記錄都有該變數的繫結時,宣告式記錄會獲勝

<script>
  let myGlobalVariable = 1; // declarative environment record
  globalThis.myGlobalVariable = 2; // object environment record

  console.log(myGlobalVariable); // 1 (declarative record wins)
  console.log(globalThis.myGlobalVariable); // 2
</script>

5.5.4 全域 ECMAScript 變數和全域主機變數

除了透過 var 和函式宣告建立的變數外,全域物件包含下列屬性

使用 constlet 可確保全域變數宣告不會影響(或受影響於)ECMAScript 和主機平台的內建全域變數。

例如,瀏覽器有 全域變數 .location

// Changes the location of the current document:
var location = 'https://example.com';

// Shadows window.location, doesn’t change it:
let location = 'https://example.com';

如果變數已經存在(例如本例中的 location),則帶有初始化項的 var 宣告會像指派一樣運作。這就是我們會在本例中遇到問題的原因。

請注意,這只會在全域範圍中發生問題。在模組中,我們永遠不會在全域範圍中(除非我們使用 eval() 或類似函式)。

圖 10 總結了我們在本節中學到的一切。

圖 10:全域範圍的環境透過全域環境記錄管理其繫結,而全域環境記錄又基於兩個環境記錄:物件環境記錄(其繫結儲存在全域物件中)和宣告式環境記錄(其繫結使用內部儲存)。因此,全域變數可以透過新增屬性至全域物件或透過各種宣告來建立。全域物件會初始化為 ECMAScript 和主機平台的內建全域變數。每個 ECMAScript 模組都有自己的環境,其外部環境是全域環境。

5.6 結論:為什麼 JavaScript 會同時有正常的全域變數和全域物件?

全域物件通常被認為是一個錯誤。因此,較新的建構式(例如 constlet 和類別)會建立正常的全域變數(在指令碼範圍中時)。

很幸運地,大部分使用現代 JavaScript 編寫的程式碼都存在於 ECMAScript 模組和 CommonJS 模組 中。每個模組都有自己的範圍,這就是為什麼控制全域變數的規則對基於模組的程式碼來說很少重要的原因。

5.7 進一步閱讀和本章的來源

ECMAScript 規範中的環境和全域物件

globalThis:

瀏覽器中的全域物件