【前端三剑客-3/Lesson7(2025-10-24)】JavaScript 变量声明详解:从 `var` 到 `let` 与 `const` 的全面解析📜

61 阅读6分钟

📜写在前面:JavaScript 作为一门动态、弱类型的脚本语言,其变量声明机制经历了从 ES5(ECMAScript 5)到 ES6(ECMAScript 2015)的重大演进。本文深入剖析 varletconst 三者的区别、行为特征、作用域规则、提升机制、暂时性死区(Temporal Dead Zone, TDZ)等核心概念,并辅以代码示例和原理讲解,力求做到“详细就完了”!


💡 一、ES5 时代的变量声明:var 的“坏味道”

1.1 var 声明的基本语法

var age = 18;

这是 ES5 中唯一的变量声明方式。它具有以下特点:

  • 函数作用域(Function Scope):不是块级作用域。
  • 变量提升(Hoisting):在编译阶段,所有 var 声明会被“提升”到当前作用域顶部。
  • 可重复声明:同一个作用域内可以多次用 var 声明同名变量。
  • 隐式全局变量:在非严格模式下,未声明直接赋值的变量会成为全局对象的属性。

1.2 变量提升(Hoisting)详解

来看你的 2.js 中的例子:

console.log(age); // undefined
var age = 18;

这段代码在 JavaScript 引擎中实际被处理为:

var age;           // 编译阶段:声明被提升
console.log(age);  // 执行阶段:此时 age 是 undefined
age = 18;          // 赋值发生在执行阶段

关键点

  • var声明被提升,但赋值不会提升
  • 这导致在声明前访问变量不会报错,而是返回 undefined
  • 这种行为违反直觉,容易引发 bug,尤其在大型项目中难以追踪。

🚫 正如 readme.md 所说:“var 申明变量 bad 和直觉不符合”,且“变量提升不利于代码的可读性,应该废弃的糟粕”。

1.3 var 不支持块级作用域

4.js 的例子:

{
  var age = 18;   // 在块中声明
  let height = 188;
}
console.log(age);    // ✅ 输出 18
console.log(height); // ❌ ReferenceError: height is not defined
  • var age 虽然写在 {} 块中,但由于 var 没有块级作用域,它实际上属于外层作用域(这里是全局)
  • 因此 console.log(age) 能正常访问。
  • let height 具有块级作用域,块外无法访问。

🔍 结论var 的作用域是函数级或全局级,不是块级。这在 iffor{} 等结构中极易造成变量污染。


🆕 二、ES6 革命:letconst 的登场

ES6(2015)引入了 letconst,解决了 var 的诸多问题。

2.1 let:块级作用域 + 暂时性死区

✅ 特性总结:

  • 块级作用域(Block Scope){} 内部形成独立作用域。
  • 不可重复声明:同一作用域内不能重复 let 同名变量。
  • 无变量提升(表现为有 TDZ):在声明前访问会抛出 ReferenceError
  • 存在暂时性死区(Temporal Dead Zone, TDZ)

🧪 示例(来自 2.js):

console.log(PI);      // ❌ ReferenceError: Cannot access 'PI' before initialization
let height = 188;
const PI = 3.1415926;

⚠️ 注意:虽然 let/const 在编译阶段也会被“识别”,但它们在初始化前处于 TDZ,任何访问都会报错。

🔬 什么是暂时性死区(TDZ)?

TDZ 是指从块开始到变量声明语句之间的区域,在此区域内访问该变量会抛出 ReferenceError

{
  console.log(x); // ❌ TDZ:Cannot access 'x' before initialization
  let x = 10;
}

📚 原理:JS 引擎在进入块作用域时,会为 let/const 变量预留内存位置,但标记为“未初始化”。只有执行到声明语句时才完成初始化。在此之前访问 = 报错。

这有效防止了“先使用后声明”的逻辑错误,提升了代码健壮性。


2.2 const:不可重新赋值的常量

✅ 特性总结:

  • 所有 let 的特性(块级作用域、TDZ、不可重复声明)
  • 必须初始化:声明时必须赋值
  • 不能重新赋值const a = 1; a = 2;TypeError
  • 但对象/数组内容可变(引用不变,内容可改)

🧪 示例(来自 3.js):

const PI = 3.1415926;
const person = { name: "ysw", age: 28 };

// person = 'hahaha'; // ❌ TypeError: Assignment to constant variable.

