在全局作用域中,使用 var a = 1 会在 window 对象上挂载属性 a,但 let b = 1 却不会。请解释这是为什么?

0 阅读3分钟

在全局作用域中,使用 var a = 1 会在 window 对象上挂载属性 a,但 let b = 1 却不会。请解释这是为什么?

核心答案

这是因为 varlet 在全局作用域中的绑定机制不同:

  • var 在全局作用域中声明的变量会成为全局对象(window)的属性,这是因为 var 声明的变量存储在 Global Environment RecordObject Environment Record 中,该记录的绑定对象就是 window
  • let 在全局作用域中声明的变量存储在 Global Environment RecordDeclarative Environment Record 中,这是一个独立的词法环境,与 window 对象无关。

深入解析

ECMAScript 规范中的 Global Environment Record

根据 ECMAScript 规范,全局环境记录(Global Environment Record)是一个复合结构,由两部分组成:

  1. Object Environment Record:绑定对象为全局对象(浏览器中是 window)。var 声明和 function 声明的标识符存储在这里,因此会自动成为 window 的属性。

  2. Declarative Environment Record:独立的词法环境,不与任何对象绑定。letconstclass 声明的标识符存储在这里,因此不会出现在 window 上。

为什么要这样设计?

  • 历史兼容性var 挂载到 window 是 ES3 时代就有的行为,大量旧代码依赖 window.xxx 来访问全局变量,不能破坏这一行为。
  • 块级作用域的需要let / const 引入了块级作用域,它们需要一种新的存储机制来支持 TDZ(暂时性死区)等特性。如果仍然挂载到 window 上,就无法实现这些语义。
  • 避免全局污染:不挂载到 window 可以减少全局命名空间的污染,也避免了意外覆盖 window 上已有属性的风险。

常见误区

  • 误区一:认为 let 声明的变量"不是全局变量"。实际上它仍然是全局作用域的变量,只是不作为 window 的属性存在。
  • 误区二:认为 let 变量存储在某个"隐藏对象"上。实际上 Declarative Environment Record 是引擎内部的数据结构,不对应任何 JS 可访问的对象。
  • 误区三:认为 this === window 时可以通过 this.b 访问 let b。不行,let 声明的变量与 this(全局对象)完全无关。

代码示例

// var 声明 —— 挂载到 window
var a = 1;
console.log(window.a);       // 1
console.log('a' in window);  // true

// let 声明 —— 不挂载到 window
let b = 2;
console.log(window.b);       // undefined
console.log('b' in window);  // false

// 但 b 确实是全局变量,可以直接访问
console.log(b);              // 2

// function 声明也会挂载到 window(和 var 行为一致)
function foo() {}
console.log(window.foo);     // ƒ foo() {}

// const 和 let 行为一致
const c = 3;
console.log(window.c);       // undefined

// 直接赋值(不声明)会挂载到 window
// 这是因为它被视为对全局对象的属性赋值
d = 4; // 非严格模式下
console.log(window.d);       // 4
// 一个实际的坑:覆盖 window 已有属性
var name = 123;
console.log(window.name);   // "123"  (注意:window.name 会强制转为字符串)
console.log(typeof name);   // "string"

let name2 = 123;
console.log(window.name2);  // undefined
console.log(typeof name2);  // "number" —— 不受 window.name 的 setter 影响

面试技巧

面试官可能的追问方向:

  1. "那 let 声明的全局变量存在哪里?" → 回答 Global Environment Record 中的 Declarative Environment Record,是引擎内部结构,无法通过 JS 代码直接访问。

  2. "TDZ(暂时性死区)和这个有关系吗?" → 有关系。正是因为 let/const 使用 Declarative Environment Record,引擎可以精确追踪变量是否已初始化,从而实现 TDZ。

  3. "在 Node.js 中 var 会挂载到 global 吗?" → 不会。Node.js 中每个文件都被包裹在一个模块函数中,var 声明的变量属于该函数作用域,不是真正的全局作用域。

  4. "ES Module 中的 var 呢?" → ES Module 有自己的模块作用域,var 不会挂载到 window

一句话总结

var 挂载到 window 是因为它绑定在 Global Environment Record 的 Object Environment Record(绑定对象为 window)中,而 let/const 绑定在独立的 Declarative Environment Record 中,与 window 无关。