大数跨境

var、let、const:为什么 JavaScript 需要三种声明变量的方式

var、let、const:为什么 JavaScript 需要三种声明变量的方式 索引目录
2026-01-12
0
导读:关注「索引目录」公众号,获取更多干货。如果你是从其他语言转而学习 JavaScript,你可能会感到困惑。

关注「索引目录」公众号,获取更多干货。

如果你是从其他语言转而学习 JavaScript,你可能会感到困惑。为什么 JavaScript 有三个不同的关键字来声明变量?一个关键字难道不够吗?

答案并非仅仅是“var 已经过时,请改用 let/const”。这些关键字与 JavaScript 执行模型的交互方式存在根本差异——具体来说,就是引擎创建和管理执行上下文和词法环境的方式不同。

让我们深入探讨一下,当你声明一个变量时,究竟发生了什么。

JavaScript 运行你的代码时究竟发生了什么

在讨论 `get` varlet`get` 或 ` get` 之前const,我们需要了解 JavaScript 如何准备运行你的代码。

当 JavaScript 进入任何可执行上下文(函数、代码块或全局代码)时,它会经历两个阶段:

  1. 创建阶段
    :设置环境
  2. 执行阶段
    :实际逐行运行您的代码

在创建阶段,JavaScript 会创建一个称为词法环境(Lexical Environment)的东西。可以把它想象成一个数据结构,它由以下部分组成:

  • 环境记录
    :存储变量和函数的地方。
  • 外部引用
    :指向父级词汇环境的链接(用于作用域链)

关键在于:var,,let并且const与这些结构的交互方式完全不同

变量:功能级环境记录

让我们来追踪一下发生了什么var

function checkAge() {
  console.log(age); // undefined
  var age = 25;
  console.log(age); // 25
}

创建阶段(任何代码运行之前):

  1. JavaScript 会为函数环境记录创建一个函数环境记录。checkAge
  2. 它会扫描整个函数体,查找var声明。
  3. 对于每个var,它在环境记录中创建一个属性并将其初始化为undefined
  4. 该赋值语句(= 25尚未执行。

因此,内部环境记录如下所示:

FunctionEnvironmentRecord {
  age: undefined  // created during setup
}

执行阶段:

  1. console.log(age)
    - 摘自《环境记录》→undefined
  2. age = 25
    - 更新环境记录中的现有属性
  3. console.log(age)
    - 摘自《环境记录》→25

这就是var“提升”的原因——并不是声明向上移动,而是变量从函数执行之初就存在于环境记录中。

块作用域问题:为什么 var 会忽略块

function test() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 10
}

以下是其内部运作原理:

