告别 var,拥抱 let/const:一文彻底搞懂 JavaScript 变量与作用域

告别 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) 引入了 letconst,彻底解决了这些历史遗留问题。同时,函数也得到了极大的增强,比如默认参数、剩余参数以及大名鼎鼎的箭头函数。

本文将带你彻底搞懂:

  1. 为什么我们要“枪毙” var
  2. letconst 到底有什么区别?
  3. 什么是 TDZ(暂时性死区)?
  4. 箭头函数的 this 到底指向谁?

2. 正文

2.1 var 的“三宗罪”

在 ES6 之前,声明变量只能用 var。它有三个著名的特性,经常让开发者踩坑:

  1. 没有块级作用域
    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 letconst 的正确打开方式

为了修复 var 的问题,ES6 引入了两个新关键字。

1. 块级作用域
letconst 声明的变量只在当前的 {} 块中有效。这完美解决了循环问题:

for(let i =0; i <3; i++){setTimeout(()=> console.log(i),100);}// 输出:0, 1, 2 ✅
浏览器控制台运行结果截图:var 的运行结果


浏览器控制台运行结果截图:let 的运行结果

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(暂时性死区)

这是一个比较“硬核”但很重要的概念。

只要块级作用域内存在 letconst 命令,它所声明的变量就“绑定”这个区域,不再受外部影响。从块级开始到变量声明语句这行代码之间的区域,被称为暂时性死区

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: 0

2. 剩余参数
当你不确定传进来多少个参数时,用 ...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));// 15

3. 箭头函数
这是 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 锁住的是内存地址。你依然可以修改对象的属性或使用数组方法(如 pushsplice)。如果想彻底冻结对象,可以使用 Object.freeze(obj)

Q2:什么情况下必须用 var
A:
几乎没有!除非你需要在旧版浏览器(如 IE10 及以下)运行代码,且没有 Babel 转译。在现代开发中,请忘掉 var

Q3:letconst 在全局作用域声明变量,会成为 window 的属性吗?
A:
不会。var 声明的全局变量会自动挂载到 window 上,而 let/const 不会,这避免了全局命名空间的污染。


4. 总结

本文我们完成了从“老旧 JS”到“现代 JS”的第一步跨越:

  1. 放弃 var:它没有块级作用域,存在变量提升,容易出 Bug。
  2. 拥抱 let/const:拥有块级作用域,存在 TDZ,更安全。默认用 const,需要改值用 let
  3. 箭头函数:语法简洁,解决了回调函数中 this 指向混乱的问题。

掌握这些特性,你的代码健壮性已经超越了 80% 的初学者。

下一篇预告:我们将学习让代码变得更优雅的“语法级”增强——解构赋值、模板字符串与展开运算符。它们能让你的代码量减少 30% 以上!


如果觉得本文对你有帮助,请点赞👍、收藏⭐、关注👀,三连支持一下!

有问题欢迎在评论区留言:你在写代码时习惯用 let 还是 const?

Read more

Flutter 组件 shelf_router 的适配 鸿蒙Harmony 实战 - 驾驭官方标准路由器架构、实现鸿蒙端 HTTP 流量精密分发与逻辑路由审计方案

Flutter 组件 shelf_router 的适配 鸿蒙Harmony 实战 - 驾驭官方标准路由器架构、实现鸿蒙端 HTTP 流量精密分发与逻辑路由审计方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 shelf_router 的适配 鸿蒙Harmony 实战 - 驾驭官方标准路由器架构、实现鸿蒙端 HTTP 流量精密分发与逻辑路由审计方案 前言 在鸿蒙(OpenHarmony)生态的分布式业务中继、政务级内嵌 API 管理平台以及需要承载大规模高频交互请求的各类全栈式应用开发中,“路由的精确支配与逻辑安全性”是决定系统架构稳健性的命门所在。面对包含上百个 RESTful 端点的复杂服务模型、需要动态解析包含 UUID、日期等多种格式的 URL 参数,或者是需要针对鸿蒙手机与智慧大屏执行差异化的路由匹配。如果仅仅依靠原始的字符串拆分或低性能的手写拦截逻辑。不仅会导致路由解析执行效率的低下,更会因为缺乏一套工业级的“官方契约”规范。引发鸿蒙端微服务接口在面对异常报文时的逻辑脆弱性风险。 我们需要一种“官方背书、匹配闭环”的路由艺术。 shelf_router 是一套由 Dart 官方团队维护的、

