【译】一种令人震惊的 globalThis Polyfill 通用实现
一种令人震惊的 globalThis Polyfill 通用实现。
1 译者序
本文翻译自Mathias的博客文章-A horrifying globalThis polyfill in universal JavaScript,文章翻译建立在我理解的基础之上,如有疏漏,欢迎各位评论区指正。
关于作者:作者名叫Mathias Bynens,Google 工程师,Chrome 浏览器 DevTools 以及 JavaScript V8 引擎开发者。擅长领域包括:JavaScript, HTML, CSS, HTTP, performance, security, Bash, Unicode, macOS。Mathias 同时痴迷于 Web 标准的制定,他也是TC39委员之一,本篇博客便是他对自己参与贡献的提案
globalThis
的总结和思考,该提案目前位于Stage 4。
正文如下:
globalThis
提案引入了一种在任何 JavaScript 环境下访问全局this
的机制。听起来似乎实现向前兼容很简单(译者注:原文为It sounds like a simple thing to polyfill,polyfill简单来说就是在早期浏览器上使用一些 hack 实现新特性,本篇译文后面的向前兼容如非特指都是指代polyfill),但实际上会比较困难。直到Toon给了我一种出乎意料的、创造性的解决方案之前,我甚至认为这不可能实现。
这篇文章描述了实现globalThis
向前兼容的困难程度,这样的向前兼容至少需要如下前置条件:
- 它必须在所有的 JavaScript 环境中都能正常工作,例如浏览器、web workers、浏览器中的插件、Node.js、Deno以及一些其他的独立 JavaScript 引擎程序。
- 它必须支持常规模式、严格模式以及 JavaScript 模块。
- 它必须在任何的上下文中都能正常工作(例如,尽管该polyfill是被打包软件在构建过程插入一段严格模式的函数中(注意:这里及下文提到的 严格模式的函数中 既包括在函数中使用严格模式,也包括使用了全局严格模式下的函数),它依然能输出正确的结果)。
2 一些术语
在这之前,我们先解释一些术语。globalThis
相当于全局作用域的this
。这和浏览器中的全局对象并不一样,且原因比较复杂。
注意在 JavaScript 模块中,在你的代码和全局作用域之间存在一个模块作用域(module scope),模块作用域中隐藏了全局作用域中的this
值,所以在模块作用域最上层中,this
等于undefined
。
长话短说,globalThis
并非“全局对象(global object)”,简而言之,它就是全局作用域中的this
。感谢Domenic帮助我理解了这一重要却细微的差别。
3 globalThis
的备选方案
在浏览器中,globalThis
就等于window
:
|
|
也等于frames
:
|
|
然而,在 workers 中(例如 web workers 或者 service workers),window
和frames
均为undefined
。不过幸运的是,self
在所有浏览器环境中都有效,因此是一个更可靠的选择:
|
|
译者注:上述原作者提到的 web worker 和 service worker 都属于 web 环境,MDN将 service worker 叫做 web worker 的一类,不过值得注意的是,在MDN 的另一篇使用说明中 web worker 并不包含 service worker。
在 Node.js 环境中,window
、frames
以及self
都不存在,不过我们可以使用global
:
|
|
上述所有变量(window
、frames
、self
以及global
)在独立的 JavaScript 引擎程序中都无法使用(你可以通过jsvu
来安装并使用这些引擎)。不过在这些引擎程序里,你可以使用this
:
|
|
由于正常模式函数中的this
就是全局的this
,所以哪怕你无法在全局作用域下运行代码,在正常模式下,仍然可以通过以下方法获取全局this
:
|
|
不过,如果使用 JavaScript 模块,模块顶层的this
等于undefined
,而且在严格模式的函数中,this
也等于undefined
。所以这些情况下这种方法也不管用。
如果位于严格模式的环境下,只有一种办法可以临时打破这一约束:使用Function
构造器函数:
|
|
应该说有两种方法,”间接的“eval
调用有同样的效果:
|
|
注:
eval(code)
是一种”直接调用“,eval
中代码的执行作用域为当前作用域。而(0, eval)(code)
是一种”间接调用“,执行作用域始终为全局作用域。
在浏览器中,如果开启了内容安全策略(Content Security Policy (CSP)),则不允许使用Function
构造函数以及eval
函数。网站通常会采用这个策略,而且Chrome 浏览器扩展中会强制执行。这意味着一个正确的 polyfill 实现不能依靠Function
或eval
。
注:
setTimeout('globalThis = this', 0)
也无法在 CSP 中使用。除了这个原因,不使用这种方式还出于以下两个考量。首先,这种方法并不是ECMAScript 规范,无法在所有 JavaScript 环境中生效。其次,它是异步执行的,在一个被作为依赖的 polyfill 中这样实现会非常糟糕。
4 一种想当然的 polyfill 实现方式
似乎将上述方法合到一起,便能实现我们想要的 polyfill,例如:
|
|
可惜,上面的实现在严格模式下的函数中,以及在非浏览器环境的 JavaScript 模块中都无法生效(除非原生支持globalThis
)。之外,getGlobal
也可能返回一个错误的结果,因为他依赖于this
,this
除了与上下文环境有关,它也可以被一些打包工具更改。
5 一种可靠的 polyfill 实现方式
假设有如下一个执行环境,有可能实现一个可靠的globalThis
polyfill 吗?
- 你不能依赖于
globalThis
、window
、self
、global
或者this
- 你不能使用
Function
或eval
- 但是你可以使用或整合 JavaScript 提供的内置功能
最终证明确实存在一种方法,但是看起来并没有那么优美。让我们先停下来思考几分钟。
最初,我们不知道如何直接访问全局this
时,该如何获取它呢?如果我们能以某种方式在globalThis
上添加一个函数属性,并且让它作为globalThis
的方法执行,我们就能从函数返回值拿到这个全局this
。
如果不依赖于globalThis
以及引用它的一些特定环境下的绑定,我们如何实现上述功能呢?(译者注:原文为How can we do something like that without relying on globalThis or any host-specific binding that refers to it?)。如果只是像下面这样实现是不够的:
|
|
foo()
现在不再作为一个对象的方法属性来调用,所以正如上面提到过的,在严格模式或者 JavaScript 模块中,this
为undefined
。严格模式下函数中的this
被设置成了undefined
。但是,在getters
和setters
中却并非如此!
|
|
上面的代码在globalThis
上添加了一个 getter,通过访问这个 getter 来获取globalThis
的引用,最后删除 getter 进行复原。使用这个技巧可以在所有期待的环境下正常访问globalThis
,但是可以看到在第一行,仍然依赖了全局的this
(代码中为globalThis
)。能否避免这种依赖呢?在不直接访问globalThis
的情况下,如何添加一个全局可访问的 getter?
除了在globalThis
上添加 getter,我们还可以在全局this
继承的对象,也就是Object.prototype
上进行添加:
|
|
注:在
globalThis
提案之前,ECMAScript 规范实际上并没有规定全局的this
必须要继承自Object.prototype
,只说了其必须是一个对象。Object.create(null)
创建的对象就没有继承自Object.prototype
,JavaScript 引擎即使使用它作为全局this
,也没有违背规范。如果这样的话上面的代码还是会失败(事实上,IE7 就是这么干的!)。幸运的是,更多现代的 JavaScript 引擎似乎都同意Object.prototype
必须在全局this
的原型链上。
在现代的 JavaScript 环境中,如果已经支持globalThis
,为了避免对Object.prototype
的修改,可以对上述代码作如下修改:
|
|
或者,也可以使用__defineGetter__
:
|
|
成功了,你见到了至今为止最震惊的 polyfill 实现!这完全违背了最佳实践:不要修改不是你创建的对象。污染内置对象的原型对象一般来说都是很糟糕的做法,我在这篇文章里做了详细解释。
从另一面来说,这个 polyfill 失败的唯一原因就是有人在 polyfill 代码运行之前,设法改变了Object
或者Object.defineProperty
(或者Object.prototype.__defineGetter__
)。我想不出更可靠的解决方案了,你呢?
6 测试这段 polyfill
这个 polyfill 是一个 很好的有趣的 通用 JavaScript 的实现案例:它是纯 JavaScript 的实现,而且不依赖于任何宿主环境特定的内置变量等等,因此在任何实现 ECMAScript 的环境中都能正常运行。这也是这个 polyfill 实现的首要目标!让我们来做一些测试进行验证。
这里有一个使用了该 polyfill 的 HTML 示例页面。页面中使用传统脚本globalthis.js
和模块脚本globalthis.mjs
打印了globalThis
(两者源代码相同)。这个示例可以用来验证该 polyfill 在浏览器中正常运行。在V8 v7.1 / Chrome 71、Firefox 65、Safari 12.1、iOS Safari 12.2 中均原生支持globalThis
。如果想要测试 polyfill 最有趣的部分,可以在一些老浏览器中打开这个示例页面。
注:该 polyfill 不支持 IE10 以及之前版本的浏览器,出于一些原因,尽管
__magic__
作为全局this
的引用,__magic__.globalThis = __magic__
却并不能让globalThis
变为全局可访问的变量。虽然__magic__
和window
都是[object Window]
,但是__magic__ !== window
,意味着这些浏览器在实现时也可能受困于全局对象和全局this
的区别。为了使其在 IE10 和 IE9 中也正常工作,可以增加上述提到的一些备选方案。为了支持 IE8,需要在try
-catch
中调用Object.defineProperty
,如果失败则进入catch
语句块进行操作(这种方法同样可以解决 IE7 中全局this
并不继承自Object.prototype
的问题)。点击此处查看这个支持老 IE 的示例。
为了在 Node.js 以及其他独立的 JavaScript 引擎程序中测试,下载示例中的 JavaScript 文件:
|
|
现在我们可以在node
里测试了:
|
|
为了在独立的 JavaScript 引擎程序中测试,可以使用jsvu
来安装你想要的引擎,然后直接运行脚本即可。例如:在 V8 v7.0 版本(该版本不原生支持globalThis
)和 v7.1 版本(该版本原生支持globalThis
)中分别进行测试:
|
|
类似地,你还可以在 JavaScriptCore、SpiderMonkey、Chakra 甚至是一些小众的例如XS
等等 JavaScript 引擎中测试,下面的例子中使用了JavaScriptCore
:
|
|
7 总结
写一个通用的 JavaScript 方案(译者注:这里的通用方案指的就是能在任何 JavaScript 环境下执行的代码方案)可能会很棘手,而且经常需要一些创造性的解决思路。如果你的方案需要获取全局this
的值,那么新的globalThis
特性会让你实现起来更加容易。正确实现globalThis
的向前兼容比想象中要困难许多,但是至少这里给出了一种解决方案。
请记得除非你真正需要,否则无需使用该 polyfill。JavaScript 模块可以让你更简单地导入和导出所需功能,而且无需修改全局状态,其实大部分的现代 JavaScript 代码都无需访问全局this
。globalThis
只对于一些需要它的库(libraries)和 polyfill 方案有用。
8 npm 上已有的一些globalThis
polyfill 实现
自从写了这篇文章以后,以下的几个 npm 包已经开始使用上面提到的一些技巧提供globalThis
polyfill 的实现: