
刚入行那会儿我最怕看到的代码不是复杂的算法而是一层层叠到天亮的 if/else。当时带我的前辈指着一坨嵌套了七八层的条件判断说“代码写得像俄罗斯套娃bug 就藏在每一层的缝隙里。”后来我自己也踩过不少条件语句的坑才明白 JavaScript 里的条件判断绝不只是“如果……否则……”这么简单。它背后牵扯到类型转换、真值假值、短路求值、作用域甚至直接决定了一段代码能不能被别人包括三个月后的自己顺畅读下去。这篇东西我想把 JavaScript 条件语句这件事掰开揉碎了聊一聊。不管你是刚摸到编程边的新手还是写了两三年业务代码想回头整理基本功的老手看完应该都能用得着。我会把 if/else、switch、三元运算符、逻辑短路这些常见写法放在真实场景里讲配合一些我在项目里实际遇到的问题和排查过程尽量让每一条经验都能直接抄作业。1. 条件判断的底层逻辑先搞懂真值与假值条件语句的本质上是在做一件事判断一个值在布尔语境下是“成立”还是“不成立”。很多人写条件判断写不明白不是语法不会而是对 JavaScript 的隐式类型转换和真值假值规则不够敏感。1.1 真值与假值很多 bug 的根源JavaScript 里的每个值天生自带“真假属性”。在一个条件上下文里会被当作 false 的值其实只有这几个false、0、-0、0n、空字符串或、null、undefined、NaN。除了这些其他所有值都会被当作 true包括空数组 []、空对象 {}、字符串 false、数字 Infinity。这个规则几乎人人都背过但实际写代码时经常被坑。举个例子有一次我处理用户输入的分页参数想给页码一个默认值随手写了这样的代码function getPage(page) { if (page) { return page; } return 1; }表面看没毛病page 有值就返回没值就默认 1。但如果用户传进来的页码是 0这个函数会直接返回 1。而在很多业务系统里第 0 页是可以合法存在的。这里的问题就是 0 被真值判断误伤了。后来我改成显式判断function getPage(page) { return page ! undefined page ! null ? page : 1; }类似的场景还有判断一个字符串是否为空时如果用 if (str) 做排除那么字符串 0 也会被当成真值放行这在某些业务里可能不是预期行为。所以我的习惯是如果业务上关心的是“有没有传值”就用 undefined 或 typeof 来判断如果关心的是“值是否有效”才用真值判断。1.2 从 if/else 说起最简单也最容易写乱if/else 的语法本身不需要多讲但它的书写姿势直接决定代码的走向。我见过很多项目里的条件判断写得像一锅粥主要问题集中在三件事嵌套过深、分支不对称、条件表达式过长。嵌套过深是最常见的。比如一个复杂的表单提交先判断用户是否登录再判断表单是否完整再判断数据是否合法于是一层套一层最后右括号一拉到底。这种代码不是不能跑而是出了问题很难定位。处理嵌套的核心思路是“提前返回”。前端表单校验就是个典型场景。我习惯把不满足条件的情况先处理掉让主流程保持在平铺状态function handleSubmit(formData) { if (!formData.user) { toast(请先登录); return; } if (!formData.name || !formData.phone) { toast(请填写姓名和手机号); return; } if (!isPhoneValid(formData.phone)) { toast(手机号格式不正确); return; } // 走到这里所有前置条件都满足了放心处理主流程 submitApi(formData); }这种写法让每一条校验规则各自独立逻辑链路非常清晰。新增一条校验直接加一个 if return 就行删掉一条校验直接删对应代码块完全不影响其他逻辑。相比之下嵌套写法在增删校验规则时容易动错地方牵一发而动全身。2. 多分支场景switch 与对象映射表的取舍当条件分支超过三个的时候继续堆 if/else 就会开始显得笨重。这时候很多人会想到 switch但 switch 并不是所有多分支场景的最优解它有自己的适用边界。2.1 switch/case 的适用场景与隐藏陷阱switch 适合的场景是“同一个变量与多个离散值做全等于比较”。比如根据订单状态码返回对应的文案这种场景值非常固定用 switch 就比一大串 if/else 清爽得多。function getOrderStatusText(status) { switch (status) { case 0: return 待付款; case 1: return 待发货; case 2: return 运输中; case 3: return 已签收; case 4: return 已取消; default: return 未知状态; } }这里有个规则必须牢记switch 用的是全等比较不是宽松相等。所以 case 0 和 case 0 是两个完全不同的分支。我曾在项目里接过一个后端传参混乱的接口状态字段一会儿是数字 1一会儿是字符串 1结果前端 switch 半天匹配不上最后在接口层统一做了 Number() 转换才消停。另外 switch 的 fall-through穿透特性也是一把双刃剑。利用穿透可以让多个 case 共用同一段逻辑比如switch (level) { case guest: case user: return 普通权限; case admin: case superadmin: return 管理权限; default: return 无权限; }但如果不小心漏写 break 或 return程序就会悄无声息地继续往下执行产生诡异的结果。所以我的建议是switch 里的每个 case 要么 return 收尾要么 break 收尾千万不要依赖穿透来“省代码”除非你有意为之且加了注释。2.2 比 switch 更灵活的思路对象映射表实际项目里我很少用 switch因为大多数多分支场景还需要附带一些额外逻辑比如调函数、做计算。这时候我会选择对象映射表本质上是拿对象属性查找替代条件跳转。举个例子一个告警系统要根据不同告警类型执行不同的处理动作用 if/else 或 switch 就是一堆分支但用对象映射就非常干净const alertHandlers { cpu: () handleCpuAlert(), memory: () handleMemoryAlert(), disk: () handleDiskAlert(), network: () handleNetworkAlert(), }; function handleAlert(type, data) { const handler alertHandlers[type] || handleUnknownAlert; handler(data); }对象映射表的优势在于新增一种告警类型只需要在对象里加一个键值对不用动主函数。这是典型的“开闭原则”实践。相比之下switch 每加一个分支都要改原函数时间长了函数越来越臃肿。还有一种进阶用法把条件判断直接变成配置数据。比如根据用户等级和购买金额计算折扣与其写一堆嵌套 if不如维护一张配置表const discountRules [ { maxLevel: 1, minAmount: 0, rate: 0.9 }, { maxLevel: 2, minAmount: 0, rate: 0.85 }, { maxLevel: 3, minAmount: 500, rate: 0.8 }, { maxLevel: 3, minAmount: 1000, rate: 0.7 }, ];条件本身变成了可查的数据逻辑就退化为一次遍历查找。这在业务规则频繁变动的系统里尤其好用产品改规则时前端甚至不需要动逻辑改配置就行。3. 条件表达式瘦身三元运算符与逻辑短路的实战用法条件语句写多了你会发现很多判断其实只有两三个分支。这时候用完整的 if/else 会显得啰嗦于是三元运算符和逻辑短路就成了更好的选择。不过这两个东西用得好是代码神兵用不好就是可读性杀手。3.1 三元运算符的正确打开方式三元运算符的语法是 condition ? expr1 : expr2。我个人的使用准则是单行能放下且逻辑简单时才用一旦表达式变复杂立刻换回 if/else。一个经典的反面案例是这样的const result (a b ? (c d ? both : a) : (c d ? c : d));这种多重嵌套的三元表达式读起来需要在大脑里建一棵语法树非常消耗精力。我见过同事 debug 这种代码一行表达式盯了十分钟才捋清楚。后来我重构时直接用 if/else 拆开配合变量命名逻辑马上清晰了。三元运算符最适合的场景是“根据条件给一个变量赋值”。比如一个自定义弹窗的主题色由于文本是浅色还是深色决定按钮文字颜色const buttonColor theme dark ? #ffffff : #333333;还有一种写法是配合模板字符串动态拼内容const tipText isMobile ? 请使用手机扫码 : 请使用电脑浏览器访问;三元运算符还有个不那么为人知的用法——处理默认值。但它和逻辑或 || 之间有一个关键区别必须讲清楚三元运算符判断的是布尔值而 || 判断的是真值。这意味着如果期待的“默认值触发条件”是 falsy两者行为就不同了。比如空字符串是 falsy所以 || 默认值 会返回默认值但如果你本来就允许空字符串作为合法值用 || 就会出问题。3.2 逻辑短路一行代码解决条件执行逻辑与 和逻辑或 || 在 JavaScript 里不只是布尔运算符它们还承担了条件执行的功能这就是短路求值。表达式 a b 时如果 a 为假就直接返回 a不会去管 b表达式 a || b 时如果 a 为真就直接返回 a。短路求值最常见的应用是在 React/Vue 里做条件渲染。比如{isLoggedIn UserMenu /}这段代码的意思是当 isLoggedIn 为真时渲染 UserMenu 组件为假时什么都不渲染。这在日常开发中非常高频。但要注意如果 isLoggedIn 不是布尔值而是一个可能为 0 或 NaN 的值 左侧的假值 0 会被直接返回并渲染到界面上。React 里 0 是会被渲染成数字 0 的浏览器页面上就可能莫名出现一个“0”。所以条件渲染使用的判断值最好显式转成布尔或者写成 !!isLoggedIn。另一个常用的场景是函数执行前的守卫config.debug console.log(当前配置:, config);这样 debug 为 false 时不会执行 console.log代码也很紧凑。还有一种常见用法是兜底取值const name user.name || 匿名用户;这段代码看起来简洁但前面提到过如果 user.name 是空字符串也会被替换成“匿名用户”。所以在处理业务数据时这个写法要谨慎。想保留空字符串得用 ES2020 引入的空值合并运算符 ??const name user.name ?? 匿名用户;?? 只有在左侧是 null 或 undefined 时才会取右侧值0、空字符串、false 都会被保留。这是我在新项目里处理默认值时的首选。4. 复杂条件的组织与重构从“能跑”到“好读”业务复杂到一定程度条件判断就不可能永远保持简单。真正拉开程序员差距的往往不是能不能写出功能而是能不能把几百行的条件逻辑收拾得整整齐齐。4.1 条件嵌套过深的补救策略前端开发里最常见的深嵌套场景是数据链路长的流程控制。比如“如果用户已登录并且有收货地址并且购物车有商品就生成订单”。每多一个条件代码就多一层缩进。等到逻辑全写完了一个函数变成二十行代码十层花括号。对这种问题除了前面说的提前返回我还会用“合并条件”来降层数。就是把多个必须同时满足的条件用逻辑运算符合并成一行// 嵌套写法 if (isLogin) { if (hasAddress) { if (cartCount 0) { createOrder(); } } } // 合并写法 if (isLogin hasAddress cartCount 0) { createOrder(); }合并之后整个函数少了好几层嵌套读起来像一句自然语言。但合并条件有个隐藏要求这几个条件之间没有任何副作用且不区分优先级。如果不同条件的组合需要走不同分支就不能盲目合并。比如复杂的权限校验用户既要有登录态又要满足角色限制但未登录和登录但角色不符的处理方式不一样。这时候合并成一句话反而抹平了差异正确的做法是保留两层判断结构if (!isLogin) { redirectToLogin(); return; } if (!hasPermission(user, admin)) { showForbiddenPage(); return; }每一层都有自己独立的“不满足时的处理”结构反而最清晰。4.2 把条件抽成函数可读性翻倍的关键一个条件表达式如果复杂到需要注释才能看懂就应该把它抽成一个命名清晰的函数。比如有一段判断用户能否参加某个活动的条件if (!user.isBlocked user.level 3 activity.status open activity.startTime Date.now() activity.endTime Date.now()) { // 参与活动 }这串条件光看就头疼更别说维护了。抽成函数之后function canJoinActivity(user, activity) { if (user.isBlocked) return false; if (user.level 3) return false; if (activity.status ! open) return false; const now Date.now(); return now activity.startTime now activity.endTime; } if (canJoinActivity(user, activity)) { // 参与活动 }函数名本身就是文档读代码的人不需要逐字分析每个 和 是什么意思只要看函数名就知道这是一次资格判断。而且抽成函数之后这个判断还可以被复用本来只能在这一个地方用的逻辑现在别的地方也能引用。抽函数时要注意函数的粒度。我见过一种过度抽象把 if (a b) 这种一句话判断也包成函数反而是画蛇添足。我的标准是条件超过一个“子句”时值得考虑抽函数或者这个条件在项目里会被多处复用时一定要抽函数。4.3 可选链运算符告别层层判空前面几节讲了很多条件判断的“厚”逻辑别忘了还有一类特殊的条件判断判空。JavaScript 里最常见的报错就是 Cannot read properties of undefined。为了防这种错很多人会写一连串的 守卫if (res res.data res.data.list res.data.list.length 0) { // 处理列表 }这种代码虽然安全但每个属性都要确认是否存在非常繁琐而且容易漏。ES2020 带来的可选链运算符 ?. 就是专门解决这个问题的if (res?.data?.list?.length 0) { // 处理列表 }?. 在左侧值为 null 或 undefined 时会直接短路返回 undefined而不会抛错。这行代码等价于之前那一长串 守卫但简洁了不止一倍。这里有个细节要特别注意?. 和 ?.()、?.[] 的用法。函数调用和数组/对象属性访问也能用可选链比如const result config?.getData?.() ?? []; const firstItem list?.[0] ?? {};在实际项目中我通常会把可选链和空值合并运算符 ?? 搭配使用一个保证“访问过程不报错”一个保证“访问结果有兜底”。这两兄弟基本可以替代过去一半以上的分散判空逻辑。5. 常见问题与排查技巧实录条件语句写的越多踩过的坑也越多。这一节我把自己在真实项目里遇到过的一些典型问题整理出来每个都附上排查思路希望能帮你少走点弯路。5.1 条件判断为何不生效类型与相等性的连环坑这是我在代码评审时最常挑出来的问题。很多人写相等判断时下意识用 结果因为 JavaScript 的类型转换机制产生了一堆匪夷所思的结果。比如0 // true 0 0 // true 0 // false null undefined // true这些结果背后是 ECMAScript 规范里一整套复杂的抽象相等比较算法。如果你不完全清楚规范写出来的条件判断就像在抽签。所以我的硬性建议是除了判断 null 或 undefined 同时存在以外一律使用 。想确认某个值是否为空也不要用 null而是明确写成 value null || value undefined或者用新语法 value null 并注释清楚意图。另一个高频坑是字符串与数字比较。后端接口偶尔会把数字字段传成字符串比如把 id 传成 1001这时如果用 比较就会不相等。正确的做法是在数据入口统一做类型转换而不是在条件判断里迁就const normalizedId typeof rawId string ? Number(rawId) : rawId; if (normalizedId targetId) { // 逻辑代码 }很多同事喜欢在比较时写 String(a) b 或者 Number(a) Number(b)虽然也能跑但每写一次就埋一条转换规则代码里到处是隐式上下文后面维护的人早晚会懵。5.2 条件覆盖不全边界值和处理分支遗漏条件语句最常见的 bug 不是写错而是漏写。比如一个评分功能大于等于 90 是优秀大于等于 60 是及格小于 60 是不及格。很多人会写if (score 90) { level 优秀; } else if (score 60) { level 及格; } else { level 不及格; }这段代码逻辑对但问题在于——score 可能是 NaN。如果某个接口返回了字符串或者没返回字段score 90 和 score 60 都是 false最终直接落到不及格。这个结果不一定合理。所以我写数值判断时会先做一个有效性校验if (typeof score ! number || Number.isNaN(score)) { level 未知; } else if (score 90) { level 优秀; } else if (score 60) { level 及格; } else { level 不及格; }边界值处理还包括“大于等于和大于”的区别。做活动时候判断用户是否在参与时间内如果开始时间精确到秒而结束时间也是精确到秒那么结束时间那一秒是否允许操作需要跟产品确认清楚。类似这些边界问题开发时不问清楚上线后就会变成线上客诉。5.3 性能与可维护性条件判断有没有可能拖慢程序绝大多数情况下条件语句的性能差异可以忽略不计。但有一种情况例外——使用了超长链式 if/else 或 switch 来处理超大范围的映射。比如一个城市编码映射表有几百个分支。这种情况下每次执行都要逐个比较几百次虽然单次耗时不多但高频调用聚合起来也够喝一壶。这种场景更适合从“条件判断”转换成“数据查找”。建一个对象或 Map把编码和值对应起来const cityMap new Map([ [110100, 北京市], [310100, 上海市], // ...几百条 ]); const cityName cityMap.get(cityCode) || 未知地区;Map 的查找性能在数据量大时明显优于线性比较。更重要的是数据维护彻底和逻辑分离开来即使后来城市列表增加几百条也不会影响主逻辑的一行代码。这又是“配置优于代码”思维的体现。还有一点是条件判断的顺序。把概率最高的分支放在最前面平均比较次数会下降。这在 if/else 链和 switch 里都适用。虽然收益不大但对于支付回调、接口路由这类高频路径积少成多还是有点意义的。5.4 排查条件语句问题的调试技巧遇到条件判断不生效的 bug我一般按三步排查。第一步确认判断的值到底是什么。很多人习惯在条件里直接打 log但变量在条件语句里的取值时机很关键。比如在异步回调里判断一个 state 值它可能还是旧值。我会先确认是不是闭包导致的变量引用问题。第二步单步跟一下比较过程的类型。用 console.table 或者打断点看变量的实际类型经常能发现字符串和数字不匹配的问题。如果页面直接显示 NaN那就要看上游数据有没有做转换。第三步考虑是不是有隐式转换干扰。例如判断一个输入框的值是否为空如果用户输入了空格那它不是 “假值”if (!value) 就不会拦下这个“看起来为空”的值。这种问题肉眼很难看出来我一般会写一个工具函数统一处理function isBlank(str) { return typeof str string str.trim().length 0; }然后所有表单校验统一走这个函数既避免了重复代码也防止每个人自己写一套规则造成行为不一致。6. 条件语句风格选择同一种需求用哪种写法更合适讲了这么多归根结底要解决的是一个问题面对实际需求手上有一堆条件语句的工具到底选哪个。我根据自己的实战经验整理了一份选择参考表贴在下面。场景特征推荐方案不推荐方案两三个分支逻辑简单三元运算符 或 if/elseswitch分支较多但针对同一个变量的离散值对象映射表 或 switch超长 if/else 链条件之间有优先级且需分别处理if/else 提前返回三元运算符嵌套多个条件必须同时满足组合条件 if (a b c)多层嵌套 if取值时需要防 undefined可选链 ?. 配合 ??超长 守卫映射数据量大且变动频繁配置表 / Map代码写死分支需要每个分支附带副作用if/else 或 switch 函数调用三元运算符这张表不是教条更多是我个人在代码审查和重构时的默认倾向。实际项目里翻车往往不是因为选错方案而是没想清楚条件的本质。就拿“是否需要副作用”这一点来说三元运算符的两个分支应该尽量是表达式不要塞一整块有副作用的语句。如果真有复杂操作老老实实写 if/else。另外提醒一下团队协作的项目里风格统一比个人技巧更重要。代码是写给机器跑的更是写给同事看的。如果你们团队约定用 if/else 不用三元那就跟着约定走。我个人比较折中简单赋值用三元流程控制用 if/else多分支映射优先对象表。我在实际开发中还有一个体会条件语句写得好的代码往往是“删”出来的。写完一段判断逻辑回头看看哪些分支可以合并哪些条件可以抽函数哪些判断根本不需要比如某个值永远会有兜底大刀阔斧删一轮剩下的才是核心逻辑。好的条件语句像高速公路方向清晰每条车道都笔直差的条件语句像老城区的巷子曲折多弯走进去容易出不来。最后分享一个小技巧每次写完条件判断试着把代码读给自己听一遍。如果你要用“如果……并且……但是……”这种句式才能解释清楚这段逻辑说明条件已经复杂到应该拆分了。反过来如果一句话能说清那代码大概率也是清晰的。这个标准很主观但非常好用我这些年一直靠它判断自己的代码是不是该重构了。