By Ne0inhk
【Spring】Spring事务和事务传播机制

【Spring】Spring事务和事务传播机制

🎬 那我掉的头发算什么:个人主页 🔥 个人专栏: 《javaSE》《数据结构》《数据库》《javaEE》 ⛺️待到苦尽甘来日 文章目录 * 事务三连 * 什么是事务 * 为什么要有事务 * 事务的操作 * Spring中事务的实现 * 准备工作 * Spring编程事务 * Spring 声明式事务 @Transactional * @Transactional详解 * rollbackFor * 事务隔离级别 * Mysql事务隔离级别 * Spring事务隔离级别 * Spring事务传播机制 * 总结 事务三连 什么是事务 事务是⼀组操作的集合, 是⼀个不可分割的操作. 事务会把所有的操作作为⼀个整体, ⼀起向数据库提交或者是撤销操作请求. 所以这组操作要么同时成功, 要么同时失败. 为什么要有事务 我们在进行程序开发时,也会有事务的需求。 比如转账操作: 第一步:A 账户 -100 元。 第二步:B 账户 +100

By Ne0inhk
掌控消息全链路(4)——RabbitMQ/Spring-AMQP高级特性详解之事务与消息分发

掌控消息全链路(4)——RabbitMQ/Spring-AMQP高级特性详解之事务与消息分发

🔥我的主页:九转苍翎⭐️个人专栏:《Java SE》《Java集合框架系统精讲》《MySQL高手之路:从基础到高阶》《计算机网络》《Java工程师核心能力体系构建》《RabbitMQ理论与实践》天行健,君子以自强不息。 1.事务 AMQP(高级消息队列协议)实现了事务机制,主要用于确保消息的原子性发布和确认。换言之,它允许你将多个操作(如发送消息、确认消息)绑定在一起,要么全部成功,要么全部失败 发送消息 @RestController@RequestMapping("/producer")publicclassProducerController{@Resource(name ="transRabbitTemplate")privateRabbitTemplate transRabbitTemplate;@Transactional@RequestMapping("/trans")publicStringtrans(){ transRabbitTemplate.convertAndSend(""

By Ne0inhk
C语言程序调试常用方法与技巧

C语言程序调试常用方法与技巧

C语言程序调试常用方法与技巧 一、学习目标与重点 学习目标 * 理解程序调试的基本概念 * 掌握常用调试工具的基本使用方法 * 学会使用调试技巧定位程序中的错误 * 提高程序调试的效率和准确率 学习重点 * 调试工具的安装与配置 * 断点设置与单步调试 * 变量值查看与内存分析 * 错误定位与修复技巧 二、程序调试的基本概念 2.1 调试的定义与意义 调试是指在程序运行过程中,通过观察和分析程序的行为,定位并修复错误的过程。程序调试的主要目的是提高程序的正确性和可靠性,确保程序能够按照预期的方式运行。 2.2 调试的主要步骤 1. 发现问题:通过测试或用户反馈,发现程序中的错误。 2. 定位问题:通过调试工具和技术,确定错误所在的位置和原因。 3. 修复问题:修改代码,修复错误。 4. 验证修复:重新测试程序,确保错误已经修复,并且没有引入新的错误。 2.3 调试的常见方法 * 输出调试:在程序中插入打印语句,输出变量值和程序执行路径。 * 单步调试:

By Ne0inhk