在普遍的JavaScript中获得恐怖的GlobalThis Polyfill

2021-03-18 10:19:16

GlobalThis提案介绍了在任何JavaScript环境中访问全局的统一机制。这听起来像是一个简单的方法,但事实证明它很难正确。直到Toon用意想不到的创造性的解决方案吹嘘我的思想,我甚至不可能。

这篇文章描述了编写适当的GlobalThis Polyfill的困难。这种聚填充有以下要求:

它必须在任何JavaScript环境中工作,包括Web浏览器,Web浏览器的工人,Web浏览器中的扩展,Node.js,Deno和独立JavaScript Engine二进制文件。

它必须如何运行代码运行的上下文。(即,即使Polyfill在构建时间的包装器中以严格的模式函数包裹,也必须仍然产生正确的结果。)

但首先,术语的注释。 GlobalThis在全球范围内提供了这一点。这与Web浏览器中的全局对象不同,因为具有复杂的原因。

请注意,在JavaScript模块中,全局范围和代码之间存在模块范围。模块范围隐藏了全局范围的这个值,因此您在模块中的顶级看到的此值实际上是未定义的。

TL; Globalthis博士不是“全球对象”;它只是来自全球范围的这一点。感谢Domenic帮助我理解这一重要的细微差别。

但是,窗口和帧在工作者上下文中未定义(例如Web工作者和服务工作者)。幸运的是,在所有浏览器上下文中都有一个更强大的替代品:

但是,既不是Node.js中的窗口,帧也没有自我可用。相反,可以使用全局:

上述(窗口,帧,自我,全局)都不是在独立的JavaScript引擎shell中使用,例如由JSVU安装的eBlane。在那里,您可以访问全局:

此外,邋mope函数始终将它们的此设置为全局方式,因此即使您无法在全局范围内运行代码,您仍然可以访问全局的访问,如邋mode的下面:

但是,顶层此值在JavaScript模块中未定义,这在严格模式功能中未定义,因此此方法在那里不起作用。

一旦您处于严格的模式上下文,只有一种方法可以暂时突破它:函数构造函数,它生成邋compure函数!

在Web浏览器中,使用内容安全策略(CSP)通常不允许使用函数构造函数和eval。例如,网站通常选择这样的策略,但它也在Chrome扩展内实施。不幸的是,这意味着一个适当的聚填充不能依赖函数构造函数或eval。

注意:SetTimout(' globalthis =这个' 0)被规定出于同样的原因。除了常用的CSP之外,还有另外其他两种原因,用于使用聚填充物。首先,它不是ECMAScript的一部分,并且在所有JavaScript环境中不可用。其次,它是异步的,因此即使到处都支持SetTimeOut,也可以在其他代码的聚填充中使用它是痛苦的。

它似乎应该可以将上述技术与单个聚填充结合起来,如下所示:

//一个天真的globalthis垫片。不要使用这个! const getGlobalthis =()=> {if(typeof globalthis!=='未定义')返回GlobalThis; if(typeof!=='未定义的')回归自我; if(typeof window!=='未定义')返回窗口; if(Typeof!=='未定义')返回全球; if(typeof!=='未定义')返回这个;抛出新的错误('无法找到全球`和#39;); }; //注意:使用在全局范围内运行时,使用“var`而不是`const`以确保”GlobalThis` //成为全局变量(而不是//顶级词汇范围中的变量)。 var globalthis = getGlobalthis();

但是,唉,这在非浏览器环境中的严格模式函数或JavaScript模块中不起作用(除了GlobalThis支持之外)。此外,GetGlobal可能会返回一个不正确的结果,因为它依赖于此,它依赖于上下文,并且可以由Bundler / Packer更改。

事实证明还有一个解决方案,但这并不漂亮。让我们这么想一分钟。

我们如何在不知道如何直接访问它的情况下访问全球信息?如果我们可以以某种方式在GlobalThis上安装函数属性,并将其称为GlobalThis上的方法,然后我们可以访问此功能:

我们怎样才能这样做,而无需依赖GlobalThis或任何宿主特定的绑定,这是指的?我们不能只是做以下事项:

