🚀 JavaScript 变量声明终极指南:var、let、const 三兄弟的“恩怨情仇”

267 阅读9分钟

在前端开发中,变量声明是咱们接触最早、也最容易踩坑的基础概念。从“上古时期”的 var,到 ES6 登场的 letconst,这三兄弟性格迥异——选对了能少走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 推出 letconst 两位“新贵”。它们的核心优点是:支持「块级作用域」,还有严格的“使用规则”,逼你写出更严谨的代码。

先搞懂:什么是块级作用域?

块级作用域就是 {} 圈出来的“独立空间”,变量只在这个空间里有效,出了门就“不认人”。比如:

  • if 语句的 {}if (true) { ... }
  • for 循环的 {}for (...) { ... }
  • 自己写的独立 {}{ let a = 1; }

对比 varlet 的作用域:

// 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 就像靠谱的打工人——能变通,但不越界:

  1. 支持块级作用域,不会泄露变量;
  2. 没有 var 的“变量提升”坑;
  3. 可重复赋值,但不能重复声明。

let 修复 for 循环bug:

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 正确输出 0、1、2!
  }, 100);
}

为啥管用?因为 let 声明的 i 是「块级作用域」的,每次循环都会创建一个新的 isetTimeout 拿到的是“当前循环的 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);

为啥报错?因为函数参数 quantitylet 声明的 quantity 重名,从函数开始到 let 声明前,quantity 处于死区——相当于“景区里有小禁区,景区开放了小禁区也没开”。

与 var 的本质区别

场景varlet/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 有其他疑问?欢迎在评论区交流~ 觉得有用的话,别忘了点赞+收藏+关注,后续还会分享更多前端避坑干货!