前言
JavaScript 的变量声明经历了从 var 到 let/const 的演进,这背后是 ES6 对语言设计的重大修正。本文结合代码示例,系统梳理三者差异、作用域、变量提升等核心概念,适合复习和面试准备。
一、JavaScript 与 ES6 的背景
先说点历史。JavaScript 和 Java 没有任何关系,纯粹是当年蹭热度起的名。JS 是 Brendan Eich 用一周时间赶工出来的浏览器副产品,最初只用来给网页加点交互(表单验证、幻灯片之类)。因为是 KPI 项目,设计上难免有瑕疵——var 就是其中之一。
ES6(ECMAScript 2015)是 JS 的一次断档式升级,标志着 JS 向企业级大型项目开发迈进。let 和 const 的引入,就是用来修复 var 的历史遗留问题。
二、var 的问题:没有块级作用域
// 1.js
var age = 100;
if (age > 12) {
var dog = age * 7; // var 声明,无视 {} 块
let x = 111; // let 声明,被限制在 {} 内
console.log(dog); // 700
}
console.log(dog); // 700 —— var 变量泄露到块外了!
console.log(x); // ReferenceError: x is not defined
var 只有全局作用域和函数作用域,{} 代码块对它形同虚设。let/const 则支持块级作用域,{} 内的变量外面拿不到。
// 2.js
{
const name = '张三';
}
console.log(name); // ReferenceError —— name 被锁在块内
三、经典面试题:for + setTimeout
这是考察作用域理解的高频题:
// 用 var —— 翻车版
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(`This number is ${i}`);
}, 1000);
}
// 输出:10 个 "This number is 10"
为什么? var 不支持块级作用域,整个循环共享同一个 i。同步代码先跑完,i 变成 10,1 秒后所有 setTimeout 回调拿到的都是同一个 i。
// 用 let —— 正确版
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(`This number is ${i}`);
}, 1000);
}
// 输出:0 1 2 3 4 5 6 7 8 9
为什么? let 支持块级作用域,每次迭代都会创建一个独立的块级作用域,保存当时的 i 值。10 次迭代 = 10 个独立的 i。
一句话:
var是"大家共用一个",let是"各用各的"。
四、const:不可变变量constant variable(常量)
// 3.js
const key = 'abc123';
key = 'newKey'; // ❌ Assignment to constant variable
// const 声明时必须赋值
const a; // ❌ Missing initializer
let b; // ✅ let 可以先声明后赋值
基本类型 vs 引用类型
const 的"不可变"针对的是内存地址:
// 基本类型:值直接存栈,不能改
const point = 50;
point = 1; // ❌ 报错
// 引用类型:变量存的是堆地址,地址不变就行,内容可以改
const person = {
name: '野原新之助',
age: 5
};
person.age++; // ✅ 可以修改属性
person = { name: '风间' }; // ❌ 不能更换整个对象(地址变了)
person = '111'; // ❌ 不能改变数据类型
记忆技巧:
- 基本数据类型 → 值和类型都不能改
- 引用数据类型 → 可以改内容,不能换类型/换对象
五、作用域与变量查找规则
JS 有三种作用域:
| 作用域 | 说明 | 对应声明 |
|---|---|---|
| 全局作用域 | 代码最外层 | 所有 |
| 函数作用域 | function 内部 | var |
| 块级作用域 | {} 内部 | let、const |
变量查找规则:作用域链
当前作用域 → 外层作用域 → ... → 全局作用域 → 报错
从内向外冒泡查找,找到就停,全局都找不到就 ReferenceError。
垃圾回收:函数/代码块执行完后,内部变量会被销毁、内存被回收。这就是变量的生命周期。
六、变量提升(Hoisting)与暂时性死区
// 4.js
console.log(pizza); // ❌ ReferenceError: Cannot access 'pizza' before initialization
let pizza = 'Deep Dish';
JS 执行分两阶段:
| 阶段 | 做什么 |
|---|---|
| 编译阶段 | 检测语法,准备执行上下文,变量声明被"提起" |
| 执行阶段 | 逐行执行代码 |
var 的提升:
console.log(x); // undefined(不是报错!因为 var 被提升了,默认值是 undefined)
var x = 10;
let/const 虽然有提升,但进入暂时性死区(TDZ),在声明之前访问会报错:
console.log(x); // ReferenceError
let x = 10;
let/const不是为了消除提升,而是让该报错的时候报错——这比var的undefined更符合直觉,更容易排查 bug。
七、一表总结
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 全局 / 函数 | 块级 {} | 块级 {} |
| 重复声明 | ✅ | ❌ | ❌ |
| 变量提升 | ✅ 初始化为 undefined | ✅ 但 TDZ 报错 | ✅ 但 TDZ 报错 |
| 声明时赋值 | 可选 | 可选 | 必须 |
| 可重新赋值 | ✅ | ✅ | ❌ |
| 修改属性(对象) | ✅ | ✅ | ✅ |
八、最佳实践
- 默认用
const,只有确定要重新赋值才用let - 彻底告别
var,ES6+ 代码里没有它存在的理由 - 声明放在作用域顶部,避免 TDZ 报错
for循环用let,远离var + setTimeout的坑
本文基于个人学习笔记整理,参考示例代码见ai_doubao_zzh: 走向AGI,走向豆包。