刨根問底:深刻研究 JavaScript 全局變量

本文的內容比較硬核,咱們一塊兒來看下 JavaScript 全局變量的底層機制究竟是怎樣的。文章會涉及腳本做用域、全局對象等概念。html

做用域

變量的詞法做用域(簡稱做用域)是程序中能夠訪問它的區域。JavaScript 的做用域是靜態的(在運行時不會改變),並能夠嵌套——例如:前端

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

if 語句(B 註釋行)引入的做用域嵌套在func()(A註釋行)的做用域內。web

包圍做用域 S 的最內層的做用域稱爲 S 的外部做用域。在本例中, funcif的外部做用域。瀏覽器

詞法環境

在 JavaScript 語言規範中,做用域是經過詞法環境實現的。它包括兩個組成部分:bash

  • 一個將變量名映射到變量值的 環境記錄。這是做用域內變量的實際存儲空間。記錄中的鍵值對稱爲 bindings
  • 外部環境 的引用——即外部做用域環境

嵌套做用域的樹形結構其實就是用互相連接的環境的樹形結構表示的。微信

全局對象

全局對象是一個特殊的對象,它的屬性就是全局變量。(後面立刻講到它是怎樣跟環境樹形結構對應上的)能夠經過如下全局變量訪問它:函數

  • 全平臺可用的 globalThis。 之因此叫這個名字,是由於它跟全局做用域裏的this值相同。
  • 其餘幾個針對特定平臺的變量:
    • window是引用全局對象的經典方式。它能夠在普通的瀏覽器代碼中使用,但不能在Web Workers 和 Node.js 中使用。
    • self 可用於瀏覽器(包括Web Workers),但不能在 Node.js 中使用。
    • global 只在 Node.js 中可用。

瀏覽器中的globalThis 並不直接指向全局對象

在瀏覽器中,globalThis 並非直接指向全局對象的,而是間接指向的。例如,假設 web 頁面裏有個 iframe:ui

  • 每當 iframe 的src 發生變化時,它都會得到一個新的全局對象。
  • 可是 globalThis 的值老是保持不變。 能夠用下面的例子來驗證:

parent.html文件:this

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

  iframe.onload = () => {
    // 訪問iframe全局對象的屬性
    const firstGlobalThis = icw.globalThis;
    const firstArray = icw.Array;
    console.log(icw.iframeName); // 'first'

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

      // 不一樣的全局對象
      console.log(icw.iframeName); // 'second'
      console.log(secondArray === firstArray); // false

      // 可是 globalThis 仍然是同樣的
      console.log(firstGlobalThis === secondGlobalThis); // true
    };
    iframe.src = 'iframe.html?second';
  };
</script>
複製代碼

iframe.html 文件:spa

<script>
  globalThis.iframeName = location.search.slice(1);
</script>
複製代碼

瀏覽器是怎麼保證 globalThis 不變的呢?其實,在內部是經過這兩個對象來區分的:

  • Window 全局對象,隨着地址改變而改變。
  • WindowProxy 代理對象,負責轉發對當前Window的訪問,這個對象不會改變。

globalThis 在瀏覽器中指向WindowProxy,在其餘環境裏中直接指向全局對象。

全局環境

全局做用域是「最外層」的做用域——它沒有外層做用域。它的環境就是 全局環境。 每一個環境都連接到它的外層環境引用,造成一個鏈條,從而連接到全局環境。全局環境的外部環境引用值爲null

全局環境記錄使用兩個環境記錄來管理其中的變量:

  • 對象環境記錄:跟普通環境記錄的接口是同樣的,只不過把 bindings  放在 JavaScript 對象裏,也就是全局對象。

  • 普通(聲明式)環境記錄:具備本身的 bindings 存儲。

這兩個記錄是怎麼用的,後面會講到。

腳本做用域和模塊做用域

在 JavaScript 中,只有在腳本的頂層屬於全局做用域。相反,每一個模塊都有本身的做用域,它是腳本做用域的子做用域。

若是咱們忽略變量 bindings 添加到全局環境的複雜規則,全局做用域和模塊做用域其實就像嵌套的代碼塊:

{ // 全局做用域 (*全部* script 的做用域)

  // (全局變量)

  { // module 1 做用域
    ···
  }
  { // module 2 做用域
    ···
  }
  // (其餘模塊做用域)
}
複製代碼

建立變量:聲明式記錄和對象記錄

爲了建立一個真正的全局變量,必須處於全局做用域——也就是在腳本的最頂層。

  • 頂層 constlet和 class 在聲明式環境記錄中建立 bindings。
  • 頂層 var 和 函數聲明在對象環境記錄裏建立 bindings。
<script>
  const one = 1;
  var two = 2;
</script>
<script>
  // 全部 script 共享同一個頂層做用域
  console.log(one); // 1
  console.log(two); // 2

  // 並不是全部聲明都會建立全局對象的屬性
  console.log(globalThis.one); // undefined
  console.log(globalThis.two); // 2
</script>
複製代碼

讀取和設置變量

當咱們獲取或設置一個變量而且兩個環境記錄都具備該變量的 binding 時,則聲明式記錄優先級更高:

<script>
  let myGlobalVariable = 1; // 聲明式環境記錄
  globalThis.myGlobalVariable = 2; // 對象環境記錄

  console.log(myGlobalVariable); // 1 (聲明式記錄優先)
  console.log(globalThis.myGlobalVariable); // 2
</script>
複製代碼

全局 ECMAScript 變量和全局宿主變量

除了經過var和函數聲明建立的變量以外,全局對象還包含如下屬性:

  • 全部內置 ECMAScript 全局變量
  • 全部內置宿主平臺全局變量(瀏覽器、Node.js 等)

使用constlet保證全局變量聲明不影響ECMAScript 和宿主平臺的內置全局變量(或免受其影響)。 例如,瀏覽器有全局變量  .location:

// 會改變當前文檔的地址:
var location = 'https://example.com';

// 不會改變 window.location
let location = 'https://example.com';
複製代碼

若是已經存在一個變量(例如本例中的location),則帶有初始化程序的var聲明的行爲就相似於賦值。這就是咱們在此示例中遇到麻煩的緣由。

請注意,只有全局做用域纔有這個問題。在模塊中,不屬於全局做用域(除非使用eval()或相似的東西)。

總結

爲何 JavaScript 既有普通全局變量又有全局對象?

全局對象一般被認爲是一個錯誤設計。所以,較新的特性實現,如 constlet,和class 則會建立普通全局變量(在 script 做用域內)。

值得慶幸的是,大多數用現代 JavaScript 編寫的代碼都位於ECMAScript 模塊和 CommonJS 模塊中。每一個模塊都有本身的做用域,這就是爲何控制全局變量的規則不多影響基於模塊的代碼。

更多前端技術乾貨盡在微信公衆號:1024譯站

微信公衆號:1024譯站
相關文章
相關標籤/搜索