一、类比:往生堂分房管账 & MC 组件分区变量隔离
上一节课咱们初识了let关键字,知道了它能禁止重复声明、不污染全局对象。今天咱们就来拆解它最核心的优势 ——块级作用域,这也是let能彻底替代var的关键原因。
先拿往生堂的规矩打比方。早年堂里只有一本总账,搁在大堂,全堂上下谁都能翻、谁都能改,经常出现账目不匹配的乌龙。后来本堂主定下新规:大堂、账房、仪倌队、库房各配各的账册,每本册子只在自己的房间里生效,出了这间房,这本账就不作数。账房的伙计管不了库房的库存册,仪倌队也改不动前台的登记本,各区域权责分明,再也没出现过数据错乱的情况。
ES2 里的var就像早年的总账册,只有全局和函数两层范围,只要不在函数里,哪怕写在if、for的大括号里,外面也能随便访问,很容易 “变量泄露”。而let带来的块级作用域,就像分房管账的规矩 —— 只要是大括号包裹的代码块,里面用let声明的变量,就只能在这个块里用,出了大括号就彻底访问不到,天然隔离、互不干扰。
放到我的世界基岩版开发里更是实用。咱们写 GUI 组件时,经常会做多个功能模块:主界面按钮、设置面板、背包弹窗,每个模块都可能用到count、index这类常用变量名。用var的话,同名变量很容易互相覆盖,设置面板改个数,主界面的计数就乱了;用let的话,每个模块包在独立的代码块里,同名变量各自生效,完全不会串线,写再多组件也不怕命名冲突。
二、正文:作用域演进 + 块级作用域规则 + 经典场景对比
一、先回顾:ES2 中 var 的作用域局限
在讲解块级作用域之前,咱们先复盘旧语法的痛点。ES2 原版语法里,var声明的变量只有两种作用域:
全局作用域:在函数外部声明,整个脚本随处都能访问;
函数作用域:在函数内部声明,只能在当前函数里使用,函数外部访问不到。
但它有一个致命缺陷:没有块级作用域。所谓 “代码块”,就是被大括号{ }包裹的区域,比如if语句、for循环、while循环,甚至单独写一对大括号,都算一个代码块。var不会被这些大括号限制范围,变量会 “泄露” 到块的外面,引发各种意料之外的问题。
经典反例 1:if 代码块变量泄露
javascript
运行
if (true) {
var message = "var声明的变量";
}
// 外部可以正常访问,变量泄露了
console.log(message); // 输出:var声明的变量
经典反例 2:for 循环变量泄露
javascript
运行
for (var i = 0; i < 5; i++) {
// 循环逻辑
}
// 循环结束后,i依然存在,值为5
console.log(i); // 输出:5
这种变量泄露在复杂代码里非常危险,很容易在不知情的情况下修改了循环变量、条件变量,造成逻辑 bug。
二、let 的块级作用域规则
let的出现,彻底补上了这个短板,它带来了真正的块级作用域:
用let声明的变量,只在当前所在的代码块(大括号包裹的区域)内有效;代码块外部无法访问该变量。
所有大括号包裹的区域都会形成独立作用域,包括但不限于:
if / else 条件语句
for / while / do-while 循环语句
switch 分支语句
try / catch 语句
单独的{ }裸代码块
基础示例:if 块内隔离
javascript
运行
if (true) {
let msg = "let声明的变量";
console.log(msg); // 块内正常访问,输出:let声明的变量
}
// 块外无法访问,直接报错
console.log(msg); // Uncaught ReferenceError: msg is not defined
变量被牢牢限制在大括号内部,不会泄露到外部,从语法根源上避免了污染。
三、重点场景:for 循环中的块级作用域
let在for循环里的优势最为明显,也是新手最容易踩var坑的地方。
用var声明循环变量时,整个循环共用同一个变量,循环结束后变量还会保留最终值,经常导致事件绑定、异步逻辑出错;而用let声明循环变量时,每一轮循环都会生成一个独立的作用域,每一轮的变量都是全新的、互不干扰。
对比案例:循环绑定点击事件
javascript
运行
// 写法1:用 var,所有按钮点击都输出 5
var btns = document.querySelectorAll("button");
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function() {
console.log("点击了第" + i + "个按钮");
}
}
// 结果:无论点击哪个按钮,都输出“点击了第5个按钮”
// 原因:var的i是全局的,点击事件触发时,循环早已结束,i的值已经变成了5
javascript
运行
// 写法2:用 let,每个按钮对应正确的索引
var btns = document.querySelectorAll("button");
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function() {
console.log("点击了第" + i + "个按钮");
}
}
// 结果:点击第1个按钮输出0,第2个输出1,依次对应
// 原因:每一轮循环都有独立的块级作用域,每个i只属于当前轮次
这个特性在我的世界基岩版开发中同样高频实用。比如循环注册多个 GUI 按钮、批量生成道具列表,用let声明索引变量,每个按钮的回调都能拿到正确的序号,再也不会出现所有按钮都触发最后一个功能的 bug。
四、块级作用域的嵌套规则
块级作用域支持多层嵌套,规则和函数作用域一致:
内层作用域可以访问外层作用域的变量;
外层作用域无法访问内层作用域的变量;
内层可以声明和外层同名的变量,遵循 “就近原则”,内层变量不会影响外层。
javascript
运行
let outer = "外层变量";
if (true) {
let inner = "内层变量";
console.log(outer); // 内层可以访问外层,正常输出
if (true) {
let deep = "深层变量";
console.log(inner); // 深层可以访问上一层
}
console.log(deep); // 报错,外层访问不到深层变量
}
五、补充:裸代码块的应用
如果只是想隔离一段变量,不需要条件和循环,可以直接写一对大括号形成裸代码块,配合let实现临时变量隔离。
javascript
运行
{
let temp = "临时计算变量";
console.log(temp);
}
// 外部访问不到temp,用完即销毁,不会污染环境
console.log(temp); // 报错
这种写法适合处理临时计算逻辑,用完就释放变量,非常干净。
三、课后练习(附答案)
基础题
什么是块级作用域?let声明的变量在什么范围内有效?
ES2 中的var有没有块级作用域?它会带来什么典型问题?
在for循环中,用let声明循环变量和用var声明,最核心的区别是什么?
进阶题
分析以下代码的运行结果,并说明原因:
javascript
运行
if (true) {
var a = 10;
let b = 20;
}
console.log(a);
console.log(b);
为什么循环绑定点击事件时,用var会出现所有索引都相同的问题,用let就能正常对应?
参考答案
块级作用域指变量只在大括号包裹的代码块内生效;let声明的变量仅在当前所在的代码块(大括号区域)内可以访问,块外无法访问。
var没有块级作用域;会导致变量泄露,代码块内声明的变量在外部也能访问,容易造成变量污染和逻辑 bug。
用var时,整个循环共用同一个变量,循环结束后变量依然存在;用let时,每一轮循环都有独立的作用域和独立的变量,互不干扰。
第一行输出10,第二行报错。原因:var没有块级作用域,变量泄露到 if 外部,可以正常访问;let有块级作用域,仅在 if 内部有效,外部访问会触发引用错误。
var声明的循环变量是全局共用的,点击事件触发时循环已经结束,变量停留在最终值;let每轮循环都生成独立的块级作用域,每个事件回调都绑定了对应轮次的变量,因此能拿到正确的索引。
本章小结
本节课我们深入学习了let最核心的块级作用域特性。它让变量的作用范围精确到每一个大括号,解决了var变量泄露、全局污染的历史痛点,尤其在循环绑定、多模块逻辑的场景中,能大幅减少隐性 bug。
不过let的特性还没讲完,下一节课我们继续拆解它的另一项关键规则 ——暂时性死区,彻底搞懂let和var在预解析阶段的本质区别。