在前端开发中,变量声明是咱们接触最早、也最容易踩坑的基础概念。从“上古时期”的 var,到 ES6 登场的 let 和 const,这三兄弟性格迥异——选对了能少走80%的弯路,选错了可能让代码变成“薛定谔的bug”。
今天这篇文章,咱们用“唠家常”的方式拆解三兄弟的区别,解锁「变量提升」「暂时性死区」等高频考点,最后附上实战避坑指南,新手也能轻松拿捏!
📌 一、先认人:三兄弟怎么“打招呼”?
变量声明本质是“给数据找个家”,三兄弟的用法各有讲究,直接上代码+大白话备注:
// 1. var:上古老大哥(现在基本被嫌弃)
var pocketMoney = 10; // 可先声明不赋值,后续能改
// 2. let:灵活小哥(值想改就改)
let gameTime = 20; // 声明时可空着,后面再赋值
// 3. const:铁憨憨硬汉(一旦定规矩就不改)
const ID_CARD = "123456789"; // 必须声明时就赋值,后续不能换“指向”
💡 小约定:用 const 时建议变量名全大写(如 ID_CARD),这是行业惯例——别人一看就知道“这玩意儿不能改”,不用猜来猜去~
⚠️ 二、var:被时代抛弃的“坑王”老大哥
var 是 JS 刚诞生时的“独苗”,当年没竞争对手,养成了一堆“反人类”毛病,现在被戏称“bug制造机”,咱们逐个拆坑!
坑1:变量提升——“人还没到,名字先传开了”
var 最让人崩溃的特性是「变量提升」:不管你在代码哪个位置声明 var 变量,JS 会偷偷把它“拎到”当前作用域最顶端,还默认赋个 undefined(相当于“占着茅坑不拉屎”)。
举个直观例子:
console.log(apple); // 输出 undefined,不是报错!
var apple = "红富士";
console.log(apple); // 输出 红富士
你以为代码是“先打印再声明”,但 JS 引擎偷偷改成了这样:
var apple; // 偷偷提升到顶部,赋值 undefined(占位置)
console.log(apple); // 此时 apple 存在但没值,所以是 undefined
apple = "红富士"; // 赋值操作还在原来的位置
console.log(apple); // 有值了,输出红富士
这就像在公司喊“老王”,明明老王还没来,大家却知道“公司有个老王”——这种模糊状态,很容易写出逻辑错误!
坑2:函数作用域——“管得太宽,容易泄露”
var 只有「函数作用域」和「全局作用域」,没有「块级作用域」(块级作用域就是 {} 大括号里的区域,比如 if/for 循环)。简单说:var 变量只要不在函数里,就会“跑遍全场”,容易和其他变量“打架”。
最经典的坑:for 循环用 var 声明计数器
// 想循环3次,每次打印当前索引
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 你以为输出 0、1、2,实际输出 3、3、3!
}, 100);
}
为啥?因为 var 声明的 i 是「全局作用域」的,循环结束后 i 已经变成 3 了。setTimeout 是“延迟执行”,等它跑的时候,只能拿到最终的 i=3。
这就像给3个朋友发消息说“等我5分钟”,结果你忘了,5分钟后才回复——3个朋友收到的都是“迟到的回复”,不是各自的“专属时间”。
✅ 三、let 和 const:现代 JS 的“靠谱兄弟”
为了收拾 var 的烂摊子,ES6 推出 let 和 const 两位“新贵”。它们的核心优点是:支持「块级作用域」,还有严格的“使用规则”,逼你写出更严谨的代码。
先搞懂:什么是块级作用域?
块级作用域就是 {} 圈出来的“独立空间”,变量只在这个空间里有效,出了门就“不认人”。比如:
- if 语句的
{}(if (true) { ... }) - for 循环的
{}(for (...) { ... }) - 自己写的独立
{}({ let a = 1; })
对比 var 和 let 的作用域:
// var 版:变量泄露到全局
if (true) {
var fruit = "香蕉";
}
console.log(fruit); // 输出 香蕉(变量跑出来了)
// let 版:变量被限制在块内
if (true) {
let vegetable = "白菜";
}
console.log(vegetable); // 报错:ReferenceError: vegetable is not defined(出块就找不到)
这就像:var 是“大门敞开的房子”,谁都能进;let/const 是“带密码锁的房间”,只有房间里的人能用到。
let:灵活变通的“打工人”
let 就像靠谱的打工人——能变通,但不越界:
- 支持块级作用域,不会泄露变量;
- 没有
var的“变量提升”坑; - 可重复赋值,但不能重复声明。
用 let 修复 for 循环bug:
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 正确输出 0、1、2!
}, 100);
}
为啥管用?因为 let 声明的 i 是「块级作用域」的,每次循环都会创建一个新的 i。setTimeout 拿到的是“当前循环的 i”,不是全局的 i——相当于给每个朋友发了“专属时间”,不串味~
再看重复声明的坑:
let age = 18;
let age = 20; // 报错:SyntaxError: Identifier 'age' has already been declared
这就像“一个人不能有两个身份证号”,避免你不小心重复声明,导致值被覆盖。
const:坚守原则的“硬汉”
const 翻译是“常量”,核心原则:「声明时必须赋值,赋值后不能改指向」——注意!是“不能改指向”,不是“不能改内容”,这是高频坑!
情况1:基本类型(数字/字符串/布尔值)——真·不能改
基本类型的值“直接存在变量里”,const 声明后,值完全不能动:
const PI = 3.14159;
PI = 3.14; // 报错:TypeError: Assignment to constant variable.(想改硬汉原则?没门!)
这就像给硬汉起了名,他就认这个名,改了就不是他本人了。
情况2:复合类型(对象/数组)——能改“内部内容”,不能换“外壳”
对象/数组的值“存在内存里”,变量存的是“指向内存的地址”(相当于“门牌号”)。const 只保证“门牌号不变”,不限制“房子里的东西怎么摆”。
举个对象例子:
// 声明对象(门牌号指向“小明家”)
const xiaoming = { name: "小明", age: 18 };
// 改内部内容:允许(换家具)
xiaoming.age = 19;
xiaoming.hobby = "打游戏";
console.log(xiaoming); // 输出:{ name: "小明", age: 19, hobby: "打游戏" }(没问题)
// 换外壳:不允许(换门牌号,指向“小红家”)
xiaoming = { name: "小红", age: 20 }; // 报错:TypeError: Assignment to constant variable.
再看数组例子:
const fruitList = ["苹果", "香蕉"];
// 改内部内容:允许(加/删水果)
fruitList.push("橙子");
fruitList[0] = "西瓜";
console.log(fruitList); // 输出:["西瓜", "香蕉", "橙子"](没问题)
// 换外壳:不允许(重新赋值数组)
fruitList = ["葡萄", "芒果"]; // 报错!
记住:const 锁的是“门牌号”,不是“房子里的东西”~
❌ 四、错误类型大揭秘:代码在“吐槽”什么?
遇到报错别慌,JS 其实在“提醒你违规了”!3种常见错误,一看就懂:
1. ReferenceError: xxx is not defined(“你喊了个不存在的人!”)
含义:用了完全没声明的变量——没⽤ var/let/const,JS 根本不认识它。
例子:
console.log(height); // 报错:ReferenceError: height is not defined
// 相当于在大街上喊“张三”,但根本没这个人
避坑:用变量前先问自己:“我有没有用 var/let/const 声明过它?”
2. TypeError: Assignment to constant variable.(“你想改硬汉的原则!”)
含义:试图给 const 变量“换指向”——不管是基本类型还是复合类型,重新赋值就报错。
例子:
const gender = "男";
gender = "女"; // 报错(基本类型换值)
const dog = { name: "旺财" };
dog = { name: "来福" }; // 报错(复合类型换指向)
避坑:用 const 前想:“这个变量以后会不会换门牌号?” 不会就用,会就用 let。
3. ReferenceError: Cannot access 'xxx' before initialization(“禁区没开放,不能进!”)
含义:在 let/const 声明+初始化前就用它——触及了「暂时性死区」。
例子:
console.log(phone); // 报错:Cannot access 'phone' before initialization
let phone = "iPhone";
避坑:记住“先声明,后使用”——let/const 必须在声明语句之后用,别提前调用。
🔍 五、核心概念:暂时性死区(TDZ)——let/const 的“禁区”
理解 let/const 的关键,用“景区开放”比喻:
「暂时性死区」是指:从代码块开始(比如 { 后面),到 let/const 声明+初始化的这段区域,像“景区没开放”,提前进去会被“保安拦住”(报错)。
死区的范围:不只是“声明前”
很多人以为死区只是“声明前一行”,其实块级作用域里,声明前的所有位置都是死区:
{
// 死区开始(针对变量「watch」)
console.log(watch); // 报错!
let watch = "劳力士"; // 死区结束(声明+初始化)
console.log(watch); // 正常输出:劳力士
}
再看隐蔽的死区:
function buyThings(quantity) {
console.log(quantity); // 正常输出(参数已声明)
let quantity = 10; // 报错:Cannot access 'quantity' before initialization
}
buyThings(5);
为啥报错?因为函数参数 quantity 和 let 声明的 quantity 重名,从函数开始到 let 声明前,quantity 处于死区——相当于“景区里有小禁区,景区开放了小禁区也没开”。
与 var 的本质区别
| 场景 | var | let/const |
|---|---|---|
| 声明前访问 | 输出 undefined | 报错(禁区不能进) |
| 逻辑意义 | 允许“提前占位” | 强制“先声明后使用” |
简单说:var 是“先占位置再说”,let/const 是“没准备好就别碰”——这也是现代开发推荐用 let/const 的原因。
📝 六、三兄弟终极对比+最佳实践
1. 核心特性对比表(大白话版)
| 特性 | var(坑王) | let(灵活小哥) | const(硬汉) |
|---|---|---|---|
| 作用域 | 函数/全局(管得宽) | 块级(守规矩) | 块级(守规矩) |
| 变量提升 | 有(默认 undefined) | 无(有死区) | 无(有死区) |
| 重复声明 | 可以(易覆盖) | 不可以(报错) | 不可以(报错) |
| 声明时是否赋值 | 不需要(var a; 可行) | 不需要(let a; 可行) | 必须(const a=1; 才行) |
| 能否重新赋值 | 可以(a=1→a=2) | 可以(a=1→a=2) | 不可以(换指向不行) |
2. 现代 JS 开发最佳实践(直接照做!)
👉 优先用 const
- 适用场景:变量“不换门牌号”时,比如:
- 配置项(
const BASE_URL = "https://api.xxx.com") - 固定值(
const PI = 3.14159) - 对象/数组(只要不换指向)
- 配置项(
- 好处:代码意图清晰,避免不小心重新赋值。
👉 需要换值时用 let
- 适用场景:变量需“更新值”或“换门牌号”,比如:
- 循环计数器(
for (let i = 0; i < 10; i++)) - 计数器(
let count = 0; count++) - 条件赋值(
let status; if (ok) status = "success")
- 循环计数器(
👉 坚决不用 var
- 理由:变量提升+函数作用域太容易踩坑,现代开发没有任何场景是
var能做到而let/const做不到的——用var纯属给自己找罪受。
🎯 最后:一句话记住三兄弟
var:上古坑王,别用!let:灵活可变,需换值时用!const:原则至上,不换指向时优先用!
变量声明是 JS 基础,也是避坑关键——现在你已经搞懂三兄弟的脾气,以后写代码再也不会被“变量提升”“死区”搞晕,还能写出更靠谱、易读的代码~
你有没有踩过 var 的坑?或者对 let/const 有其他疑问?欢迎在评论区交流~ 觉得有用的话,别忘了点赞+收藏+关注,后续还会分享更多前端避坑干货!