globalThis
沒有直接指向全域物件在本章中,我們將深入探討 JavaScript 的全域變數如何運作。有幾個有趣的現象會發揮作用:腳本的範圍、所謂的全域物件等等。
變數的詞法範圍(簡稱:範圍)是程式中可以存取它的區域。JavaScript 的範圍是靜態的(它們在執行時不會改變),而且它們可以巢狀,例如
由 if
陳述式(B 行)引入的範圍巢狀在函式 func()
(A 行)的範圍內。
範圍 S 最內層的周圍範圍稱為 S 的外部範圍。在範例中,func
是 if
的外部範圍。
在 JavaScript 語言規範中,範圍是透過詞法環境「實作」的。它們包含兩個組成部分
將變數名稱對應到變數值的環境記錄(想像成字典)。這是範圍中變數的實際儲存空間。記錄中的名稱值條目稱為繫結。
對外部環境的參考,也就是外部範圍的環境。
因此,巢狀範圍的樹狀結構由透過外部環境參考連結的環境樹狀結構表示。
全域物件是一個物件,其屬性會變成全域變數。(我們很快就會探討它如何精確地融入環境樹狀結構中。)它可以透過下列全域變數存取
globalThis
。名稱的由來在於它與全域範圍內的 this
具有相同的值。window
是指稱全域物件的傳統方式。它適用於一般瀏覽器程式碼,但不適用於網頁工作執行緒(與一般瀏覽器程序同時執行的程序)和 Node.js。self
可用於瀏覽器的所有地方(包括網頁工作執行緒)。但 Node.js 不支援它。global
僅可於 Node.js 使用。globalThis
沒有直接指向全域物件在瀏覽器中,globalThis
沒有直接指向全域,而是透過間接方式。例如,考慮網頁中的 iframe
src
變更時,它就會取得新的全域物件。globalThis
的值始終相同。該值可從 iframe 外部檢查,如下所示(靈感來自 globalThis
提案中的範例)。檔案 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
瀏覽器如何確保 globalThis
在此情況下不會變更?它們在內部區分兩個物件
Window
是全域物件。每當位置變更時,它就會變更。WindowProxy
是物件,會將所有存取轉發給目前的 Window
。此物件永遠不會變更。在瀏覽器中,globalThis
指稱 WindowProxy
;在其他所有地方,它直接指稱全域物件。
全域範圍是「最外層」範圍,沒有外層範圍。它的環境是全域環境。每個環境都透過由外層環境參考連結的環境鏈,與全域環境連接。全域環境的外層環境參考為 null
。
全域環境記錄使用兩個環境記錄來管理其變數
物件環境記錄具有與一般環境記錄相同的介面,但會將其繫結保留在 JavaScript 物件中。在此情況下,物件是全域物件。
一般的(宣告式)環境記錄,具有自己的儲存空間來儲存其繫結。
何時使用這兩個記錄中的哪一個記錄,將在稍後說明。
在 JavaScript 中,我們只會在指令碼的最上層處於全域範圍。相對地,每個模組都有自己的範圍,是指令碼範圍的子範圍。
如果我們忽略變數繫結如何新增至全域環境的相對複雜規則,則全域範圍和模組範圍會像嵌套的程式碼區塊一樣運作
{ // Global scope (scope of *all* scripts)
// (Global variables)
{ // Scope of module 1
···
}
{ // Scope of module 2
···
}
// (More module scopes)
}
為了建立一個真正全域的變數,我們必須在全域範圍中,這只會發生在指令碼的最上層
const
、let
和 class
會在宣告式環境記錄中建立繫結。var
和函式宣告會在物件環境記錄中建立繫結。<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>
當我們取得或設定變數,且兩個環境記錄都有該變數的繫結時,宣告式記錄會獲勝
<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>
除了透過 var
和函式宣告建立的變數外,全域物件包含下列屬性
使用 const
或 let
可確保全域變數宣告不會影響(或受影響於)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 總結了我們在本節中學到的一切。
全域物件通常被認為是一個錯誤。因此,較新的建構式(例如 const
、let
和類別)會建立正常的全域變數(在指令碼範圍中時)。
很幸運地,大部分使用現代 JavaScript 編寫的程式碼都存在於 ECMAScript 模組和 CommonJS 模組 中。每個模組都有自己的範圍,這就是為什麼控制全域變數的規則對基於模組的程式碼來說很少重要的原因。
ECMAScript 規範中的環境和全域物件
globalThis
:
globalThis
」this
值的各種方式:Mathias Bynens 的 「通用 JavaScript 中令人恐懼的 globalThis
polyfill」瀏覽器中的全域物件
this
:「InitializeHostDefinedRealm()」 一節