创建阶段:

  • JavaScript 会为整个函数创建一个函数环境记录。test
  • 它在方块var x内部。if
  • 它会添加x: undefined到功能环境记录中。
  • if模块不会创建自己的环境记录(对于var

执行阶段:

  • 进入if方块
  • x = 10
    更新x函数环境记录
  • 退出if阻塞(但环境记录保留——它是函数的记录)
  • console.log(x)
    从同一函数环境记录读取 →10

规范中并没有规定在使用代码块时,代码块本身就拥有环境记录var。代码块只是控制流程,它们不会创建新的作用域。

循环之谜:为什么所有回调函数都共享同一个变量

接下来才是真正有趣的部分:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3

让我们一步一步地追踪执行过程:

会发生什么:

  1. 循环初始化会i在函数环境记录中创建(如果没有函数,则在全局环境中创建)。
  2. 第一次迭代:i = 0setTimeout 会安排一个回调函数
  3. 第二次迭代:i = 1setTimeout 会安排另一个回调函数
  4. 第三次迭代:i = 2setTimeout 会安排另一个回调函数
  5. 循环条件检查:i = 3条件不成立,循环退出。
  6. 大约 100 毫秒后,回调函数执行

为什么它们都打印了3:

每个箭头函数都会创建一个闭包。闭包是一个函数加上对创建该函数的词法环境的引用。这三个箭头函数都对同一个词法环境(包含循环的词法环境)进行闭包。

当回调函数执行时,它们会i从共享的环境记录中读取数据。此时,循环已经i结束3

视觉上:

Outer Lexical Environment:
  EnvironmentRecord: { i: 3 }  // Final value after loop completes

Callback 1: () => console.log(i)  ──┐
Callback 2: () => console.log(i)  ──┼─→ All reference the same environment
Callback 3: () => console.log(i)  ──┘

内存中只有一个 i变量,所有回调函数共享该变量。

let 和 const:块级环境记录

现在,JavaScript 的行为从根本上发生了变化:

{
  let x = 10;
}
console.log(x); // ReferenceError

会发生什么:

let当 JavaScript 遇到带有 ` <br>` 或 `<br>` 的代码块时const,它会创建一个新的词法环境,并拥有自己的环境记录(称为代码块的声明式环境记录)。

创建阶段(进入区块):

  1. 为该代码块创建一个新的词法环境
  2. 查找该代码块中的所有let/const声明
  3. 在环境记录中创建未初始化的绑定
  4. 将外部引用设置为父环境

执行阶段:

  1. 当代码执行到此处时let x = 10;,绑定将被初始化。
  2. 退出代码块后,词法环境将被丢弃(如果没有闭包引用它)。

关键区别在于:该变量从一开始就存在于环境记录中(已注册),但在初始化之前访问它会引发错误。

时间死区:不仅仅是一条规则,而是一种状态

console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;

事情的真相是这样的:

当 JavaScript 进入作用域时,它会x在环境记录中创建一个绑定,但该绑定处于一种特殊状态:未初始化。这与undefined.

在内部,环境记录可能如下所示:

DeclarativeEnvironmentRecord {
  x: <uninitialized>  // Special internal state, not undefined
}

如果你尝试读取数据x,JavaScript 会检查:“此绑定是否已初始化?” 如果没有,它会抛出一个 ReferenceError 异常。这就是 TDZ——绑定创建到初始化之间的时间差。

当执行到 时let x = 5;,绑定从 转换<uninitialized>5

它存在的意义是什么?

设想以下情景:

let x = x + 1; // ReferenceError

如果没有 TDZ,x右侧会是什么?undefined这会悄无声息地造成错误(undefined + 1 = NaN)。TDZ 会强制报错,使错误显而易见。

循环修复:每次迭代创建新环境

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2

这里规范做了一件非常巧妙的事情。let在循环中for,JavaScript 会为每次迭代创建一个新的词法环境

以下是实际流程:

迭代 0:

  1. 创建一个新的词汇环境:{ i: 0 }
  2. 在此环境中执行循环体
  3. setTimeout
    回调函数会关闭此环境

迭代 1:

  1. 创建一个新的词汇环境:{ i: 1 }
  2. 在此环境中执行循环体
  3. setTimeout
    回调函数会在这个环境中关闭。

迭代 2:

  1. 创建一个新的词汇环境:{ i: 2 }
  2. 在此环境中执行循环体
  3. setTimeout
    回调函数会在这个环境中关闭。

视觉上:

Environment 0: { i: 0 } ← Callback 1 references this
Environment 1: { i: 1 } ← Callback 2 references this
Environment 2: { i: 2 } ← Callback 3 references this

每个回调函数都有自己的i环境变量,因为每次迭代都会创建一个独立的词法环境。这在 ECMAScript §13.7.4.8 中有明确规定——该规范明确指出要创建一个新的环境并将循环变量复制到其中。

这与以下情况不同

let i = 0;
// Create environment 0
i = 1;
// Create environment 1
i = 2;
// Create environment 2

那只是对同一个变量进行重新赋值。规范明确地创建新的绑定。

const:环境记录中的不可变绑定

const x = 10;
x = 20; // TypeError: Assignment to constant variable

引擎层面发生了什么?

当你声明时const x = 10,JavaScript 会在环境记录中创建一个绑定,但将其标记为不可变。在内部,这可能表示为:

DeclarativeEnvironmentRecord {
  x: { 
    value: 10, 
    mutable: false  // Internal flag
  }
}

当你尝试这样做时x = 20,JavaScript 会检查该mutable标志。如果该标志为 false false,则会抛出一个 TypeError 异常。

重要提示:这里指的是绑定,而不是值:

const obj = { count: 1 };
obj.count = 2; // OK
obj = {}; // TypeError

绑定obj是不可变的(你不能让它指向其他对象),但它指向的对象仍然是可变的。环境记录只控制绑定,而不控制堆分配的对象。

奇异的互动:阴影与重新宣告

let x = 1;
{
  var x = 2; // SyntaxError: Identifier 'x' has already been declared
}

为什么在解析时(执行前)会失败?

当 JavaScript 解析这段代码时,它会看到:

  1. let x
    - 将在外部环境中形成结合
  2. var x
    - 尝试在……等等,在哪儿创建绑定?

var由于是函数作用域的,它会尝试在与外部函数相同的x环境记录中创建。但规范规定同一作用域内不能有重复的词法声明。解析错误。let x

但这样做可行:

var x = 1;
{
  let x = 2; // OK
  console.log(x); // 2
}
console.log(x); // 1

因为在代码块的let x环境记录中创建了一个绑定,这与函数/全局环境记录所在的环境记录不同。var x

环境链如下所示:

Block Environment:
  Record: { x: 2 }
  Outer → Function/Global Environment:
            Record: { x: 1 }

x在代码块内部访问时,JavaScript 首先会在最内层环境中查找(查找 `this` 2)。在代码块外部,它会在函数/全局环境中查找(查找 `this` 1)。

性能如何?

你可能会想:如果let每次循环迭代都创建新的环境,那岂不是会更慢吗?

现代 JavaScript 引擎(V8、SpiderMonkey、JavaScriptCore)对此进行了深度优化。如果循环变量没有被闭包捕获,引擎实际上不会创建单独的环境——它足够智能,能够识别出该变量未被共享。

额外的环境只有在语义上必要的时候才会存在(当闭包捕获它们时)。

摘要:关键在于词汇环境

var`<script>`、let`<script>` 和` <script>` 之间的真正区别const不在于语法,而在于它们如何与 JavaScript 的词法环境系统交互:

  • var
    :在函数环境记录中创建绑定,立即初始化undefined,函数作用域
  • let
    :在块环境记录中创建绑定,从 TDZ 开始,块级作用域,每次循环迭代一个环境
  • const
    :与 相同let,但绑定被标记为不可变。

理解这一点有助于预测行为和调试问题。当您遇到奇怪的作用域错误时,可以追踪环境的创建过程,并准确查看变量所在的位置。

下次当你编写let循环代码时,你就会知道 JavaScript 在幕后创建了单独的环境——以及为什么这对闭包很重要。


关注「索引目录」公众号,获取更多干货。


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读12
粉丝0
内容444