通用 JavaScript 中可怕的 globalThis polyfill
globalThis
提案引入了一种统一的机制,可以在任何 JavaScript 环境中访问全局 this
。 polyfill
听起来很简单,但事实证明很难做到正确。 我甚至认为这是不可能的,直到图恩用一个意想不到的、创造性的解决方案让我大吃一惊。
这篇文章描述了编写适当的 globalThis
polyfill 的困难。 这样的 polyfill 有以下要求:
- 它必须在任何 JavaScript 环境中工作,包括网络浏览器、网络浏览器中的工作程序、网络浏览器中的扩展、Node.js、Deno 和独立的 JavaScript 引擎二进制文件。
- 它必须支持草率模式、严格模式和 JavaScript 模块。
- 无论代码运行在何种上下文中,它都必须工作。(也就是说,即使 polyfill 在构建时由打包程序包装在严格模式函数中,它也必须仍然产生正确的结果。)
术语
但首先,请注意一些术语。 globalThis
在全局范围内提供 this
的值。 由于复杂的原因,这与 Web 浏览器中的全局对象不同。
请注意
,在 JavaScript 模块中,在全局范围和我们的代码之间有一个模块范围。 模块作用域隐藏了全局作用域的this
值,所以你在模块顶层看到的this
值实际上是未定义的。
globalThis
不是“全局对象”; 它只是来自全局范围的this
。
globalThis 替代品
在浏览器中,globalThis
等同于 window
:
globalThis === window;
// → true
frames 也适用:
globalThis === frames;
// → true
但是,window 和 frames 在上下文(例如网络工作者和服务工作者)中是未定义的。 幸运的是,self
可以在所有浏览器上下文中工作,因此是一个更健壮的替代方案:
globalThis === self;
// → true
不过,window
、frames
和 self
在 Node.js 中都不可用。 相反,可以使用 global
:
globalThis === global;
// → true
以上(window、frames、self、global)在独立的 JavaScript 引擎 shell 中都不可用,例如 jsvu 安装的那些。 在那里,我们可以访问全局:
globalThis === this;
// → true
此外,sloppy
模式函数总是将它们的 this 设置为全局 this,因此即使我们不能在全局范围内运行代码,我们仍然可以在 sloppy
模式下访问全局 this,如下所示:
globalThis === (function() {
return this;
})();
// → true
然而,顶层的 this
值在 JavaScript 模块中是未定义的,而 this
在严格模式函数中也是未定义的,所以这种方法在那里不起作用。
一旦处于严格模式上下文中,只有一种方法可以暂时摆脱它:Function 构造函数,它会生成 sloppy
的函数!
globalThis === Function('return this')();
// → true
好吧,有两种方法,因为间接 eval
具有相同的效果:
globalThis === (0, eval)('this');
// → true
在 Web 浏览器中,使用内容安全策略 (CSP) 通常不允许使用 Function constructor
和 eval
。 网站通常会选择加入这样的政策,但它也在 Chrome 扩展程序中强制执行,例如。 不幸的是,这意味着适当的 polyfill 不能依赖 Function 构造函数或 eval。
注意
:setTimeout('globalThis = this', 0)
出于同样的原因被排除。 除了通常被 CSP 阻止之外,还有两个其他原因反对将setTimeout
用于 polyfill。 首先,它不是 ECMAScript 的一部分,并非在所有 JavaScript 环境中都可用。 其次,它是异步的,所以即使到处都支持setTimeout
,在其他代码依赖的 polyfill 中使用它也会很痛苦。
简单的 polyfill
似乎可以将上述技术组合成一个 polyfill,如下所示:
// A naive globalThis shim. Don’t use this!
const getGlobalThis = () => {
if (typeof globalThis !== 'undefined') return globalThis;
if (typeof self !== 'undefined') return self;
if (typeof window !== 'undefined') return window;
if (typeof global !== 'undefined') return global;
if (typeof this !== 'undefined') return this;
throw new Error('Unable to locate global `this`');
};
// Note: `var` is used instead of `const` to ensure `globalThis`
// becomes a global variable (as opposed to a variable in the
// top-level lexical scope) when running in the global scope.
var globalThis = getGlobalThis();
但是,遗憾的是,这在非浏览器环境中的严格模式函数或 JavaScript 模块中不起作用(具有 globalThis
支持的那些除外)。 此外,getGlobal
可能会返回不正确的结果,因为它依赖于上下文相关的 this
,并且可能会被 bundler/packer 更改。
一个健壮的 polyfill
是否有可能编写一个健壮的 globalThis
polyfill? 假设一个环境:
-
我们不能依赖
globalThis
、window
、self
、global
或this
的值; - 我们不能使用 Function 构造函数或 eval;
- 但我们可以依赖所有其他 JavaScript 内置功能的完整性。
事实证明有一个解决方案,但它并不漂亮。 让我们考虑一下。
我们如何在不知道如何直接访问它的情况下访问全局 this
? 如果我们能以某种方式在 globalThis
上安装一个函数属性,并将其作为 globalThis
上的方法调用,那么我们就可以从该函数访问 this:
globalThis.foo = function() {
return this;
};
var globalThisPolyfilled = globalThis.foo();
我们如何在不依赖 globalThis
或任何引用它的主机特定绑定的情况下做类似的事情? 我们不能只做以下事情:
function foo() {
return this;
}
var globalThisPolyfilled = foo();
foo()
现在不再作为方法调用,因此它的 this
在严格模式或上面讨论的 JavaScript 模块中是未定义的。 严格模式函数将其 this
设置为未定义。 然而,getter
和 setter
却不是这样!
Object.defineProperty(globalThis, '__magic__', {
get: function() {
return this;
},
configurable: true // This makes it possible to `delete` the getter later.
});
// Note: `var` is used instead of `const` to ensure `globalThis`
// becomes a global variable (as opposed to a variable in the
// top-level lexical scope) when run in the global scope.
var globalThisPolyfilled = __magic__;
delete globalThis.__magic__;
上面的程序在 globalThis
上安装了一个 getter,访问 getter 以获得对 globalThis
的引用,然后通过删除 getter 进行清理。 这种技术使我们能够在所有需要的情况下访问 globalThis
,但它仍然依赖于在第一行(它说 globalThis
的地方)对全局 this
的引用。 我们能避免这种依赖吗? 我们如何在不直接访问 globalThis
的情况下安装全局可访问的 getter?
我们没有将 getter 安装在 globalThis
上,而是将其安装在全局 this
对象继承自的对象上——Object.prototype
:
Object.defineProperty(Object.prototype, '__magic__', {
get: function() {
return this;
},
configurable: true // This makes it possible to `delete` the getter later.
});
// Note: `var` is used instead of `const` to ensure `globalThis`
// becomes a global variable (as opposed to a variable in the
// top-level lexical scope).
var globalThis = __magic__;
delete Object.prototype.__magic__;
注意
:在globalThis
提议之前,ECMAScript 规范实际上并没有强制要求全局 this 继承自Object.prototype
,只是它必须是一个对象。Object.create(null)
创建一个不继承自Object.prototype
的对象。 JavaScript 引擎可以在不违反规范的情况下使用全局 this 这样的对象,在这种情况下,上面的代码片段仍然无法工作(事实上,Internet Explorer 7 做了类似的事情!)。 幸运的是,更多现代 JavaScript 引擎似乎都同意全局 this 必须在其原型链中包含Object.prototype
。
为了避免在 globalThis
已经可用的现代环境中改变 Object.prototype
,我们可以按如下方式更改 polyfill:
(function() {
if (typeof globalThis === 'object') return;
Object.defineProperty(Object.prototype, '__magic__', {
get: function() {
return this;
},
configurable: true // This makes it possible to `delete` the getter later.
});
__magic__.globalThis = __magic__; // lolwat
delete Object.prototype.__magic__;
}());
// Your code can use `globalThis` now.
console.log(globalThis);
或者,我们可以使用 __defineGetter__
:
(function() {
if (typeof globalThis === 'object') return;
Object.prototype.__defineGetter__('__magic__', function() {
return this;
});
__magic__.globalThis = __magic__; // lolwat
delete Object.prototype.__magic__;
}());
// Your code can use `globalThis` now.
console.log(globalThis);
就是这样:你见过的最可怕的 polyfill
! 它完全违背了不修改不属于你的对象的普遍最佳做法。 正如 JavaScript 引擎基础:优化原型中所述,使用内置原型通常不是一个好主意。
另一方面,如果有人在 polyfill 代码运行之前设法改变 Object 或 Object.defineProperty
(或 Object.prototype.__defineGetter__
),那么这个 polyfill 唯一可能被破坏的方法。 我想不出更强大的解决方案。 你是否可以?
测试 polyfill
polyfill 是通用 JavaScript 的一个很好的有趣示例:它是纯 JavaScript 代码,不依赖于任何特定于主机的内置函数,因此可以在任何实现 ECMAScript 的环境中运行。 这首先是 polyfill 的目标之一! 让我们确认它确实有效。
这是 polyfill 的 HTML 演示页面,它使用经典脚本 globalthis.js 和模块 globalthis.mjs(具有相同的源代码)记录 globalThis
。 该演示可用于验证 polyfill 在浏览器中的工作情况。 globalThis
在 V8 v7.1 / Chrome 71、Firefox 65、Safari 12.1 和 iOS Safari 12.2 中得到原生支持。 要测试 polyfill 有趣的部分,请在旧版浏览器中打开演示页面。
注意
:polyfill 在 Internet Explorer 10 及更早版本中不起作用。 在那些浏览器中,__magic__.globalThis = __magic__
行并没有使globalThis
全局可用,尽管__magic__
是对全局 this 的工作引用。 结果是__magic__ !== window
虽然都是[object Window]
,说明这些浏览器可能会混淆全局对象和全局 this 的区别。 修改 polyfill 以回退到备选方案之一,使其在 IE 10 和 IE 9 中工作。对于 IE 8 支持,将对Object.defineProperty
的调用包装在 try-catch 中,类似地回退到 catch 块中。 (这样做也避免了 IE 7 的全局 this 不继承自Object.prototype
的问题。)尝试使用旧的 IE 支持的演示。
要在 Node.js 和独立的 JavaScript 引擎二进制文件中进行测试,请下载完全相同的 JavaScript 文件:
# Download the polyfill + demo code as a module.
$ curl https://www.jiyik.com/demo_source/globalthis.mjs > globalthis.mjs
# Create a copy (well, symlink) of the file, to be used as a classic script.
$ ln -s globalthis.mjs globalthis.js
现在我们可以在 node 中测试:
$ node globalthis.mjs
Testing the polyfill in a module
[object global]
$ node globalthis.js
Testing the polyfill in a classic script
[object global]
要在独立的 JavaScript 引擎 shell 中进行测试,请使用 jsvu 安装任何所需的引擎,然后直接运行脚本。 例如,要在 V8 v7.0(不支持 globalThis)和 v7.1(支持 globalThis)中进行测试:
$ jsvu v8@7.0 # Install the `v8-7.0.276` binary.
$ v8-7.0.276 globalthis.mjs
Testing the polyfill in a module
[object global]
$ v8-7.0.276 globalthis.js
Testing the polyfill in a classic script
[object global]
$ jsvu v8@7.1 # Install the `v8-7.1.302` binary.
$ v8-7.1.302 globalthis.js
Testing the polyfill in a classic script
[object global]
$ v8-7.1.302 globalthis.mjs
Testing the polyfill in a module
[object global]
同样的技术允许我们在 JavaScriptCore、SpiderMonkey、Chakra 和其他 JavaScript 引擎(如 XS)中进行测试。 这是一个使用 JavaScriptCore 的例子:
$ jsvu # Install the `javascriptcore` binary.
$ javascriptcore globalthis.mjs
Testing the polyfill in a module
[object global]
$ javascriptcore globalthis.js
Testing the polyfill in a classic script
[object global]
总结
编写通用 JavaScript 可能很棘手,并且通常需要创造性的解决方案。 新的 globalThis
特性使得编写需要访问全局 this 值的通用 JavaScript 变得更加容易。 Polyfilling globalThis
正确地比看起来更具挑战性,但有一个可行的解决方案。
只在你真正需要的时候使用这个 polyfill。 JavaScript 模块使得在不改变全局状态的情况下导入和导出功能比以往任何时候都更容易,而且大多数现代 JavaScript 代码无论如何都不需要访问全局 this。 globalThis
仅对有此功能的库和 polyfill 有用。
相关文章
使用 CSS 和 JavaScript 制作文本闪烁
发布时间:2023/04/28 浏览次数:146 分类:CSS
-
本文提供了使用 CSS、JavaScript 和 jQuery 使文本闪烁的详细说明。
在 PHP 变量中存储 Div Id 并将其传递给 JavaScript
发布时间:2023/03/29 浏览次数:69 分类:PHP
-
本文教导将 div id 存储在 PHP 变量中并将其传递给 JavaScript 代码。
在 JavaScript 中从字符串中获取第一个字符
发布时间:2023/03/24 浏览次数:93 分类:JavaScript
-
在本文中,我们将看到如何使用 JavaScript 中的内置方法获取字符串的第一个字符。
在 JavaScript 中获取字符串的最后一个字符
发布时间:2023/03/24 浏览次数:141 分类:JavaScript
-
本教程展示了在 javascript 中获取字符串最后一个字符的方法