☰
JavaScript控制流程:Switch、三元运算符与循环实践指南
2026/10/31 5:38:23 网站建设 项目流程

做前端这行,隔三差五就会有人问我同一个问题:基础语法要不要专门花时间记笔记。我的答案一直是——记,但别按教科书那样记。第20天这个节点很微妙,前面的变量、数据类型、运算符、条件判断都过了一遍,往后马上要碰函数、对象、数组方法,卡在中间这几块语法如果含糊,后面写任何业务逻辑都会别扭。今天记的重点就三样:Switch语句、三元运算符、循环。这三块凑在一天不是巧合,它们本质上都是"控制流程"的工具,解决的问题高度重合:怎么让程序在不同情况下走不同的路,怎么让一段代码重复跑很多遍。差别只在于适用场景、可读性和执行细节。写业务代码的人最常犯的错,不是不会写,而是写错地方——该用 switch 的地方堆了一串 if-else,该用三元的地方硬塞了五层嵌套,该用 for-of 的地方用了 for-in 然后被原型链上的属性坑到半夜。这篇笔记我按"为什么这么选、怎么写不出事、出了问题怎么查"三条线走完,刚入门的朋友能直接抄结构,写过一段时间的人可以对照着检查自己的旧代码。里面所有的例子、参数、坑点都是我自己和身边同行真实踩过的,涉及的人物和项目一律用代称。

1. 三块语法到底在解决什么问题

1.1 从业务场景反推语法选择

刚学语法的人容易陷入一种误区:把每个语法点当成独立的知识背下来,考试能填对空就算学会了。真到写页面逻辑的时候才发现,脑子里装的是十几个孤立的规则,面对一个"根据订单状态显示不同文案"的需求,反而不知道从哪下手。我的经验是先建立场景映射:当你要根据同一个变量的多个固定取值做分支处理,Switch语句是第一选择;当你只需要在两个结果之间做一次简短选择,三元运算符最省事;当你需要对一组数据或一段逻辑重复执行,循环就是唯一答案。

这个映射关系听起来朴素,但它能挡掉一大半选择困难。举个例子,商品列表页要根据状态码渲染标签,状态码有 0 到 5 六种,每种对应一句文案和一个颜色。这时候你脑子里应该立刻跳出 switch,而不是"我学过 if-else,就用 if-else 吧"。原因很实在:if-else 链在分支变多之后,每一层都要重复写变量名和比较运算符,视觉噪音大,改动时容易漏掉某一行;switch 把"判断谁"这个信息只写一次,下面全是 case 列表,扫一眼就知道覆盖了哪些取值。同一个逻辑,switch 版本比 if-else 版本少写将近三分之一的字符,而且新增一个状态时只需要在列表里插一行,不用去找最后一个 else if 在哪。

三元运算符的适用面更窄也更精准。它天生适合"一个值,两种可能"的场合,比如渲染时判断列表是否为空、给可选参数设默认值、根据布尔标志决定样式类名。很多人对它有偏见,觉得"三元运算符可读性差",其实问题不在语法本身,而在滥用——把三层以上嵌套塞进一行,或者在三元里写带副作用的函数调用。控制在一层、最多两层,返回值是简单表达式,三元写出来比 if-else 更紧凑也更清晰。

循环这块的选择维度比前两个多,因为它有五种常见写法,各自的行为差异很大。普通 for 适合需要索引、需要精确控制步长的场景;while 适合循环次数不确定、靠条件退出的场景;do-while 适合"至少执行一次"的场景,比如让用户输入直到输入合法;for-of 适合遍历可迭代对象的值,数组、字符串、Map、Set 都能用;for-in 只适合遍历对象的键,而且必须配合 hasOwnProperty 过滤,否则会把原型链上的可枚举属性一起带出来。选错的代价不是报错,而是拿到一堆莫名其妙的数据,排查起来非常费劲。

1.2 三者的定位差异与选择优先级

我把这三块语法的核心差异整理成一张对照表,实际写代码时按这张表走,基本不会选错:

