用 globalThis 访问全局对象[每日前端夜话0xF6]

JavaScript 语言越来越被广泛地用于各种环境中。除了 Web 浏览器(这是 JavaScript 的最常见的宿主环境类型)之外,你还可以在服务器,智能手机甚至机器人硬件中运行 JavaScript 程序。

每个环境都有其自己的对象模型,并提供了不同的语法来访问全局对象。例如,在Web浏览器中,可以通过 windowselfframes 访问全局对象。但是在 Node.js 中,这些属性不存在,而你必须使用 global 。在 Web Worker 中,只有 self 可用。

这些引用全局对象的不同方式使编写能够在多个环境中工作的可移植 JavaScript 代码变得非常困难。幸运的是,有一个正在开发中的提案【 https://github.com/tc39/proposal-global 】打算通过引入一个名为 globalThis 的标准属性来解决这个问题,该属性将在所有环境中可用。

在本文中,我们将首先研究流行的 JavaScript 环境中的全局对象,然后看看 globalThis 是如何提供一种统一的机制来访问它。

`window`

window 属性用于在浏览器环境中引用当前文档的全局对象。在代码的顶层,使用 var 关键字声明的变量将成为 window 的属性,并且可能够在代码中的任何位置访问:

1var a = [10, 20];
2
3console.log(window.a);          // → [10, 20]
4console.log(a === window.a);    // → true

通常在使用 window 的属性时,由于隐含引用的缘故不必直接引用 window 。但是当有一个与全局变量同名的局部变量时,使用 window 是唯一的选择:

1var a = 10;
2
3(function() {
4  var a = 20;   
5  console.log(a);           // → 20
6  console.log(window.a);    // → 10
7})();

如你所见,无论代码在什么作用域内运行, window 对于引用全局对象都非常有用。注意, window 实际上引用了 window.window 。因此, window.window === window

除了标准的 JavaScript 属性和方法之外, window 对象还包含其他一些属性和方法,这些属性和方法使我们能够控制 Web 浏览器窗口以及文档本身。

`self`

Web Workers API没有 window 对象,因为它没有浏览上下文。相反,它提供了 WorkerGlobalScope 接口,其中包含通常由 WorkerGlobalScope 承载的数据。

为了访问 Web Workers 中的全局对象,我们需要使用 self ,它是 Window 对象的 window 属性的同义词。与 window 类似, self 是对全局对象的引用,可用于显式引用:

1// a web worker
2console.log(self);    // => DedicatedWorkerGlobalScope {...}
3
4var a = 10;
5
6console.log(self.a);          // → 10
7console.log(a === self.a);    // → true

在浏览器环境中,此代码将记录 Window 而不是 DedicatedWorkerGlobalScope 。由于 self 的值会根据使用环境的不同而变化,所以有时最好使用 Windowself 在 web worker 上下文中引用 WorkerGlobalScope.self ,而在浏览器上下文中引用 window.self

重要的是不要将 self 属性与声明局部变量(用于维护对上下文的引用)的常见 JavaScript 模式混淆。例如:

 1const obj = {
 2  myProperty: 10,
 3  myMethod: function(){
 4    console.log(this === obj);    // => true
 5
 6    // store the value of this in a variable for use in nested functions
 7    const self = this;
 8
 9    const helperFunction = (function() {
10      console.log(self === obj);  // => true (self refers to the outer this value)
11      console.log(this === obj);  // => false (this refers to the global object. In strict mode, it has a value of undefined)
12    })();
13  }
14};
15
16// invoke myMethod on the object obj.
17obj.myMethod();

`frames`

另一种在浏览器环境中访问全局对象的方法是使用 frames 属性,该属性的作用类似于 selfwindow

1// browser environment
2console.log(frames);    // => Window {...}

这个只读属性通常用于获取当前窗口的子帧列表。例如你可以用 window.frames [0]frames [0] 访问第一帧。

`global`

在 Node.js 中,你可以使用 global 关键字访问全局对象:

1// node environment
2console.log(global);    // => Object [global] {...}

windowselfframes 在 Node 环境中不起作用。请记住,Node.js 中的顶级作用域不是全局作用域。在浏览器中, var abc = 123 将创建一个全局变量。但是在 Node.js 中变量是模块本身的局部变量。

`this`

在浏览器中,可以在程序的顶层使用 this 关键字来引用全局对象:

1this.foo = 123;
2console.log(this.foo === window.foo);    // => true

this 在非严格模式下在函数或箭头函数内也引用全局对象。但是在严格模式下运行的函数就不是这种情况了,其中 this 的值为 undefined

 1(function() {
 2  console.log(this);    // => Window {...}
 3})();
 4
 5(() => {
 6  console.log(this);    // => Window {...}
 7})();
 8
 9(function() {
10  "use strict";
11  console.log(this);    // => undefined
12})();

