本书标签: 游戏同人 

第 9 节 let 核心特性 —— 块级作用域(重点)

原神第二课堂:胡桃讲编程之JS——ES6进阶实战

一、类比:往生堂分房管账 & 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在预解析阶段的本质区别。