语法核心用途判断依据典型场景主要风险
Switch语句同一变量的多分支等值判断严格相等(===)状态码转文案、类型分发、指令路由漏写 break 导致穿透;case 用了对象或表达式匹配失败
三元运算符二选一取值条件表达式的真假值默认值、类名拼接、空态文案嵌套过深;优先级搞错;在返回值位置做赋值
循环重复执行循环条件或迭代器列表渲染、批量处理、累加统计边界差一;死循环;遍历方式选错

这张表里最值得注意的是"判断依据"这一列。switch 用的是严格相等,意味着 case 后面写'1'和写1是两个完全不同的分支,从接口拿到的字符串状态码如果不做转换,直接和数字 case 匹配,会一路落到 default,而且不报错,排查时极难发现。三元运算符的条件部分只认真假值,空字符串、0、null、undefined、NaN 都会被当成假,这一点和 switch 的严格相等逻辑完全不同,混用的时候要格外小心。循环的判断依据是条件表达式每次迭代前的求值结果,任何一次求值为假就退出,所以条件里的变量更新位置如果放错,很容易写成死循环。

2. Switch语句:多分支等值判断的稳妥写法

2.1 基本结构与执行逻辑

先把骨架摆出来,这段代码我几乎每个项目里都会写一次以上:

function getOrderStatusText(status) { switch (status) { case 0: return '待支付'; case 1: return '已支付'; case 2: return '已发货'; case 3: return '已完成'; default: return '状态未知'; } }

执行逻辑用一句话讲清楚:把 switch 括号里的表达式求值一次,然后自上而下拿这个值和每个 case 后的表达式做严格相等比较,匹配上就从这个 case 开始执行,一直往下走,直到遇到 break、return 或者整个 switch 结束。注意两个细节,第一,括号里的表达式只求值一次,这也是 switch 比 if-else 链更高效的原因之一——if-else 链里如果写的是函数调用,每次比较都要重新执行;第二,比较用的是严格相等,不做类型转换,这一点和==的行为完全不同。

default分支的位置值得单独说一句。很多人以为 default 必须写在最后,其实语法上它可以在任意位置,匹配逻辑是"所有 case 都没匹配上才进 default"。但即便如此,我仍然建议把它放在最后,理由是可读性——读代码的人从上往下扫,最后看到一个兜底分支,心里是踏实的;如果 default 藏在中间,读者会怀疑自己漏看了什么。另外 default 也不是必须写,但如果你的 switch 是处理外部数据的,我强烈建议写上,哪怕只是return '未知状态',也好过函数在某些输入下返回 undefined,然后在调用方引发一堆莫名其妙的错误。

还有一个容易被忽略的点:case 后面跟的必须是表达式,不能是声明语句。见过有人在 case 后面直接写let temp = 1;然后报语法错误,其实只要加大括号包起来就行,但更推荐的做法是把声明提到 switch 外面,保持每个 case 内部只做返回或赋值。

2.2 穿透(fall-through)的两种面貌

break 漏写是 switch 最经典的坑。漏了之后不会报错,程序会继续执行下一个 case 的代码,这种行为叫穿透(fall-through)。新手遇到穿透通常是在调试时发现"明明状态是 1,怎么把状态 2 的逻辑也执行了",然后回头一行行看,才发现少了个 break。用 return 直接返回的写法天然免疫这个问题,因为 return 会直接结束整个函数,所以我个人的习惯是:能 return 就 return,确实需要在 switch 里赋值的场景才用 break。

不过穿透也不全是坏事,它有一种正当用法——多个 case 共享同一段逻辑。比如判断某个月份属于哪个季度:

function getQuarter(month) { switch (month) { case 1: case 2: case 3: return '第一季度'; case 4: case 5: case 6: return '第二季度'; case 7: case 8: case 9: return '第三季度'; case 10: case 11: case 12: return '第四季度'; default: return '月份不合法'; } }

