告别 var,拥抱 let/const:一文彻底搞懂 JavaScript 变量与作用域
文章目录
1. 引言
相信很多前端开发者在刚开始接触 JavaScript 时,都遇到过下面这个经典的“坑”:
for(var i =0; i <3; i++){setTimeout(()=> console.log(i),100);}// 期望输出:0, 1, 2// 实际输出:3, 3, 3为什么?因为 var 没有块级作用域,导致循环结束后的 i 变成了 3。如果你不懂变量提升和作用域,这种 Bug 往往让人排查一整天。
ES2015(ES6) 引入了 let 和 const,彻底解决了这些历史遗留问题。同时,函数也得到了极大的增强,比如默认参数、剩余参数以及大名鼎鼎的箭头函数。
本文将带你彻底搞懂:
- 为什么我们要“枪毙”
var? let和const到底有什么区别?- 什么是 TDZ(暂时性死区)?
- 箭头函数的
this到底指向谁?
2. 正文
2.1 var 的“三宗罪”
在 ES6 之前,声明变量只能用 var。它有三个著名的特性,经常让开发者踩坑:
- 没有块级作用域:
var声明的变量,只有函数作用域和全局作用域,没有块级作用域(即{}内部)。这就是开篇那个循环 Bug 的根源。
允许重复声明:
你可以无意中多次声明同一个变量,导致旧变量被覆盖,且不报错。
var a =1;var a =2;// 居然不报错!变量提升:
你可以在声明之前使用变量,虽然它的值是 undefined,但这会引发逻辑混乱。
console.log(myName);// undefined,不会报错!var myName ="Jack";// 实际上 JS 把它变成了:// var myName;// console.log(myName);// myName = "Jack";2.2 let 与 const 的正确打开方式
为了修复 var 的问题,ES6 引入了两个新关键字。
1. 块级作用域let 和 const 声明的变量只在当前的 {} 块中有效。这完美解决了循环问题:
for(let i =0; i <3; i++){setTimeout(()=> console.log(i),100);}// 输出:0, 1, 2 ✅

2. 不存在变量提升
你必须遵循“先声明,后使用”的原则。
console.log(age);// 报错:ReferenceError: Cannot access 'age' before initializationlet age =18;
3. const 的特性
const声明的是一个常量,一旦声明,必须立即初始化,且值不可重新赋值。- 注意:
const保证的是变量指向的内存地址不变。- 对于基本类型(数字、字符串),值就是内容,所以确实改不了。
- 对于引用类型(对象、数组),虽然地址不能变,但内容可以改!
constPI=3.14;// PI = 3.15; // 报错!TypeError: Assignment to constant variable.const user ={name:"Alice"}; user.name ="Bob";// ✅ 合法!对象内容修改了,但内存地址没变// user = {}; // ❌ 报错!试图改变内存地址最佳实践:
默认使用const,当你发现后续需要修改这个变量时,再改回let。尽量不要使用var。
2.3 深入理解 TDZ(暂时性死区)
这是一个比较“硬核”但很重要的概念。
只要块级作用域内存在 let 或 const 命令,它所声明的变量就“绑定”这个区域,不再受外部影响。从块级开始到变量声明语句这行代码之间的区域,被称为暂时性死区。
var tmp =123;// 全局变量if(true){// TDZ 开始// tmp = 'abc'; // 报错!ReferenceError// 这里虽然全局有 tmp,但在 let 声明前,不能访问 console.log(tmp);// 报错!let tmp;// TDZ 结束,tmp 绑定到此作用域 console.log(tmp);// undefined tmp =456; console.log(tmp);// 456}
TDZ 的存在让代码行为更加可预测,强制我们养成良好的变量声明习惯。
2.4 函数的全面升级
ES6 对函数做了大量增强,让写函数变得极其丝滑。
1. 参数默认值
再也不用写 var x = x || 1 这种 hack 语法了。
functionlogUser(name ="Guest", age =0){ console.log(`Name: ${name}, Age: ${age}`);}logUser();// Name: Guest, Age: 0logUser("Alice");// Name: Alice, Age: 02. 剩余参数
当你不确定传进来多少个参数时,用 ...args 把它们收成一个真正的数组。
// ES5 写法:arguments 是伪数组,很难用// ES6 写法:functionsum(...nums){return nums.reduce((total, num)=> total + num,0);} console.log(sum(1,2,3));// 6 console.log(sum(1,2,3,4,5));// 153. 箭头函数
这是 ES6 最流行的特性之一,语法极简。
// 普通constadd=function(a, b){return a + b;};// 箭头constadd=(a, b)=> a + b;// 只有一个参数时,括号可以省略constdouble=x=> x *2;箭头函数的一个重要陷阱:没有自己的 this
箭头函数不会创建自己的 this 上下文,它会捕获其所在上下文的 this 值。这让它成为回调函数的最佳选择。
functionPerson(){this.age =0;// 普通 setInterval 里的 this 指向 window,导致 window.age++// 使用箭头函数,这里的 this 继承自 Person 实例setInterval(()=>{this.age++; console.log(this.age);},1000);}const p =newPerson();// 每秒输出 1, 2, 3...注意:不要在对象的方法里使用箭头函数(除非你确定 this 指向外层),也不要用箭头函数作为构造函数(不能 new)。
3. 常见问题 (FAQ)
Q1:const 定义的数组或对象,真的完全改不了吗?
A: 不是。const 锁住的是内存地址。你依然可以修改对象的属性或使用数组方法(如 push、splice)。如果想彻底冻结对象,可以使用 Object.freeze(obj)。
Q2:什么情况下必须用 var?
A: 几乎没有!除非你需要在旧版浏览器(如 IE10 及以下)运行代码,且没有 Babel 转译。在现代开发中,请忘掉 var。
Q3:let 和 const 在全局作用域声明变量,会成为 window 的属性吗?
A: 不会。var 声明的全局变量会自动挂载到 window 上,而 let/const 不会,这避免了全局命名空间的污染。
4. 总结
本文我们完成了从“老旧 JS”到“现代 JS”的第一步跨越:
- 放弃
var:它没有块级作用域,存在变量提升,容易出 Bug。 - 拥抱
let/const:拥有块级作用域,存在 TDZ,更安全。默认用const,需要改值用let。 - 箭头函数:语法简洁,解决了回调函数中
this指向混乱的问题。
掌握这些特性,你的代码健壮性已经超越了 80% 的初学者。
下一篇预告:我们将学习让代码变得更优雅的“语法级”增强——解构赋值、模板字符串与展开运算符。它们能让你的代码量减少 30% 以上!
如果觉得本文对你有帮助,请点赞👍、收藏⭐、关注👀,三连支持一下!
有问题欢迎在评论区留言:你在写代码时习惯用 let 还是 const?