12k
All articles

JavaScript 全局作用域快速指南

JavaScript 全局作用域在经典脚本与 ES 模块中的行为存在差异,介绍 var、let、const 以及 globalThis 与全局对象的交互方式。

OpenReplay Team
OpenReplay Team
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 环境中运行的可预测代码。

常见问题

我可以从同一页面上的其他脚本访问 let 和 const 变量吗?

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

何时应该有意使用全局对象而不是模块导出?

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

globalThis 在旧版浏览器中有效吗?

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

为什么 var 在顶层的行为与 let 和 const 不同?

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

We use cookies to improve your experience. By using our site, you accept cookies.