这种写法是刻意为之,空 case 依次往下落,直到命中带逻辑的那个,可读性反而比写一堆month >= 1 && month <= 3更好。区分"故意穿透"和"漏写 break"有个简单标准:故意穿透的 case 后面一定是空的,没有任何语句;一旦 case 下面写了语句却没有 break,就要高度警惕。我后来养成一个习惯,写完 switch 之后专门扫一遍,看看有没有哪个 case 有语句但结尾不是 break 或 return,扫一遍只要几秒钟,能省掉很多调试时间。

注意:团队协作时,如果确实要故意穿透,可以在空 case 下加一行注释说明"有意落穿",避免后人误以为是漏写而给你补上 break,把逻辑改坏。

2.3 switch 的能力边界与替代方案

switch 不是万能的,用错地方会很别扭。它的第一个边界是只能做等值判断,做不了范围判断。想表达"分数 90 以上是优秀"这种需求,硬用 switch 就得写case 90: case 91: ... case 100:,这不现实。这种情况老老实实用 if-else 链,或者更优雅一点,用数组加 find 的映射方式:

const scoreRules = [ { min: 90, label: '优秀' }, { min: 75, label: '良好' }, { min: 60, label: '及格' }, { min: 0, label: '不及格' }, ]; function getScoreLabel(score) { const hit = scoreRules.find(rule => score >= rule.min); return hit ? hit.label : '分数不合法'; }

这种映射表写法在规则变多、需要动态配置(比如从后台拉取规则)的时候优势明显,switch 是写死在代码里的,映射表可以随时替换数据源。第二个边界是case 数量特别多的时候,比如几十种错误码,switch 会写得非常长,这时候用对象字面量做字典更合适:

const ERROR_TEXT = { 1001: '参数缺失', 1002: '参数格式错误', 2001: '资源不存在', }; function getErrorText(code) { return ERROR_TEXT[code] || '未知错误'; }

三行代码解决几十行 switch 的活,而且查找是哈希级别的,代码也更短。代价是对象键只能是字符串或 Symbol,数字键会被隐式转成字符串,所以用数字 code 查找时要确认类型一致。我的实际取舍是:分支在 3 到 8 个之间、每个分支逻辑有点复杂,用 switch;分支超过十个、每个分支只是一句简单映射,用字典对象;涉及范围判断,用 if-else 或规则数组。

3. 三元运算符:短小但容易被滥用的表达

3.1 语法构成与运算优先级陷阱

三元运算符的形式是条件 ? 值1 : 值2,条件为真取值1,为假取值2。它本质上是一个表达式,有返回值,所以能出现在任何需要值的位置——赋值号右边、函数参数里、模板字符串里、数组元素里。这个"是表达式"的特性是它和 if-else 最根本的区别,也是它存在的意义。

最典型的用法就是给默认值:

const displayName = user.nickname ? user.nickname : '匿名用户'; const listText = cart.length > 0 ? `共 ${cart.length} 件商品` : '购物车是空的';

注意这里用的是真假值判断,不是判 undefined。如果user.nickname是空字符串,也会走"匿名用户"分支,这通常符合预期,但如果业务上要求空字符串也算有效昵称,就得改成user.nickname !== undefined ? ...,判断条件写清楚比省字重要。

优先级是三元运算符最容易翻车的地方。它的优先级低于比较运算符、高于赋值运算符,这个规则导致几个经典写法:

// 想先赋值再比较,实际会报错 let a = 1, b = 2; a > b ? a : b = 5; // SyntaxError: Invalid left-hand side in assignment // 正确写法,加括号 a > b ? a : (b = 5);

原因在于解析时a > b ? a : b会先被当成一个整体,然后才轮到= 5,于是赋值号左边成了一个表达式,直接语法错误。这类问题在编辑器里通常会有提示,但在压缩过或者拼接过的代码里不一定能看见。我的做法是:三元运算符只在"取值"场景用,绝不在里面写赋值、自增、函数调用这类有副作用的操作。一旦需要在分支里做事而不是取值,立刻换成 if-else,代码会清楚很多。

