polyfilling globalThis

15.09.20205 Min Read — In ECMA Standards

定义

globalThis 提案引入了一种在任何 JavaScript 环境中访问全局变量的机制。听起来似乎是个很简单的 polyfill,但实际上实施起来我们遇到了诸多困难。

根据tc39 提案一个正确的 polyfill 应该具有以下几点要求:

  1. 必须在任何 JavaScript 环境下工作,包括浏览器,浏览器 web worker,浏览器插件,Node.js,Deno,和各种 js 引擎实现
  2. 必须支持粗糙模式,严格模式,以及 js 模块
  3. 在任何执行上下文内都可用(换言之,即使在构建是被包裹在严格模式的方法中依然能保证可用)

术语

首先,请注意,globalThis 提供的是全局作用域中的 this 值,这浏览器中的全局对象是有区别的。原因 JavaScript 模块中,在全局全局作用域和你的代码中还有一层模块作用域,因此,在模块顶层 this 值会是 undefined。

globalThis 的备选项

在浏览器中,globalThis 的值等于 window

globalThis === window
// -> true

在 frames 中也是

globalThis === frames
// -> true

然而, windowframes 在 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

看似上面提到的方法可以被整合到单个方法 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 的返回值)

健壮的 polyfill

在如下的环境中:

  1. 你不能依靠的价值 globalThis,window,self,global,或 this;
  2. 您不能使用 Function 构造函数或 eval;
  3. 但是您可以依靠所有其他 JavaScript 内置功能的完整性 我们还能实现一个正确的 polyfill 吗?

事实证明,有解决方案,但他并不优雅。

我们在不知道如何直接访问 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

原文译自:https://mathiasbynens.be/notes/globalthis

你必须登录才能发表评论