迹忆客 专注技术分享

当前位置:主页 > 学无止境 > 编程语言 >

通用 JavaScript 中可怕的 globalThis polyfill

作者:迹忆客 最近更新:2023/01/06 浏览次数:

globalThis 提案引入了一种统一的机制,可以在任何 JavaScript 环境中访问全局 thispolyfill 听起来很简单,但事实证明很难做到正确。 我甚至认为这是不可能的,直到图恩用一个意想不到的、创造性的解决方案让我大吃一惊。

这篇文章描述了编写适当的 globalThis polyfill 的困难。 这样的 polyfill 有以下要求:

  • 它必须在任何 JavaScript 环境中工作,包括网络浏览器、网络浏览器中的工作程序、网络浏览器中的扩展、Node.js、Deno 和独立的 JavaScript 引擎二进制文件。
  • 它必须支持草率模式、严格模式和 JavaScript 模块。
  • 无论代码运行在何种上下文中,它都必须工作。(也就是说,即使 polyfill 在构建时由打包程序包装在严格模式函数中,它也必须仍然产生正确的结果。)

术语

但首先,请注意一些术语。 globalThis 在全局范围内提供 this 的值。 由于复杂的原因,这与 Web 浏览器中的全局对象不同。

global this visualization

请注意 ,在 JavaScript 模块中,在全局范围和我们的代码之间有一个模块范围。 模块作用域隐藏了全局作用域的 this 值,所以你在模块顶层看到的 this 值实际上是未定义的。

globalThis 不是“全局对象”; 它只是来自全局范围的 this


globalThis 替代品

在浏览器中,globalThis 等同于 window

globalThis === window;
// → true

frames 也适用:

globalThis === frames;
// → true

但是,windowframes 在上下文(例如网络工作者和服务工作者)中是未定义的。 幸运的是,self 可以在所有浏览器上下文中工作,因此是一个更健壮的替代方案:

globalThis === self;
// → true

不过,windowframesself 在 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 constructoreval。 网站通常会选择加入这样的政策,但它也在 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? 假设一个环境:

  • 我们不能依赖 globalThiswindowselfglobalthis 的值;
  • 我们不能使用 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 设置为未定义。 然而,gettersetter 却不是这样!

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 代码无论如何都不需要访问全局 thisglobalThis 仅对有此功能的库和 polyfill 有用。

转载请发邮件至 1244347461@qq.com 进行申请,经作者同意之后,转载请以链接形式注明出处

本文地址:

相关文章

扫一扫阅读全部技术教程

社交账号
  • https://www.github.com/onmpw
  • qq:1244347461

最新推荐

教程更新

热门标签

扫码一下
查看教程更方便