现代JavaScript在顶层作用域的变量声明上提供了多种选择,不同声明方式在作用域、提升行为以及和全局对象的关系上都有明显差异。过去我们习惯在脚本里直接用var定义变量,但现在随着ES6模块和块级作用域的普及,let与const已经成为主流写法。理清它们之间的区别,是写出可维护代码的基础。

var在顶层声明中的历史角色与隐患
在早期的JavaScript设计中,使用var在脚本顶层声明变量是最自然的方式。无论是在浏览器直接引入的普通脚本文件,还是在HTML内联的script标签中,var声明的变量不仅会进入全局词法环境,还会作为属性被挂载到全局对象上。在浏览器里,这个全局对象就是window;在Node.js的非模块脚本中,则对应global。这意味着你写下的每一个顶层var变量,实际上都成了全局对象的成员,可以被任意位置的代码通过window.xxx访问或修改。
这种机制在小型页面或快速原型中看似方便,却埋下了不少隐患。最典型的问题是命名冲突:不同脚本文件如果都用var声明了同名变量,后加载的会直接覆盖前面的,且不会抛出任何错误。此外,var存在变量提升,声明会被移到作用域顶部,但赋值保留在原地,这常常让初学者在调试时看到undefined而非报错。下面的代码展示了var变量如何成为window的属性:
<script> var appName = 'demo'; console.log(window.appName); // 输出 demo var count = 10; window.count = 20; console.log(count); // 输出 20,被全局属性修改影响 </script>
正因为var的宽松特性,在现代化工程中它逐渐被替代。不过在维护老项目或编写需要兼容极低版本运行时的代码时,理解var的全局挂载行为仍然必要,否则你可能会误以为某个变量是局部安全的,实则已被外部脚本篡改。
let与const带来的块级作用域与全局绑定变化
ES2015引入的let和const彻底改变了顶层声明的规则。在普通脚本(非模块)的顶层使用let或const,变量依然处于全局作用域,但它们不会成为全局对象的属性。也就是说,在浏览器中你无法通过window来访问用let声明的变量,这些绑定只存在于一个独立的全局词法环境中。这一设计避免了无意间污染全局对象,也减少了不同库之间的命名碰撞。
let和const之间唯一的区别是可变性:let允许重新赋值,const要求在声明时初始化且之后不能更改绑定。两者都具有块级作用域特性,在顶层虽然不存在外层块,但如果在函数或花括号内使用,则仅在该块中有效。下面的示例说明let声明不会挂载到window,且重复声明会报错:
<script>
let userName = 'alice';
const maxRetry = 3;
console.log(window.userName); // 输出 undefined
// let userName = 'bob'; // 同一作用域重复声明会抛出 SyntaxError
if (true) {
let temp = 1;
console.log(temp); // 1
}
// console.log(temp); // 报错,temp不在外层作用域
</script>
从工程角度看,优先使用const声明那些不需要重新绑定的值,只在确实要修改变量时使用let,几乎可以避免所有var带来的提升与覆盖问题。同时因为let和const存在暂时性死区,在声明前访问会直接报错,这比var的undefined更能及早暴露代码顺序错误。
模块环境下的顶层声明隔离机制
当代码以ES模块(type="module"或.mjs文件)方式加载时,顶层声明的隔离性进一步加强。在模块顶层使用var、let或const,变量都只属于该模块的作用域,既不会成为全局对象属性,也不会与其他模块共享同名绑定。模块之间必须通过显式的import和export来交换数据,这种设计让顶层变量声明变得非常安全,不必担心脚本加载顺序导致的全局污染。
对比普通脚本,模块顶层即使使用var也不会挂到window上,这是模块规范刻意做的沙箱处理。下面展示模块中声明的变量无法被外部脚本直接读取:
// module.js 以 type=module 加载
var innerVar = 100;
let innerLet = 200;
const innerConst = 300;
export const getValues = () => ({ innerVar, innerLet, innerConst });
// 在普通脚本中
// console.log(window.innerVar); // undefined
// 必须 import 才能使用
在实际项目中,如果旧系统是通过多个普通脚本拼接全局变量来通信,迁移到模块后就需要重构为导出接口。这也解释了为什么很多老代码直接复制进模块会失效:原来依赖全局挂载的隐式共享不见了。理解这套隔离机制,有助于你在重构时规划清晰的模块边界,把顶层状态收敛到单一数据源,而不是散落在全局各处。
如何选择适合的顶层声明方式
面对三种声明方式,选择原则其实很直接。新写的非模块脚本若必须兼容旧浏览器且依赖全局挂载,可有限使用var,但应集中管理并加命名前缀。绝大多数场景下,普通脚本顶层应用let和const来避免意外全局属性。如果是模块开发,则统一使用const和let,并依靠export控制暴露面,完全不需要考虑var。
在团队规范中,可以强制开启lint规则禁止顶层var,同时要求const优先。这样既能利用块级作用域减少bug,也能让代码读者一眼看出哪些值是固定配置、哪些会变化。下面的表格总结了主要区别:
| 声明方式 | 作用域 | 提升行为 | 普通脚本全局对象 | 模块中可见性 |
|---|---|---|---|---|
| var | 函数或全局 | 提升且初始化为undefined | 挂载为属性 | 仅模块内 |
| let | 块级 | 提升但暂时性死区 | 不挂载 | 仅模块内 |
| const | 块级 | 提升但暂时性死区 | 不挂载 | 仅模块内 |
综合来看,现代JavaScript的顶层变量声明不再是单纯的个人偏好问题,而是关系到作用域安全与项目可维护性的基础决策。结合构建工具与模块系统,开发者完全可以把全局状态控制在最小范围,从而写出更稳健的应用。
JavaScript顶层变量var_let_const修改时间:2026-08-18 05:14:31