先打个比方:声明变量就像往抽屉里放东西并贴个标签。var 是那种没规矩的旧抽屉——标签能随便重贴、东西能从抽屉缝里漏到外面;let 是"可换标签的箱子"——东西能换,但标签不能重名、箱子外碰不到里面;const 是"贴了封条的箱子"——贴好就不许再动。记住一句话:平时默认用 const,需要改值才用 let,var 基本别再用了。
| 对比项 | var(旧抽屉) | let(可换标签箱子) | const(贴封条箱子) |
|---|---|---|---|
| 作用域 | 函数作用域(只认函数,不认花括号) | 块级作用域(认花括号 {}) | 块级作用域(认花括号 {}) |
| 重复声明 | 可以(后写的把先写的盖掉) | 不可以(直接报错) | 不可以(直接报错) |
| 变量提升 | 有(先能用、值是 undefined) | 没有(用之前必须先声明,报错) | 没有(用之前必须先声明,报错) |
| 声明必须赋值 | 不用(不写就是 undefined) | 不用 | 必须赋初始值 |
| 值能否修改 | 能 | 能 | 不能再指向别的东西(对象内部属性例外) |
一句话抓重点:var 只认函数这堵墙,let/const 连一对花括号 {} 都能当墙。如果你在 for 循环里用 var 声明 i,循环结束后这个 i 会"漏"到外面变成同一个;换成 let,每次循环都给你一个新 i——这是面试最爱考的坑。
var 就是那个"旧抽屉":你贴了一张写着 a 的纸条,嫌不够再贴一张,它也不拦你,只是后一张把前一张盖住。
var a = 1; // 第一次贴标签 a,里面放 1 var a = 2; // 又贴一次 a!var 不报错,直接把里面换成 2 a = 3; // 改值,当然也行 // 坑:大项目里手滑把同名变量又写了一遍,原数据被悄悄盖掉,查 bug 查到崩溃
let 是"可换标签的箱子":箱子里的东西可以拿出来换新的(改值),但你不能再拿来一张写着同样名字的标签贴上去(重复声明会报错)。
let b = 1; // 声明 b,放 1 b = 2; // 允许!把里面换成 2(改值) // let b = 3; // ★ 报错:重复声明(SyntaxError)—— 同一张标签不许贴两次
let 的脾气:改值随便来,重名没门。日常"会变的量"就用它——比如计数器、循环里的临时结果。
const 是"贴了封条的箱子":你贴封条的时候必须把东西放进去(不能空着贴),贴好之后整个箱子不许再换、不许再打开。
const c = 1; // 贴封条,同时放好 1
// c = 2; // ★ 报错:不能改(TypeError)——封条动不了
// const d; // ★ 报错:必须赋初始值 —— 不许空箱子贴封条
const user = { name: "大喇叭" }; // const 锁的是这个变量指向哪个对象
user.name = "小王"; // 不报错!const 锁的是"这个箱子不许换",箱子里的东西随便改
最容易踩的坑:以为 const 写的对象就完全不能改。如果你写 const user = {...} 然后去改 user.name,程序不会拦你——因为 const 封的是"这个变量不能再指向别的箱子",箱子里面那层抽屉你还是能拉出来换东西的。真要连内部一起锁死,才需要用 Object.freeze。
"作用域"就是"这个名字在哪些地方能被找到"。var 不认花括号这道墙,let/const 认。
// var:花括号拦不住,东西会漏到外面
{ // 一对花括号,var 不认这道墙
var x = 1; // var 声明,塞进花括号里
} // 花括号结束
console.log(x); // 1 —— 漏出来了!外面照样能用
// let / const:花括号就是一堵墙,墙外面摸不到
{ // 一对花括号,let 认这道墙
let y = 1; // 塞进花括号里
} // 出了花括号 y 就消失
// console.log(y); // ★ 报错:y is not defined —— 墙外面找不到
想象你循环 3 次,想给 3 个按钮各绑定一个"打印自己序号"的函数。用 var 时,3 个按钮其实共用同一个 i,等按钮真被点的时候循环早就跑完了,i 已经是 3。
// 用 var:3 个按钮被点时全都打印 3(i 是同一个变量)
for (var i = 0; i < 3; i++) { // 用 var 声明 i,3 轮共用一个
btn.onclick = function () { console.log(i); }; // 绑点击事件
}
// 用 let:分别打印 0、1、2(每一轮循环都新复制了一个 i)
for (let i = 0; i < 3; i++) { // 循环 3 次,每轮新 i
btn.onclick = function () { console.log(i); }; // 各自记住当轮的 i
}
如果你在循环里用 var 绑定事件,点哪个按钮都打印最后那个值,因为 3 个回调闭包共享同一个 i。换成 let,每轮循环 JS 都会悄悄复制一份新的 i 给当前回调,点谁就是谁——这就是"闭包陷阱"的标准解法。
① 可能在什么地方用:固定配置/常量(const 颜色值、接口地址)、循环里的索引(let i)、计数器/状态变量(let count);新项目里基本不再用 var。
② 常见的问题:for 循环里用 var 绑事件,点哪个都打最后一个值;以为 const 对象完全不能改;声明 const 忘了赋初值直接报错。
③ 解决思路:循环绑事件一律 let i;const 对象内部属性照改,要锁死用 Object.freeze;const 必须声明时赋值;新代码默认 const,要改值再换 let。