在 Node 模块中,顶层的 this 不引用全局对象。相反,它与 module.exports 具有相同的值。在函数内部(Node 环境), this 的值取决于函数的调用方式。在 JavaScript 模块中,顶层的 thisundefined

介绍 `globalThis`

globalThis 旨在通过定义标准的全局属性来整合越来越分散的访问全局对象的方式。 globalThis 提案目前处于第 4 阶段,这意味着它已准备好纳入 ES2020 标准。所有流行的浏览器,包括 Chrome 71 +,Firefox 65+和Safari 12.1+,都已支持该功能。你也可以在 Node.js 12+ 中使用它。

1// browser environment
2console.log(globalThis);    // => Window {...}
3
4// node.js environment
5console.log(globalThis);    // => Object [global] {...}
6
7// web worker environment
8console.log(globalThis);    // => DedicatedWorkerGlobalScope {...}

通过使用 globalThis ,你的代码能够在窗口和非窗口上下文中工作,而无需编写其他检查或测试代码。在大多数环境中, globalThis 直接引用该环境的全局对象。但是在浏览器中,内部需要使用代理来考虑 iframe 和跨窗口安全性。实际上,它并不会改变你编写代码的方式。

通常,当你不确定要在哪种环境中使用代码时,或者当你想使代码在不同环境中可执行时,可以用 globalThis 属性。不过你必须用 polyfill 在不支持该功能的旧版浏览器上实现该功能。

另一方面,如果需要你确定要在什么环境中使用代码,请使用前面列举引用环境全局对象的现有方法之一,避免为 globalThis 添加 polyfill 的麻烦。

创建一个 `globalThis` polyfill

在引入 globalThis 之前,一种常用的跨环境访问全局对象的方法是使用以下模式:

1function getGlobalObject() {
2  return Function('return this')();
3}
4
5if (typeof getGlobalObject().Promise.allSettled !== 'function') {
6  // the Promise.allSettled() method is not available in this environment
7}

这段代码的问题在于,在强制执行内容安全策略(CSP)【 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP 】的网站中不能用 Function 构造函数和 eval 。由于CSP的缘故,Chrome 的扩展程序系统也不允许此类代码运行。

引用全局对象的另一种模式如下:

 1function getGlobalObject() {
 2  if (typeof globalThis !== 'undefined') { return globalThis; }
 3  if (typeof self !== 'undefined') { return self; }
 4  if (typeof window !== 'undefined') { return window; }
 5  if (typeof global !== 'undefined') { return global; }
 6  throw new Error('cannot find the global object');
 7};
 8
 9if (typeof getGlobalObject().Promise.allSettled !== 'function') {
10  // the Promise.allSettled() method is not available in this environment
11}

这种模式通常在 web 上使用。但也有几个缺陷【 https://mathiasbynens.be/notes/globalthis#naive-polyfill 】,使其在某些情况下不可靠。幸运的是 Chrome DevTools 团队的Mathias Bynens 提出了一种创意模式【 https://mathiasbynens.be/notes/globalthis#robust-polyfill 】,它没有这些缺点:

 1(function() {
 2  if (typeof globalThis === 'object') return;
 3  Object.defineProperty(Object.prototype, '__magic__', {
 4    get: function() {
 5      return this;
 6    },
 7    configurable: true // This makes it possible to `delete` the getter later.
 8  });
 9  __magic__.globalThis = __magic__; // lolwat
10  delete Object.prototype.__magic__;
11}());
12
13// Your code can use `globalThis` now.
14console.log(globalThis);

与其他方法相比,polyfill 是更可靠的解决方案,但仍然不够完美。正如  Mathias 提到的那样,修改 ObjectObject.defineProperty 或   Object.prototype.__defineGetter__ 可能会破坏 polyfill。

总结

能够用在多种环境中的可移植 JavaScript 代码很难编写。每个主机环境都有一个略有不同的对象模型。因此,要访问全局对象,你需要在不同的 JavaScript 环境中使用不同的语法。

通过引入 globalThis 属性,访问全局对象将变得更加简单,并且不再需要去检测代码所运行的环境。

乍一看 globalThis 似乎很容易实现。但是实际上,正确地进行操作是非常复杂的。现有的解决方法都不完美,如果不小心就可能会引入错误。

ECMAScript 正在迅速发展,你可以期望它能够更多地引入新功能。要获取有关规范最新添加的更新,请查看完成的提案【 https://github.com/tc39/proposals/blob/master/finished-proposals.md 】列表。

What is globalThis, and why should you start using it?