还有一个和字符串拼接相关的坑。三元运算符的优先级低于+,所以'总计:' + count > 0 ? '有商品' : '无商品'会先算'总计:' + count > 0,结果永远是"有商品"。正确写法要用括号把三元整体包起来:'总计:' + (count > 0 ? '有商品' : '无商品')。这种错误不报错,只是结果不对,特别隐蔽。

3.2 嵌套三元的可读性边界

嵌套三元是争议最大的写法。支持的人说它紧凑,反对的人说它难读。我的观点是:两层以内可以接受,前提是每一层都格式清晰;三层以上一律改写。两层的情况,比如根据角色和登录状态给不同的提示:

const tip = isLogin ? (isAdmin ? '进入管理后台' : '进入个人中心') : '请先登录';

这个写法可读,因为括号明确了嵌套层级,缩进也对齐了。但同样是两层,如果写成一行isLogin ? isAdmin ? 'A' : 'B' : 'C',读的人就得在脑子里做括号匹配,体验差很多。所以即使只有两层,我也会换行加括号。

三层以上的情况,我一般用三种方式改写。第一种是提前返回:

function getTip(isLogin, isAdmin) { if (!isLogin) return '请先登录'; if (isAdmin) return '进入管理后台'; return '进入个人中心'; }

第二种是查表:

const TIP_MAP = { 'false': '请先登录', 'true-true': '进入管理后台', 'true-false': '进入个人中心', }; const tip = TIP_MAP[`${isLogin}-${isAdmin}`];

第三种是数组索引,适合少量互斥取值:

const LEVEL_TEXT = ['低', '中', '高']; const levelText = LEVEL_TEXT[level] || '未知';

这三种改写方式各有适用面:提前返回适合有条件优先级的情况,查表适合组合数量确定且不多的情况,数组索引适合取值本身就是连续整数的场景。选哪个看数据形状,不必拘泥。

3.3 常见误区:返回值位置、短路与类型转换

三元运算符有三个高频误区,我按踩坑频率排一下。第一个是把它当成语句用,也就是不接收返回值:

// 能跑,但没意义 status === 1 ? doSomething() : doOtherThing();

这种写法语法上没问题,但既然两个分支都要执行动作而不是取值,用 if-else 更符合语义,也更好调试。三元的存在价值是表达式,用成语句就失去了优势。

第二个是忽略短路特性。三元运算符只会执行被选中的那个分支,另一个分支不会求值,这一点和逻辑运算符&&、||的性质一致。利用这个特性可以避免不必要的计算,比如list.length ? list[0].name : '无数据',当 list 为空时不会去读list[0].name,也就不会报"读取 undefined 的属性"错误。但如果两个分支里都写了函数调用,就要意识到只有一个会真正执行,别指望它俩都跑。

第三个是类型不一致导致的后续问题。const val = flag ? 1 : '1'这种写法,val 的类型在运行时是变化的,一处是 number 一处是 string,后续做val === 1判断时就会出问题。我遇到过一次线上问题,就是接口返回的字段有时是数字有时是字符串,前端用了三元做了层保底处理,结果保底分支返回的是字符串,导致后面的严格比较全部失效。修起来简单,但定位花了不少时间。所以三元两个分支的返回值类型,我尽量保持统一,要么都是字符串,要么都是数字,宁可多写一次转换。

4. 循环:五种写法怎么选、怎么写不炸

4.1 for 循环的三段式与执行顺序

普通 for 是最基础的循环,结构是for (初始化; 条件; 更新)。它的执行顺序值得背下来,因为大量"差一错误"和"死循环"都源于对这个顺序理解不清:

  1. 执行初始化表达式,只执行一次;
  2. 求值条件表达式,为真则进入循环体,为假则结束循环;
  3. 执行循环体;
  4. 执行更新表达式;
  5. 回到第 2 步。

关键点在于更新表达式在循环体之后执行,条件判断在循环体之前执行。这意味着一件事:如果在循环体内用 continue,更新表达式仍然会执行,不会死循环。但如果是 while 循环,你在 continue 之前忘了更新条件变量,那就会无限循环。这个差异很实用,也是我在处理"跳过某些元素但要继续迭代"的场景时优先选 for 的原因。

