JavaScript 全局作用域快速指南
当你在 JavaScript 文件的顶层声明一个变量时,它实际上存在于哪里?答案取决于你编写的是经典脚本还是 ES 模块——弄错这一点会导致难以调试的细微错误。
本指南解释了 JavaScript 全局作用域在现代开发中的工作原理,为什么 globalThis 是引用全局对象的可靠方式,以及 var、let 和 const 在顶层的不同行为。
核心要点
- 全局作用域行为在经典脚本和 ES 模块之间存在差异——只有经典脚本中的
var会在全局对象上创建属性。 let和const创建全局绑定,但不会附加到全局对象上。- 当需要访问全局对象时,使用
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 在顶层的差异
即使在经典脚本中,let 和 const 的行为也与 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 会在全局对象上创建属性。let 和 const 都会在全局作用域中创建绑定,这些绑定在整个代码中都可访问,但它们不会成为全局对象的属性。
当第三方脚本期望在全局对象上找到你的变量时,或者当你直接检查全局对象进行调试时,这种区别就很重要。
Discover how at OpenReplay.com.
为什么 globalThis 是现代标准
不同的 JavaScript 环境历史上对全局对象使用不同的名称:
- 浏览器使用
window - Node.js 使用
global - Web Workers 使用
self
编写跨环境代码意味着需要检查哪个全局对象存在。globalThis 标准通过提供通用引用解决了这个问题:
// Works everywhere
globalThis.sharedValue = 'accessible across environments'
使用 globalThis 可确保你的代码在浏览器、Node.js、Deno 或 Web Worker 中都能正确运行。这是正确的、平台无关的方法。
全局作用域中的变量遮蔽
一个常见的误解:你不能重新声明已经存在于全局作用域中的 let 或 const 变量。但是,你可以使用词法声明遮蔽全局引入的 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"。
全局作用域的最佳实践
- 优先使用模块 — 它们提供自动作用域隔离和显式的导入/导出
- 使用
globalThis— 当你确实需要全局对象时,这在任何地方都有效 - 避免在顶层使用
var— 使用let或const以防止意外的全局对象污染 - 明确声明全局变量 — 如果某些内容必须是全局的,有意地将其附加到
globalThis上,而不是依赖隐式行为
结论
JavaScript 中的全局作用域并不像”可在任何地方访问的变量”那么简单。经典脚本和 ES 模块对顶层绑定的处理方式不同,并且只有经典脚本中的 var 会创建全局对象属性。使用 globalThis 实现跨环境兼容性,并优先使用模块以获得其内置的封装性。理解这些区别有助于你编写可在不同 JavaScript 环境中运行的可预测代码。
常见问题
可以,但仅当两个脚本都是经典脚本并共享相同的全局作用域时。这些变量可以通过名称跨脚本访问,但它们不是全局对象的属性。如果任一脚本是 ES 模块,则变量将保持该模块私有,除非显式导出。
当需要与你无法控制的脚本共享数据时使用全局对象,例如第三方分析工具或期望全局变量的遗留代码。还可以用于必须普遍可用的 polyfill。对于你控制的代码,优先使用模块导出以获得更好的封装性和显式的依赖跟踪。
globalThis 在所有现代 JavaScript 环境中都受支持。如果必须支持 Internet Explorer 等旧版平台,请使用 polyfill 或依赖打包工具/转译器提供的 polyfill。
这是一个历史性的设计决策。var 从 JavaScript 诞生之初就存在,被设计为创建全局对象属性以实现互操作性。let 和 const 在 ES6 中引入,具有更严格的作用域规则,以避免隐式全局污染的陷阱,同时仍允许全局可访问性。