本文的內容比較硬核,咱們一塊兒來看下 JavaScript 全局變量的底層機制究竟是怎樣的。文章會涉及腳本做用域、全局對象等概念。html
變量的詞法做用域(簡稱做用域)是程序中能夠訪問它的區域。JavaScript 的做用域是靜態的(在運行時不會改變),並能夠嵌套——例如:前端
function func() { // (A)
const aVariable = 1;
if (true) { // (B)
const anotherVariable = 2;
}
}
複製代碼
if
語句(B 註釋行)引入的做用域嵌套在func()
(A註釋行)的做用域內。web
包圍做用域 S 的最內層的做用域稱爲 S 的外部做用域。在本例中, func
是if
的外部做用域。瀏覽器
在 JavaScript 語言規範中,做用域是經過詞法環境實現的。它包括兩個組成部分:bash
嵌套做用域的樹形結構其實就是用互相連接的環境的樹形結構表示的。微信
全局對象是一個特殊的對象,它的屬性就是全局變量。(後面立刻講到它是怎樣跟環境樹形結構對應上的)能夠經過如下全局變量訪問它:函數
globalThis
。 之因此叫這個名字,是由於它跟全局做用域裏的this
值相同。window
是引用全局對象的經典方式。它能夠在普通的瀏覽器代碼中使用,但不能在Web Workers 和 Node.js 中使用。self
可用於瀏覽器(包括Web Workers),但不能在 Node.js 中使用。global
只在 Node.js 中可用。globalThis
並不直接指向全局對象在瀏覽器中,globalThis
並非直接指向全局對象的,而是間接指向的。例如,假設 web 頁面裏有個 iframe:ui
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 做用域
···
}
// (其餘模塊做用域)
}
複製代碼
爲了建立一個真正的全局變量,必須處於全局做用域——也就是在腳本的最頂層。
const
,let
和 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>
複製代碼
除了經過var
和函數聲明建立的變量以外,全局對象還包含如下屬性:
使用const
或let
保證全局變量聲明不影響ECMAScript 和宿主平臺的內置全局變量(或免受其影響)。 例如,瀏覽器有全局變量 .location
:
// 會改變當前文檔的地址:
var location = 'https://example.com';
// 不會改變 window.location
let location = 'https://example.com';
複製代碼
若是已經存在一個變量(例如本例中的location
),則帶有初始化程序的var
聲明的行爲就相似於賦值。這就是咱們在此示例中遇到麻煩的緣由。
請注意,只有全局做用域纔有這個問題。在模塊中,不屬於全局做用域(除非使用eval()
或相似的東西)。
爲何 JavaScript 既有普通全局變量又有全局對象?
全局對象一般被認爲是一個錯誤設計。所以,較新的特性實現,如 const
,let
,和class
則會建立普通全局變量(在 script 做用域內)。
值得慶幸的是,大多數用現代 JavaScript 編寫的代碼都位於ECMAScript 模塊和 CommonJS 模塊中。每一個模塊都有本身的做用域,這就是爲何控制全局變量的規則不多影響基於模塊的代碼。