差一错误是最常见的。想遍历数组的每个元素,条件是i < arr.length,不是i <= arr.length。写成<=会在最后一次取到 undefined,然后用 undefined 的属性,直接报错。我的习惯是写循环时脑子里过一遍边界:长度 0、长度 1、最后一个元素的索引是多少。长度 0 时条件0 < 0为假,循环一次都不进,这是正确的;长度 1 时只跑一次,也正确。把这两个极端情况想清楚,基本就不会差一。

4.2 while 与 do-while 的差别

while 的结构最简单:条件为真就执行,循环体里必须有能改变条件的语句,否则就是死循环。

let index = 0; while (index < list.length) { process(list[index]); index += 1; // 忘了这行就是死循环 }

do-while 和 while 的唯一差别是先执行一次循环体,再判断条件,所以无论如何至少执行一次:

let input; do { input = getUserInput(); } while (input === '');

这个"至少执行一次"的语义在需要先拿到数据才能判断的场景里非常自然,用 while 写就要先手动执行一遍再进循环,代码重复。不过实际项目里 do-while 用得不多,因为它的条件写在末尾,读代码时视线要来回跳,而且容易忘掉末尾的分号(do-while 的 while 后面必须有分号)。我的建议是:能不用就不用,除非场景确实是"先做再判"。

死循环是这个环节最需要警惕的。除了忘记更新变量,还有一种隐蔽的情况:循环条件里用了一个在循环体内被意外修改的值。我见过一次因为循环体内调用的函数修改了全局计数变量,导致条件永远为真。排查这类问题的办法是在循环体开头加一行console.log打印条件变量,跑几轮就能看出它在往哪个方向走,如果打印出来的值不变化或者往回跳,问题就找到了。

4.3 for-of 与 for-in 的本质区别

这两个是最容易被搞混的,我明确说清楚:for-of 遍历的是值,for-in 遍历的是键(而且是字符串形式的键),并且 for-in 会沿着原型链向上找可枚举属性。用一个例子就能看明白:

const arr = ['a', 'b', 'c']; for (const i in arr) { console.log(i); // '0' '1' '2',注意是字符串 } for (const v of arr) { console.log(v); // 'a' 'b' 'c' }

数组用 for-in 遍历会导致三个问题:拿到的是字符串索引,做算术运算前要转换;遍历顺序在某些引擎上不保证;如果数组对象上被挂载了额外属性(很多人会给数组加自定义方法),这些属性也会被遍历出来。所以数组遍历永远用 for-of 或普通 for,绝不用 for-in。

for-in 唯一正当的用途是遍历普通对象的键:

const config = { host: 'localhost', port: 8080 }; for (const key in config) { if (Object.prototype.hasOwnProperty.call(config, key)) { console.log(key, config[key]); } }

加上 hasOwnProperty 判断是为了过滤原型链上的属性,虽然现代开发中很少往 Object.prototype 上挂东西了,但防御性写法不亏。更省事的替代方案是Object.keys(config).forEach(...)或者for (const key of Object.keys(config)),后者能直接拿到数组,遍历的是自己的键,不需要额外判断。

for-of 的能力范围比很多人以为的广。它不只能遍历数组,字符串、Map、Set、NodeList、arguments 对象,只要实现了迭代器协议都能用。字符串遍历尤其好用,for (const ch of '你好世界')会正确按字符拆分,不会像charAt那样把代理对拆坏。但要注意 for-of 拿不到索引,需要索引的场景要么用entries(),要么老老实实写普通 for。

4.4 循环控制:break、continue 与标签

break 跳出整个循环,continue 跳过本次迭代进入下一次,这两个不难。真正少有人用的是标签(label),它可以让你从嵌套循环的内层直接跳到外层:

outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (i === 1 && j === 1) { continue outer; // 直接跳回外层循环的下一次迭代 } console.log(i, j); } }

没有标签的话,内层循环里的 break 只能跳出内层,外层还在继续跑。标签的写法在多层嵌套查找的场景下很实用。但标签也有代价:它的可读性对不熟悉的人来说很差,而且很多代码规范不建议使用。我的实际做法是优先重构掉嵌套,比如把内层循环抽成函数,用 return 提前退出,或者用Array.prototype.some、find这类自带提前退出的方法。实在改不了再用标签,用的时候在标签上写一行注释说明意图。

5. 把三块语法串起来:一个订单统计的完整实操

5.1 需求拆解与实现思路

光看语法点容易散,我拿一个真实点的场景把三块串起来。需求是这样:有一批订单数据,每条包含订单号、状态码、金额、商品件数。要求输出四样东西——每个状态各有多少单、总金额、需要提醒用户补运费的单据列表(金额低于某个门槛的)、以及一句总体说明文案。

拆解一下:状态统计需要遍历加多分支判断,正好用循环加 switch;补运费判断是二选一,用三元运算符;总金额累加和文案选择分别是循环加三元。整个实现能覆盖今天学的三块内容。

5.2 具体代码实现

const orders = [ { id: 1001, status: 0, amount: 39.9, items: 2 }, { id: 1002, status: 1, amount: 129, items: 1 }, { id: 1003, status: 1, amount: 59, items: 3 }, { id: 1004, status: 2, amount: 88.5, items: 1 }, { id: 1005, status: 9, amount: 20, items: 1 }, ]; const FREE_SHIPPING_LINE = 99; function summarize(list) { const statusCount = { 0: 0, 1: 0, 2: 0 }; const needShippingFee = []; let totalAmount = 0; for (const order of list) { // switch 统计状态分布 switch (order.status) { case 0: statusCount[0] += 1; break; case 1: statusCount[1] += 1; break; case 2: statusCount[2] += 1; break; default: statusCount.unknown = (statusCount.unknown || 0) + 1; break; } // 累加金额 totalAmount += order.amount; // 三元判断是否需要补运费 const feeTip = order.amount < FREE_SHIPPING_LINE ? `订单 ${order.id} 还差 ${(FREE_SHIPPING_LINE - order.amount).toFixed(2)} 元免运费` : ''; if (feeTip) { needShippingFee.push(feeTip); } } // 三元生成总结文案 const summaryText = totalAmount > 0 ? `共 ${list.length} 笔订单,合计 ${totalAmount.toFixed(2)} 元` : '暂无有效订单'; return { statusCount, needShippingFee, totalAmount, summaryText }; } console.log(summarize(orders));

跑一遍结果:状态统计是{0: 1, 1: 2, 2: 1, unknown: 1},总金额 336.4,补运费提醒两条(1001 和 1005)。

5.3 关键设计点复盘

这段代码里有几个地方值得说。第一,statusCount我用了一个对象而不是三个变量,因为状态是有限的枚举值,对象可以直接用状态码做键,统计时statusCount[order.status] += 1一行搞定,但这里我仍然用了 switch,原因是需要处理"未知状态"这种扩展情况——直接索引的话,遇到不存在的状态会得到undefined + 1 = NaN,而 switch 的 default 分支能显式兜底。这就是我在第 2.3 节说的取舍:分支逻辑有点内容时,switch 更稳。

第二,FREE_SHIPPING_LINE提成常量而不是写在表达式里。包邮门槛这种东西业务侧随时会调,硬编码在判断里,改的时候要翻好几处,还容易漏。常量集中放在文件顶部,后续换成配置项也方便。

第三,toFixed(2)处理金额。浮点数直接相加会有精度问题,39.9 + 99在某些情况下会得到138.89999999999998,展示层必须格式化。这不是循环或三元的问题,但金额计算离不开这个处理,一起记下来。

第四,补运费判断用三元生成字符串,然后用if (feeTip)判断是否收集。有人会问为什么不直接在三元里 push,因为 push 有副作用,放在三元分支里虽然能跑,但语义不清。前面说过,三元用于取值,不做动作,这里严格遵守了这个原则。

6. 常见问题与排查技巧实录

6.1 问题速查表

踩过的坑整理成表,出问题时对着看,比漫无目的地翻代码快得多:

现象可能原因排查动作修复方式
明明匹配了 case,下面逻辑也执行了case 分支漏写 break搜 switch 块内所有 case,看是否都有 break 或 return补 break,或改用 return
字符串状态码匹配不上数字 caseswitch 用严格相等,不做类型转换打印 switch 表达式的值和类型统一类型,或在进入 switch 前转换
三元判断结果总是同一个分支优先级问题,条件被拼接符吃掉把三元整体加括号后测试用括号明确分组
三元赋值语句报语法错误赋值号左边成了表达式看报错位置的行号对应哪段把赋值拆成独立语句
数组遍历出 undefined条件写成<=导致越界打印循环变量和数组长度改成<
循环停不下来循环体没改变条件变量,或函数内部改了共享变量在循环体内打印条件变量补更新语句,或隔离变量作用域
for-in 遍历数组多出奇怪的值原型链上有可枚举属性打印 key 和 typeof key改用 for-of 或 Object.keys
累加金额出现一长串小数浮点精度问题打印每一步的中间值展示层用 toFixed,计算侧考虑整数分
do-while 报语法错误末尾 while 后漏了分号看报错行补分号
嵌套循环只跳出了内层break 默认只跳一层检查循环嵌套层数用标签,或抽函数用 return

6.2 几个不写在文档里的实操心得

速查表之外,有几个经验是文档里不会写、但实际开发中很值钱的。

第一,switch 的 case 尽量写常量,不要写表达式。见过有人写case getStatus():,每次匹配都调用一次函数,如果这个函数有副作用(比如打日志、改计数),行为会很诡异,而且性能也浪费。case 后面只放字面量或已定义的常量,这条我当纪律执行。

第二,循环内部避免重复计算长度。for (let i = 0; i < arr.length; i++)每次迭代都读一次 length,现代引擎优化得很好,实际影响很小,但在明确知道数组不变的场景下,我仍然习惯先存到变量里const len = arr.length。更重要的是,如果循环体内部会修改这个数组(push、splice),那用缓存的长度会导致漏遍历或者越界,这时候反而必须每次重新读 length。所以这条不是死规则,要看循环体有没有副作用。

第三,循环里做异步操作要特别小心。for循环里直接用await是串行执行的,一个接一个;用forEach加async回调则会并发跑,而且forEach不会等回调完成,后面的代码会先执行。这两种行为差异巨大,我见过统计结果总是少几条的 bug,最后定位到是用forEach配异步。需要串行就用for-of加await,需要并发就用Promise.all配map,别用forEach。这块算是循环知识往异步方向的延伸,第 20 天先把结论记住,后面学到 Promise 再回来理解细节。

第四,三元运算符里不要调用有副作用的函数。虽然语法允许,但读代码的人很难一眼看出哪个分支会被执行,调试时也不方便打断点。我现在的做法是:分支里要做事,一律 if-else;分支里只是取值,才用三元。

第五,写完循环先想边界。空数组、单元素数组、超大数组、循环体里可能抛异常的情况,这四个想一遍,能挡掉大部分运行时错误。特别是空数组,for-of会安全跳过,但如果你在循环外面用list[0]取第一个元素,就会拿到 undefined,加个三元判断或者可选链能解决。

第六,控制台调试循环时用 console.table。打印一堆对象用console.log很难看,console.table(list)会以表格形式展示,字段对齐,一眼能看出哪条数据的字段缺失或类型不对。这个小技巧帮我省了很多排查时间。

最后分享一个我自己在用的方法:每学完一天语法,我会挑一个之前写过的旧代码片段,用当天学的东西重写一遍,然后对比两版。第 20 天这天,我重写的是一个状态渲染的小函数,原来用了一串 if-else,改完变成 switch 加一个映射表,行数少了将近一半,而且新增状态时不用再动逻辑判断部分。这种"回头改一遍"的练习比刷十道语法题有用,因为你能感受到每种写法在真实代码里的分量,而不是停留在"我知道它是什么意思"的层面。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询