foo()现在不再被称为方法,因此它在严格的模式下或如上所述的JavaScript模块中的这一点。严格的模式函数将此设置为未定义。但是,这不是Getter和Setter的情况!

object.defineProperty(GlobalThis,' __魔法__' {get:function(){returning this;},可配置:true //这使得可以“删除”getter以后。}; //注意:使用“var”而不是`const`,以确保在全局范围内运行时,确保“GlobalThis` //成为全局变量(而不是//顶级词汇范围的变量)。 var globalthispolyfulled = __magic__;删除GlobalThis .__ Magic__;

上面的程序在GlobalThis上安装Getter,访问Getter以获得对GlobalTh的引用,然后通过删除Getter来清理。这种技术使我们能够在所有所需情况下访问GlobalThis,但它仍然依赖于在第一行上对全局的引用(它说是GlobalThis)。我们可以避免这个依赖吗?我们如何在不直接访问Globallththis的情况下安装全球可访问的吸气剂?

我们不是在GlobalThis上安装getter,我们将其安装在全局此对象继承的内容 - Object.Prototype:

Object.DefineProperty(Object.Prototype,' __魔术__' {get:function(){returning;},可配置:true //这使得可以在稍后的删除中删除getter。}; //注意:使用`var`而不是`const`,以确保`globalthis` //成为全局变量(而不是//顶级词汇范围中的变量)。 var globalthis = __magic__;删除object.Prototype .__ magic__;

注意:在GlobalThis的提案之前,ECMAScript规范实际上并没有授权从Object.Prototype继承的全局授权它必须是一个对象。 Object.Create(null)创建不会从object.prototype继承的对象。 JavaScript引擎可以使用这样的对象作为全局本,而不违反规范,在这种情况下,上面的代码段仍然无法正常工作(并且确实,Internet Explorer 7做了那样的事情!)。幸运的是,更多现代的JavaScript引擎似乎同意全局这必须具有其原型链中的对象。

为避免在GlobalThis已有的现代环境中突变对象。我们可以如下更改聚填充:

(函数(){if(typeof globalthis ==='对象')return; object.defineproperty(object.prototype,' __魔术__' {get:function(){返回它; },可配置:TRUE //这使得可以“删除”稍后的Getter。}); __Magic __。GlobalThis = __Magic__; // lolwat删除对象。__魔术__;}()); //您的代码可以使用“globalththis”。 console.log(globalthis);

(函数(){if(typeof globalthis ==='对象'')return; object.prototype .__ definegetter __(' __ magic __'函数(){returning;}); __magic__。 globalthis = __magic__; // lolwat删除object.prototype .__ magic__;}()); //您的代码可以使用“globalththis”。 console.log(globalthis);

在那里,你有它:你见过的最恐怖的合成垃圾!它完全违背了不普遍的最佳实践,以修改您自己不拥有的对象。用内置原型破坏通常是一个坏主意,如JavaScript发动机基础所解释的:优化原型。

另一方面,如果有人管理在聚填充代码运行之前,则该聚填充可以破坏的唯一方法是and antumber antuments或object.defineproperty(或object.prototype .__definegetter__)。我无法想到更强大的解决方案。你能?

Polyfill是一个很好的通用JavaScript的一个有趣示例:它是纯粹的JavaScript代码,不依赖于任何特定于主论的内置内部,因此在任何实现ECMAScript的环境中运行。这是第一个地方的聚丙烯的目标之一!让我们确认它实际上是有效的。

以下是使用Classic Sc​​ript Tlumenthis.js和Module TlobalThis.ms(具有相同源代码)来记录GlobalThis的HTML演示页面。此演示可用于验证浏览器中的Polyfill工作。 GlobalThis在V8 V7.1 / Chrome 71,Firefox 65,Safari 12.1和iOS Safari 12.2中受到了全球性的支持。要测试Polyfill的有趣部分,请在较旧的浏览器中打开演示页面。

注意:Polyfill在Internet Explorer 10及以上不起作用。在这些浏览器中,行__Magic __。Globalthis = __Magic__某种方式不会使全球全球可用的全球性,尽管__Magic__是对全球的工作。事实证明__Magic__!==窗口虽然都是[对象窗口],但表明这些浏览器可能会对全局对象和全局的区别混淆。修改聚填充物返回到其中一个替代方案使其在IE 10和IE 9中工作。对于IE 8支持,将呼叫与Object.DefineProperty包装在一起,同样地回到Catch块中。 (这样做也可以避免IE 7问题,而不是从Object.Prototype继承。)尝试具有旧IE支持的演示。

要在Node.js和独立JavaScript引擎二进制文件中测试,请下载相同的JavaScript文件:

#将Polyfill +演示代码下载为模块。卷曲https://mathiasbynens.be/demo/globalthis.mjs> GlobalThis.mjs#创建文件的副本(井,符号链接),用作经典脚本。 ln -s globalthis.mjs globalthis.js

$ node - 实验模块--no-warnings globalthis.mjs在模块[对象全局] $ node globalThis.js在经典脚本[对象全球]中测试聚填充

要在独立的JavaScript引擎shell中测试,请使用JSVU安装任何所需的引擎,然后直接运行脚本。例如,在V8 V7.0(没有GlobalThis Support)和V7.1(GlobalThis支持)中进行测试:

$ jsvu [email protected]#安装`v8-7.0.276`二进制。 $ v8-7.0.276 globalthis.mjs在模块中测试Polyfill [Object Global] $ v8-7.0.276 $ v8-7.0.276在经典脚本[对象global] $ jsvu [email protected]#安装`v8- 7.1.302二进制。 $ v8-7.1.302 globalthis.js在经典脚本中测试Polyfill [Object Global] $ V8-7.1.302 Globalthis.mjs在模块中测试Polyfill [Object Global]

相同的技术允许我们在javascriptcore,spidermonkey,chakra和其他JavaScript引擎中测试,如XS。以下是使用javascriptcore的示例:

$ jsvu#安装`javascriptcore`二进制。 $ javascriptcore globalthis.mjs在模块[对象全局] $ javascriptcore globalthis.js在经典脚本[对象global]中测试聚填充

编写通用的JavaScript可能很棘手,并且经常呼吁创意解决方案。新的GlobalThis功能使得更容易编写需要访问全局此值的通用JavaScript。合并GlobalThis正确地比似乎更具挑战性,但有一个工作解决方案。

当您真的需要时才使用此聚填充物。 JavaScript模块使得在未更改全球状态的情况下进口和导出功能的更容易,并且大多数现代JavaScript代码无论如何都不需要访问全局。 GlobalThis仅适用于图书馆和多算法。

自发布本文以来,以下NPM软件包开始提供利用此技术的GlobalThis Polyfills:

免责声明:我不是作者,也不是维护者,这些包中的任何一个。

你好呀!我是Mathias。我在Google上工作了Chrome DevTools和V8 JavaScript引擎。 HTML,CSS,JavaScript,Unicode,性能和安全性让我兴奋。在Twitter和GitHub上关注我。

聚填充物本身不会让我感到困惑。这是一个合并,可以实现最糟糕的目标。最终用户无需了解使用它的详细信息。

严重,wut?这不是相当于eval('这个')?为什么逗号运营商? 🤔

maxArt:eval(代码)是一个“Direct Eval”并在当前范围中执行代码。 (0,eval)(代码)是一个间接eval,并在全局范围内执行代码。 嗨,非常丰富的帖子,帮助我清除一堆盲点! 我对以下两行有一个问题: 我不确定为什么你必须将__magic__附在__magic__的财产上。 当您在__magic__上执行删除操作时,也不会删除GlobalThis属性? Sichao:聚丙烯的目标是使全球全球化的本地可用。 可以通过将它们添加为全局的属性来创建全局变量。 在您突出显示的代码片段中,__magic__是对全局的引用,或者在大多数浏览器上下文中您知道窗口。 所以这条线: 删除行删除了getter,因为我们只需要它一次以获得对全局的引用。