person.age = 21; // ✅ 允许!修改对象属性
console.log(person); // { name: "ysw", age: 21 }

💡 关键理解

  • const 保证的是绑定(binding)不可变,即变量名永远指向同一个内存地址。
  • 对于基本类型(number, string, boolean),值存储在栈中,地址即值 → 值不可变。
  • 对于引用类型(object, array),地址指向堆中的对象 → 地址不变,但对象内部可变。

🔒 如何真正“冻结”对象?

若希望对象内容也不可变,需使用 Object.freeze()

const wes = Object.freeze(person);
wes.age = 17; // 在严格模式下静默失败,非严格模式也无效
console.log(wes); // age 仍是 21(或原值)

⚠️ 注意:Object.freeze()浅冻结,嵌套对象仍可变。如需深度冻结,需递归实现。


🔄 三、函数提升 vs 变量提升(来自 5.js

3.1 函数是一等公民(First-Class Citizen)

JavaScript 中函数可以:

  • 赋值给变量
  • 作为参数传递
  • 作为返回值

3.2 函数声明提升 vs var 提升

5.js 的代码:

setWidth(); // ✅ 可以调用!

function setWidth() {
  var width = 100;
  console.log(width);
}

这里发生了函数提升(Function Hoisting)

  • 函数声明function foo() {})会被完整提升(包括函数体)。
  • var 只提升声明,不提升赋值

对比:

foo(); // ✅ 正常执行
function foo() { console.log('I am hoisted!'); }

bar(); // ❌ TypeError: bar is not a function
var bar = function() { console.log('Not hoisted as function'); };

📌 结论

  • 函数声明提升 > var 提升
  • 函数表达式(赋值给变量的函数)遵循 var/let 的提升规则

🧩 四、作用域体系全景图

声明方式作用域类型是否提升TDZ可重复声明可重新赋值
var函数/全局✅ 声明提升
let块级❌(表现为 TDZ)
const块级❌(表现为 TDZ)

🎯 最佳实践

  • 永远不要使用 var(除非维护老旧代码)
  • 默认使用 const
  • 当需要重新赋值时,改用 let
  • 避免在块外访问块内变量

🧠 五、错误类型详解(来自 readme.md

你提到的几个典型错误:

1. ReferenceError: height is not defined

  • 原因:访问了未声明的变量(或块级作用域外访问 let/const
  • 示例:4.jsconsole.log(height) 在块外调用

2. TypeError: Assignment to constant variable.

  • 原因:试图对 const 变量重新赋值
  • 示例:1.jskey = 'abc234'keyconst

3. ReferenceError: Cannot access 'PI' before initialization

  • 原因:在 TDZ 中访问 let/const 变量
  • 示例:2.jsconsole.log(PI)const PI = ... 之前

📚 六、延伸思考:为什么 JS 会有“编译阶段”?

虽然 JS 是解释型语言,但它在执行前会经历一个快速的编译过程(由 V8、SpiderMonkey 等引擎完成):

  1. 词法分析(Lexing):将代码转为 token 流
  2. 语法分析(Parsing):构建 AST(抽象语法树)
  3. 编译(Compilation)
    • 识别所有变量、函数声明
    • 确定作用域链
    • 处理 var 提升、函数提升
    • 标记 let/const 的 TDZ 区域
  4. 执行(Execution):逐行运行代码

🌟 正因为有这个“编译阶段”,才有了“提升”和“TDZ”等行为。


✅ 七、总结:现代 JavaScript 变量声明指南

场景推荐写法
声明后不再改变的值const
循环计数器、状态变量等let
兼容 IE 或老旧环境(不推荐)var

🚀 记住

  • const 优先:大多数变量其实不需要重新赋值。
  • 块级作用域是现代 JS 的基石:避免变量泄漏。
  • TDZ 是保护机制:防止“先用后声明”的逻辑错误。
  • 函数声明提升很强大:但也要注意可读性。

📖 推荐阅读

  • 《你不知道的 JavaScript(上卷)》第 1–2 章(你已提及)
  • MDN Web Docs: let, const
  • LeetCode Top 100 Liked(巩固算法同时练习现代 JS 写法)

🎉 结语:从 var 的混乱到 let/const 的清晰,JavaScript 正在变得越来越像一门“严肃”的编程语言。掌握这些细节,不仅能写出更安全的代码,还能在面试中脱颖而出!继续加油,攻克吧!💪🔥