globalThis
并不直接指向全局对象在本章中,我们将详细介绍 JavaScript 中全局变量的工作原理。其中涉及几个有趣的现象:脚本的作用域、所谓的*全局对象*等等。
变量的*词法作用域*(简称:*作用域*)是指程序中可以访问该变量的区域。JavaScript 的作用域是*静态的*(它们在运行时不会改变),并且可以嵌套——例如
由 if
语句(B 行)引入的作用域嵌套在函数 func()
(A 行)的作用域内。
作用域 S 的最内层外部作用域称为 S 的*外部作用域*。在示例中,func
是 if
的外部作用域。
在 JavaScript 语言规范中,作用域是通过*词法环境*“实现”的。它们由两个组件组成
一个*环境记录*,用于将变量名映射到变量值(类似于字典)。这是作用域中变量的实际存储空间。记录中的名称-值对称为*绑定*。
对*外部环境*的引用——外部作用域的环境。
因此,嵌套作用域树由通过外部环境引用链接的环境树表示。
全局对象是一个对象,其属性将成为全局变量。(我们很快就会研究它究竟是如何融入环境树的。)可以通过以下全局变量访问它
globalThis
。该名称基于这样一个事实,即它在全局作用域中的值与 this
相同。window
是引用全局对象的经典方式。它在普通的浏览器代码中有效,但在*Web Workers*(与普通浏览器进程并发运行的进程)和 Node.js 中无效。self
在浏览器中的任何地方都可用(包括 Web Workers 中)。但 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()”部分