globalThis 提案引入了一种在任何 JavaScript 环境中访问全局变量的机制。听起来似乎是个很简单的 polyfill,但实际上实施起来我们遇到了诸多困难。
根据tc39 提案一个正确的 polyfill 应该具有以下几点要求:
在任何执行上下文内都可用(换言之,即使在构建是被包裹在严格模式的方法中依然能保证可用)
首先,请注意,globalThis 提供的是全局作用域中的 this 值,这浏览器中的全局对象是有区别的。原因 JavaScript 模块中,在全局全局作用域和你的代码中还有一层模块作用域,因此,在模块顶层 this 值会是 undefined。
在浏览器中,globalThis 的值等于 window
globalThis === window
// -> true
在 frames 中也是
globalThis === frames
// -> true
然而, window
和 frames
在 web worker 上下文中是undefined
, 所幸的是, self 能在任何浏览器上下文中工作,更适合作为一个通用的替代:
globalThis === self
// -> true
但是,上述的所有变量在 Node.js 中均无效,我们需要使用 global:
globalThis === global
// → true
在其他 JavaScript 引擎实现中,(window,frames,self,global)可能都无效,这时候我们就只能使用 this 来获得 globalThis 的值
globalThis === this
// → true
此外,草率模式功能始终将其 this 设置为 global this,因此,即使您无法在全局作用域内运行代码,您仍然可以 this 按如下方式在全局模式下访问 global:
globalThis ===
(function() {
return this
})()
// → true
但是,在 JavaScript 模块和严格模式方法中,顶层 this 始终为 undefined,所以上述方法无效。此时,只有两种方法可以打破这种限制: Function constructor 和 eval 方法
globalThis === Function('return this')()
// → true
globalThis === (0, eval)('this')
// → true
现代浏览器中,Function.constructor 和 eval 往往会因为 CSP(Content Security Policy) 被禁用,所以一个完美的 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 模块中无法工作。除此之外,getGlobalThis 可能返回错误的结果(因为他依赖 this 的返回值)
在如下的环境中:
事实证明,有解决方案,但他并不优雅。
我们在不知道如何直接访问 global this 的情况下获得他的值呢?如果我们能够想方法在 globalThis 对象上注入一个方法,那样我们就能通过这个方法来获得 this 的值:
globalThis.foo = function() {
return this
}
var globalThisPolyfilled = globalThis.foo()
但是如何在不依赖 globalThis 和环境特定变量的情况下注入这个方法呢? 我们不能使用:
function foo() {
return this
}
var globalThisPolyfilled = foo()
foo()不再作为一个方法被调用,所以他的 this 值会指向 undefined,在严格模式和 JavaScript 模块中亦是如此。但是!setter 和 getter 可以绕过这种限制:
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 的引用,然后我们通过 delete 来清除这个 getter。这个 polyfill 已经可以在任何环境中获得正确的 globalThis 值,但是他仍然直接使用了 globalThis 的值(第一行),是否有方法可以在不直接依赖 globalThis 的情况下注入这个方法呢? 答案是肯定的。 我们将方法注入到继承于 globalThis 的对象
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 在其原型链中。
为了避免 Object.prototype 在 globalThis 已有的现代环境中发生变异,我们可以如下更改 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)
至此,我们终于获得了一个能在所有环境下工作的 globalThis polyfill