Back

JavaScript 全局作用域快速指南

JavaScript 全局作用域快速指南

当你在 JavaScript 文件的顶层声明一个变量时,它实际上存在于哪里?答案取决于你编写的是经典脚本还是 ES 模块——弄错这一点会导致难以调试的细微错误。

本指南解释了 JavaScript 全局作用域在现代开发中的工作原理,为什么 globalThis 是引用全局对象的可靠方式,以及 varletconst 在顶层的不同行为。

核心要点

  • 全局作用域行为在经典脚本和 ES 模块之间存在差异——只有经典脚本中的 var 会在全局对象上创建属性。
  • letconst 创建全局绑定,但不会附加到全局对象上。
  • 当需要访问全局对象时,使用 globalThis 以实现跨环境兼容性。
  • 优先使用 ES 模块以实现自动作用域隔离并避免意外的命名空间污染。

什么是 JavaScript 全局作用域?

全局作用域是指 JavaScript 环境中最外层的作用域。全局作用域中的变量可以从代码的任何地方访问——函数内部、代码块内部或嵌套模块内部。

但这里有一个大多数教程都忽略的关键区别:JavaScript 中的顶层作用域行为取决于脚本的加载方式。

经典脚本 vs ES 模块

在经典脚本中(不使用 type="module" 加载),使用 var 声明的顶层变量会成为全局对象的属性:

// classic script
var config = { debug: true }
console.log(globalThis.config) // { debug: true }

而在 ES 模块中,情况完全不同。模块和全局作用域的工作方式在设计上就不同。模块中的顶层绑定保留在该模块的作用域内——它们不会附加到全局对象上:

// module script (type="module")
var config = { debug: true }
console.log(globalThis.config) // undefined

这种隔离是有意为之的。模块提供封装性,防止跨文件的意外命名空间污染。有关更深入的参考,请参阅 MDN 上的 JavaScript 模块指南。

var、let 和 const 在顶层的差异

即使在经典脚本中,letconst 的行为也与 var 不同:

// classic script
var a = 1
let b = 2
const c = 3

console.log(globalThis.a) // 1
console.log(globalThis.b) // undefined
console.log(globalThis.c) // undefined

只有 var 会在全局对象上创建属性。letconst 都会在全局作用域中创建绑定,这些绑定在整个代码中都可访问,但它们不会成为全局对象的属性。

当第三方脚本期望在全局对象上找到你的变量时,或者当你直接检查全局对象进行调试时,这种区别就很重要。

为什么 globalThis 是现代标准

不同的 JavaScript 环境历史上对全局对象使用不同的名称:

  • 浏览器使用 window
  • Node.js 使用 global
  • Web Workers 使用 self

编写跨环境代码意味着需要检查哪个全局对象存在。globalThis 标准通过提供通用引用解决了这个问题:

// Works everywhere
globalThis.sharedValue = 'accessible across environments'

使用 globalThis 可确保你的代码在浏览器、Node.js、Deno 或 Web Worker 中都能正确运行。这是正确的、平台无关的方法。

全局作用域中的变量遮蔽

一个常见的误解:你不能重新声明已经存在于全局作用域中的 letconst 变量。但是,你可以使用词法声明遮蔽全局引入的 var 绑定——包括在现代引擎中动态引入的 var 绑定(例如,通过 eval):

var x = 1
{
  let x = 2 // shadows the global var
  console.log(x) // 2
}
console.log(x) // 1

内部的 let x 创建了一个独立的变量,在该代码块内临时隐藏了外部的 var x。它们是不同的绑定——修改其中一个不会影响另一个。

仅限模块的特性:顶层 Await

JavaScript 开发者应该了解的顶层作用域的一个实际差异:await 在顶层仅在模块中有效:

// Only valid in modules
const data = await fetch('/api/config').then(r => r.json())

经典脚本不支持此语法。如果需要顶层 await,必须在 script 标签上使用 type="module"

全局作用域的最佳实践

  1. 优先使用模块 — 它们提供自动作用域隔离和显式的导入/导出
  2. 使用 globalThis — 当你确实需要全局对象时,这在任何地方都有效
  3. 避免在顶层使用 var — 使用 letconst 以防止意外的全局对象污染
  4. 明确声明全局变量 — 如果某些内容必须是全局的,有意地将其附加到 globalThis 上,而不是依赖隐式行为

结论

JavaScript 中的全局作用域并不像”可在任何地方访问的变量”那么简单。经典脚本和 ES 模块对顶层绑定的处理方式不同,并且只有经典脚本中的 var 会创建全局对象属性。使用 globalThis 实现跨环境兼容性,并优先使用模块以获得其内置的封装性。理解这些区别有助于你编写可在不同 JavaScript 环境中运行的可预测代码。

常见问题

可以,但仅当两个脚本都是经典脚本并共享相同的全局作用域时。这些变量可以通过名称跨脚本访问,但它们不是全局对象的属性。如果任一脚本是 ES 模块,则变量将保持该模块私有,除非显式导出。

当需要与你无法控制的脚本共享数据时使用全局对象,例如第三方分析工具或期望全局变量的遗留代码。还可以用于必须普遍可用的 polyfill。对于你控制的代码,优先使用模块导出以获得更好的封装性和显式的依赖跟踪。

globalThis 在所有现代 JavaScript 环境中都受支持。如果必须支持 Internet Explorer 等旧版平台,请使用 polyfill 或依赖打包工具/转译器提供的 polyfill。

这是一个历史性的设计决策。var 从 JavaScript 诞生之初就存在,被设计为创建全局对象属性以实现互操作性。let 和 const 在 ES6 中引入,具有更严格的作用域规则,以避免隐式全局污染的陷阱,同时仍允许全局可访问性。