以卫语句取代嵌套条件表达式
以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)把“特殊情况”从层层嵌套的 if/else 里拎出来,在函数开头一个个检查、一旦命中就立刻返回,剩下的正常流程不再缩进。 它解决的痛是:函数的主要逻辑被埋在第三、第四层缩进里,读者得先读完所有例外,才找得到这个函数平时到底在干什么。
小郑负责公司薪资系统里发工资的那个函数。大多数员工按月薪加加班费发放,加班费按月薪折成时薪再乘 1.5。但有三种特殊情况:员工身故,发两个月月薪作为抚恤金;员工已离职,这个月不发(离职时已结清);员工已退休,也不发(退休金走另一套系统)。函数返回金额和原因,财务对账时要看原因。
"""薪资计算:重构前。"""from dataclasses import dataclass
@dataclassclass Employee: name: str is_dead: bool is_separated: bool is_retired: bool monthly_salary: float overtime_hours: float
@dataclassclass Pay: amount: float reason: str
def pay_amount(employee: Employee) -> Pay: if employee.is_dead: result = Pay(employee.monthly_salary * 2, "身故") else: if employee.is_separated: result = Pay(0.0, "离职") else: if employee.is_retired: result = Pay(0.0, "退休") else: hourly = employee.monthly_salary / 174 overtime = employee.overtime_hours * hourly * 1.5 result = Pay(employee.monthly_salary + overtime, "正常") return result
STAFF = [ Employee("张三", False, False, False, 8700, 10), Employee("李四", True, False, False, 6000, 0), Employee("王五", False, True, False, 7000, 0), Employee("赵六", False, False, True, 9000, 0),]
if __name__ == "__main__": for e in STAFF: pay = pay_amount(e) print(f"{e.name}:{pay.amount:.2f}({pay.reason})")
# 输出:# 张三:9450.00(正常)# 李四:12000.00(身故)# 王五:0.00(离职)# 赵六:0.00(退休)// 薪资计算:重构前。export interface Employee { name: string; isDead: boolean; isSeparated: boolean; isRetired: boolean; monthlySalary: number; overtimeHours: number;}
export interface Pay { amount: number; reason: string;}
export function payAmount(employee: Employee): Pay { let result: Pay; if (employee.isDead) { result = { amount: employee.monthlySalary * 2, reason: "身故" }; } else { if (employee.isSeparated) { result = { amount: 0, reason: "离职" }; } else { if (employee.isRetired) { result = { amount: 0, reason: "退休" }; } else { const hourly = employee.monthlySalary / 174; const overtime = employee.overtimeHours * hourly * 1.5; result = { amount: employee.monthlySalary + overtime, reason: "正常" }; } } } return result;}
function staff(name: string, flags: [boolean, boolean, boolean], salary: number, overtime: number): Employee { const [isDead, isSeparated, isRetired] = flags; return { name, isDead, isSeparated, isRetired, monthlySalary: salary, overtimeHours: overtime };}
export const STAFF: Employee[] = [ staff("张三", [false, false, false], 8700, 10), staff("李四", [true, false, false], 6000, 0), staff("王五", [false, true, false], 7000, 0), staff("赵六", [false, false, true], 9000, 0),];
for (const e of STAFF) { const pay = payAmount(e); console.log(`${e.name}:${pay.amount.toFixed(2)}(${pay.reason})`);}
// 输出:// 张三:9450.00(正常)// 李四:12000.00(身故)// 王五:0.00(离职)// 赵六:0.00(退休)测试覆盖正常发放和三种特殊情况,外加一个“既身故又已离职”的组合,确认身故优先:
import unittest
from target import STAFF, Employee, pay_amount
class PayAmountTest(unittest.TestCase): def test_normal_pay_includes_overtime(self): pay = pay_amount(STAFF[0]) self.assertAlmostEqual(pay.amount, 9450) self.assertEqual(pay.reason, "正常")
def test_special_cases(self): self.assertEqual([pay_amount(e).reason for e in STAFF[1:]], ["身故", "离职", "退休"]) self.assertAlmostEqual(pay_amount(STAFF[1]).amount, 12000)
def test_dead_takes_priority_over_separated(self): pay = pay_amount(Employee("钱七", True, True, False, 5000, 0)) self.assertEqual((pay.amount, pay.reason), (10000, "身故"))
if __name__ == "__main__": unittest.main()import test from "node:test";import assert from "node:assert/strict";import { STAFF, payAmount } from "./target";
test("正常发放含加班费", () => { assert.deepEqual(payAmount(STAFF[0]), { amount: 9450, reason: "正常" });});
test("三种特殊情况", () => { assert.deepEqual(STAFF.slice(1).map((e) => payAmount(e).reason), ["身故", "离职", "退休"]); assert.equal(payAmount(STAFF[1]).amount, 12000);});
test("身故优先于离职", () => { const e = { ...STAFF[0], isDead: true, isSeparated: true, monthlySalary: 5000 }; assert.deepEqual(payAmount(e), { amount: 10000, reason: "身故" });});如果现在要加一个需求:停薪留职的员工这个月不发工资,原因写“停薪留职”。
小郑要在三层 else 的最里面再套一层 if,正常发放的那三行代码被推到第五层缩进。他还得想清楚新的 if 该插在哪一层:放在退休的里面还是外面?每一层 else 都对应着前面的某个 if,要对齐括号或者缩进才能确认。Python 版里少缩进一格,代码照样能运行,只是逻辑变了;TS 版里漏一个花括号,编辑器会报错,但报错位置可能在函数末尾,离真正漏掉的地方很远。
读的问题更明显。这个函数最重要的事是“按月薪加加班费发工资”,它占了全公司 99% 的调用。可是读者打开函数,先看到的是身故、离职、退休,正常流程藏在最深处、最靠右的地方。三个特殊情况和正常情况在代码里地位相同,都是 if/else 的一个分支,可业务上它们根本不对等:特殊情况是“先排除掉”,正常情况才是“真正要做的事”。
还有那个 result 变量。每个分支给它赋值,最后统一 return result。读者看到某个分支给 result 赋了值,还得往下扫到函数末尾,确认中间没有别的代码再改它。这个函数里确实没有,但读者不读完就不敢确定。
用第 2 章的名字说,这是过长函数:行数不多,但嵌套让它的阅读成本成倍增加。每加一种特殊情况就多一层缩进,这个函数会越来越长、越来越深。
"""薪资计算:重构后。"""from dataclasses import dataclass
@dataclassclass Employee: name: str is_dead: bool is_separated: bool is_retired: bool monthly_salary: float overtime_hours: float
@dataclassclass Pay: amount: float reason: str
def pay_amount(employee: Employee) -> Pay: if employee.is_dead: return Pay(employee.monthly_salary * 2, "身故") if employee.is_separated: return Pay(0.0, "离职") if employee.is_retired: return Pay(0.0, "退休") hourly = employee.monthly_salary / 174 overtime = employee.overtime_hours * hourly * 1.5 return Pay(employee.monthly_salary + overtime, "正常")
STAFF = [ Employee("张三", False, False, False, 8700, 10), Employee("李四", True, False, False, 6000, 0), Employee("王五", False, True, False, 7000, 0), Employee("赵六", False, False, True, 9000, 0),]
if __name__ == "__main__": for e in STAFF: pay = pay_amount(e) print(f"{e.name}:{pay.amount:.2f}({pay.reason})")
# 输出:# 张三:9450.00(正常)# 李四:12000.00(身故)# 王五:0.00(离职)# 赵六:0.00(退休)// 薪资计算:重构后。export interface Employee { name: string; isDead: boolean; isSeparated: boolean; isRetired: boolean; monthlySalary: number; overtimeHours: number;}
export interface Pay { amount: number; reason: string;}
export function payAmount(employee: Employee): Pay { if (employee.isDead) return { amount: employee.monthlySalary * 2, reason: "身故" }; if (employee.isSeparated) return { amount: 0, reason: "离职" }; if (employee.isRetired) return { amount: 0, reason: "退休" }; const hourly = employee.monthlySalary / 174; const overtime = employee.overtimeHours * hourly * 1.5; return { amount: employee.monthlySalary + overtime, reason: "正常" };}
function staff(name: string, flags: [boolean, boolean, boolean], salary: number, overtime: number): Employee { const [isDead, isSeparated, isRetired] = flags; return { name, isDead, isSeparated, isRetired, monthlySalary: salary, overtimeHours: overtime };}
export const STAFF: Employee[] = [ staff("张三", [false, false, false], 8700, 10), staff("李四", [true, false, false], 6000, 0), staff("王五", [false, true, false], 7000, 0), staff("赵六", [false, false, true], 9000, 0),];
for (const e of STAFF) { const pay = payAmount(e); console.log(`${e.name}:${pay.amount.toFixed(2)}(${pay.reason})`);}
// 输出:// 张三:9450.00(正常)// 李四:12000.00(身故)// 王五:0.00(离职)// 赵六:0.00(退休)输出不变,三个测试照样通过,包括“身故优先于离职”那个:卫语句的先后顺序就是原来嵌套的由外到内。
停薪留职的需求,现在是在退休那行下面加一行 if employee.is_on_leave: return Pay(0.0, "停薪留职"),不碰任何缩进,也不用想它属于哪一层 else。
函数的形状现在和业务的形状一致:前三行是“先排除掉这些人”,后三行是“给其余的人发工资”。读者扫一眼就知道正常流程在哪:最后那几行,没有缩进。
result 变量消失了。每个卫语句命中就直接返回,读者看到 return 就知道这条路到此为止,不用再往下扫。
怎么一步步做
Section titled “怎么一步步做”每改一步跑一次测试。
- 找到最外层、最该先排除的那个特殊情况,这里是
is_dead。把它的分支改成直接return,然后把它的 else 去掉,else 里的代码整体往外缩一层。(IDE 可自动完成:PyCharm 和 VS Code 的部分扩展在 if 上提供 “Invert if condition” 或 “Remove redundant else” 一类的快速修复,但要逐个确认它的结果。) - 跑测试。
- 对下一层的特殊情况重复第 1 步,直到所有特殊情况都变成了函数开头的一行
if ...: return ...。 - 原来的
result变量现在只在最后一个分支里赋值、然后立刻返回,把它内联掉,直接return(第 5 章的内联变量,IDE 可自动完成)。 - 如果几个卫语句返回同样的结果、而且业务上是同一条规则,用第 36 章的合并条件表达式把它们并成一个。这里离职和退休都返回 0,但原因不同,所以不合并。
改的时候要守住一条:卫语句的先后顺序必须和原来嵌套的由外到内一致。原来身故在最外层,就意味着“既身故又离职”的人按身故处理;把身故挪到第二行,这个人就会拿到 0 元。本章测试里专门有一个用例防这件事。
另一种常见的形状是:嵌套的条件写的是“满足什么才做事”,而不是“什么时候不做事”。这时要先把条件反过来,再改成卫语句。下面是金融系统里算调整后资本的函数:资本为正、利率和期限都为正时才计算,否则为 0。
"""把条件反过来写成卫语句:重构前。"""from dataclasses import dataclass
@dataclassclass Instrument: capital: float interest_rate: float duration: int income: float adjustment_factor: float
def adjusted_capital(instrument: Instrument) -> float: result = 0.0 if instrument.capital > 0: if instrument.interest_rate > 0 and instrument.duration > 0: result = instrument.income / instrument.duration * instrument.adjustment_factor return result
if __name__ == "__main__": print(f"{adjusted_capital(Instrument(100000, 0.05, 12, 1200, 1.5)):.2f}") print(f"{adjusted_capital(Instrument(0, 0.05, 12, 1200, 1.5)):.2f}") print(f"{adjusted_capital(Instrument(100000, 0, 12, 1200, 1.5)):.2f}")
# 输出:# 150.00# 0.00# 0.00"""把条件反过来写成卫语句:重构后。"""from dataclasses import dataclass
@dataclassclass Instrument: capital: float interest_rate: float duration: int income: float adjustment_factor: float
def adjusted_capital(instrument: Instrument) -> float: if instrument.capital <= 0: return 0.0 if instrument.interest_rate <= 0 or instrument.duration <= 0: return 0.0 return instrument.income / instrument.duration * instrument.adjustment_factor
if __name__ == "__main__": print(f"{adjusted_capital(Instrument(100000, 0.05, 12, 1200, 1.5)):.2f}") print(f"{adjusted_capital(Instrument(0, 0.05, 12, 1200, 1.5)):.2f}") print(f"{adjusted_capital(Instrument(100000, 0, 12, 1200, 1.5)):.2f}")
# 输出:# 150.00# 0.00# 0.00// 把条件反过来写成卫语句:重构前。export interface Instrument { capital: number; interestRate: number; duration: number; income: number; adjustmentFactor: number;}
export function adjustedCapital(instrument: Instrument): number { let result = 0; if (instrument.capital > 0) { if (instrument.interestRate > 0 && instrument.duration > 0) { result = (instrument.income / instrument.duration) * instrument.adjustmentFactor; } } return result;}
const base: Instrument = { capital: 100000, interestRate: 0.05, duration: 12, income: 1200, adjustmentFactor: 1.5 };console.log(adjustedCapital(base).toFixed(2));console.log(adjustedCapital({ ...base, capital: 0 }).toFixed(2));console.log(adjustedCapital({ ...base, interestRate: 0 }).toFixed(2));
// 输出:// 150.00// 0.00// 0.00// 把条件反过来写成卫语句:重构后。export interface Instrument { capital: number; interestRate: number; duration: number; income: number; adjustmentFactor: number;}
export function adjustedCapital(instrument: Instrument): number { if (instrument.capital <= 0) return 0; if (instrument.interestRate <= 0 || instrument.duration <= 0) return 0; return (instrument.income / instrument.duration) * instrument.adjustmentFactor;}
const base: Instrument = { capital: 100000, interestRate: 0.05, duration: 12, income: 1200, adjustmentFactor: 1.5 };console.log(adjustedCapital(base).toFixed(2));console.log(adjustedCapital({ ...base, capital: 0 }).toFixed(2));console.log(adjustedCapital({ ...base, interestRate: 0 }).toFixed(2));
// 输出:// 150.00// 0.00// 0.00反转条件时,a > 0 变成 a <= 0,x and y 变成 not x or not y(德摩根定律)。稳妥的做法是先机械地写成 if not (instrument.capital > 0): return 0.0,跑过测试,再化简成 <= 0。这一步不是纯形式的:如果字段可能是 NaN,not (x > 0) 为真,x <= 0 却为假,化简就改变了行为。浮点数据来自外部输入时,宁可留着 not (...) 的写法。两个卫语句都返回 0,接着就可以用第 36 章把它们合成一个 if is_not_applicable(instrument): return 0.0。
定义与适用场景
Section titled “定义与适用场景”以卫语句取代嵌套条件表达式:如果某个条件极其罕见,就单独检查它,并在条件为真时立刻从函数返回。这种单独的检查叫卫语句(guard clause)。
条件语句有两种不同的用意。一种是两个分支同等重要,都是正常行为,比如夏季电价和平时电价,这时应该用 if/else,让两条路在代码里对称。另一种是一个分支是正常行为,另一个分支是罕见的例外,这时卫语句告诉读者:“这种情况很少见,遇到了就处理掉,别在它上面花心思。“嵌套条件把这两种用意混在了一起,所有分支看起来都一样重要。
它最常出现在两类地方。一是业务函数开头的资格检查:用户未登录、订单已取消、余额不足。二是 Web 框架的入口:中间件、视图、路由处理器先校验请求,不合格就返回 4xx,合格才进入业务逻辑。后者几乎是现代 Web 代码的标准写法,下面两个例子都是。
有人坚持“一个函数只能有一个出口”,认为多个 return 难以追踪。这条规则来自需要手动释放资源的年代,函数中途返回容易漏掉清理代码。今天 Python 有 with,TS 有 try/finally,资源释放不再依赖单一出口,而嵌套带来的阅读成本是实实在在的。判断标准是:函数是否更清楚了。
真实系统里的例子
Section titled “真实系统里的例子”Express 中间件的鉴权
Section titled “Express 中间件的鉴权”Express 的中间件接收 req、res、next,校验不通过就直接写响应,通过才调用 next() 交给下一个处理器。鉴权中间件天然就是一串卫语句:没带令牌、令牌无效、账号被封,各自返回不同的状态码。下面用简化的 Req、Res 和一个内存里的令牌表模拟,authV1 是嵌套写法,authV2 是卫语句写法。
// 模拟 Express 的鉴权中间件。interface Req { headers: Record<string, string>; user?: string;}
interface Res { status?: number; body?: string;}
type Next = () => void;
const TOKENS: Record<string, { user: string; active: boolean }> = { t1: { user: "alice", active: true }, t2: { user: "bob", active: false },};
// 嵌套写法:放行的那一行藏在最里面。function authV1(req: Req, res: Res, next: Next): void { const header = req.headers["authorization"]; if (header && header.startsWith("Bearer ")) { const account = TOKENS[header.slice(7)]; if (account) { if (account.active) { req.user = account.user; next(); } else { res.status = 403; res.body = "账号已停用"; } } else { res.status = 401; res.body = "令牌无效"; } } else { res.status = 401; res.body = "缺少令牌"; }}
// 卫语句写法:每种拒绝各占一行,放行在最后。function authV2(req: Req, res: Res, next: Next): void { const header = req.headers["authorization"]; if (!header || !header.startsWith("Bearer ")) { res.status = 401; res.body = "缺少令牌"; return; } const account = TOKENS[header.slice(7)]; if (!account) { res.status = 401; res.body = "令牌无效"; return; } if (!account.active) { res.status = 403; res.body = "账号已停用"; return; } req.user = account.user; next();}
function run(auth: typeof authV1, headers: Record<string, string>): string { const req: Req = { headers }; const res: Res = {}; let passed = false; auth(req, res, () => { passed = true; }); return passed ? `放行 ${req.user}` : `${res.status} ${res.body}`;}
const cases: Record<string, string>[] = [{}, { authorization: "Bearer xx" }, { authorization: "Bearer t2" }, { authorization: "Bearer t1" }];console.log(cases.every((h) => run(authV1, h) === run(authV2, h)));for (const h of cases) console.log(run(authV2, h));
// 输出:// true// 401 缺少令牌// 401 令牌无效// 403 账号已停用// 放行 alice真实的 Express 代码里,卫语句常写成 return res.status(401).send("缺少令牌"),一行完成响应和返回。忘写 return 是 Express 新手最常见的 bug 之一:响应发出去了,代码却继续往下走,最后又调用一次 next(),框架报“响应头已发送”。嵌套写法不会有这个问题,但代价是放行逻辑永远在最深处;卫语句写法要求每个卫语句都以返回结束,这也是本章 Python 版用 return Pay(...) 而不是先赋值的原因。
Python 视图的参数校验
Section titled “Python 视图的参数校验”Django、Flask、FastAPI 的视图函数都要先校验查询参数。下面用字典模拟请求参数、用元组模拟响应的状态码和内容,search_view_v1 是嵌套写法,search_view_v2 是卫语句写法。
"""模拟 Django 视图:搜索接口的参数校验。"""
ARTICLES = ["重构入门", "重构与测试", "设计模式导读"]
def search_view_v1(params: dict) -> tuple[int, str]: """嵌套写法。""" if "q" in params: q = params["q"].strip() if q: page = params.get("page", "1") if page.isdigit() and int(page) >= 1: hits = [a for a in ARTICLES if q in a] return 200, f"第 {page} 页:{'、'.join(hits) or '无结果'}" else: return 400, "page 必须是正整数" else: return 400, "q 不能为空" else: return 400, "缺少参数 q"
def search_view_v2(params: dict) -> tuple[int, str]: """卫语句写法。""" if "q" not in params: return 400, "缺少参数 q" q = params["q"].strip() if not q: return 400, "q 不能为空" page = params.get("page", "1") if not (page.isdigit() and int(page) >= 1): return 400, "page 必须是正整数" hits = [a for a in ARTICLES if q in a] return 200, f"第 {page} 页:{'、'.join(hits) or '无结果'}"
if __name__ == "__main__": cases = [{}, {"q": " "}, {"q": "重构", "page": "0"}, {"q": "重构"}] print(all(search_view_v1(c) == search_view_v2(c) for c in cases)) for c in cases: print(*search_view_v2(c))
# 输出:# True# 400 缺少参数 q# 400 q 不能为空# 400 page 必须是正整数# 200 第 1 页:重构入门、重构与测试嵌套写法里,错误信息和触发它的条件隔着整个 if 体:"缺少参数 q" 在最后一行,检查 "q" in params 在第一行,中间隔了十行。卫语句写法里,每个条件和它的错误信息紧挨着。注意 page 的校验用了 not (...) 包住原条件,而不是手工化简,这正是前面说的稳妥做法。FastAPI 更进一步,用类型注解让框架替你写这些卫语句,但框架做不了的业务校验,最后还是写成同样的形状。
什么时候别用
Section titled “什么时候别用”两个分支同等重要时别用。夏季电价和平时电价不是“例外和正常”的关系,写成 if is_summer: return summer_charge() 再接 return regular_charge() 也能跑,但它暗示平时电价才是“正常”的,夏季是个特例,这和业务不符。第 35 章用条件表达式写成一行,两条路对称,才是诚实的形状。
卫语句一多,函数开头会变成一长串 if。超过五六个时,说明这个函数在替太多种情况做决定。如果这些卫语句都在检查同一个对象的“类型”(这是哪种员工、哪种订单),该考虑第 38 章以多态取代条件表达式;如果它们都在检查“有没有这个对象”,该考虑第 39 章引入特例。
函数要在返回前做清理(关文件、释放锁、记审计日志)时,多个出口意味着每个出口都要记得清理。这时先用 with 或 try/finally 把清理收到一处,再改卫语句,否则就是在制造漏掉清理的机会。
容易混淆的手法
Section titled “容易混淆的手法”| 手法 | 它关心什么 | 以卫语句取代嵌套条件表达式关心什么 |
|---|---|---|
| 合并条件表达式(第 36 章) | 结果相同的几个条件合成一个 | 结果可以不同,只是让每种例外先返回;改完常接着合并 |
| 分解条件表达式(第 35 章) | 给条件和分支起名字,两条路仍然对称 | 打破对称,表明一条是例外、一条是主线 |
| 引入特例(第 39 章) | 消除散落在很多调用方的同一种检查 | 整理一个函数内部的多种检查 |
下面这个函数判断一个订单能不能发货,三层嵌套。把它改成卫语句。注意测试里的最后一个用例:未付款又已取消的订单,应当显示“等待付款”,卫语句的顺序要守住这一点。
"""练习:订单发货状态。"""from dataclasses import dataclass
@dataclassclass Order: paid: bool cancelled: bool address: str
def ship_status(order: Order) -> str: if order.paid: if not order.cancelled: if order.address: status = f"可发货:寄往{order.address}" else: status = "缺少收货地址" else: status = "订单已取消" else: status = "等待付款" return status
if __name__ == "__main__": print(ship_status(Order(True, False, "杭州"))) print(ship_status(Order(True, False, ""))) print(ship_status(Order(False, True, "杭州")))
# 输出:# 可发货:寄往杭州# 缺少收货地址# 等待付款import unittest
from target import Order, ship_status
class ShipStatusTest(unittest.TestCase): def test_ready(self): self.assertEqual(ship_status(Order(True, False, "杭州")), "可发货:寄往杭州")
def test_missing_address(self): self.assertEqual(ship_status(Order(True, False, "")), "缺少收货地址")
def test_cancelled(self): self.assertEqual(ship_status(Order(True, True, "杭州")), "订单已取消")
def test_unpaid_wins_over_cancelled(self): self.assertEqual(ship_status(Order(False, True, "杭州")), "等待付款")
if __name__ == "__main__": unittest.main()// 练习:订单发货状态。export interface Order { paid: boolean; cancelled: boolean; address: string;}
export function shipStatus(order: Order): string { let status: string; if (order.paid) { if (!order.cancelled) { if (order.address) { status = `可发货:寄往${order.address}`; } else { status = "缺少收货地址"; } } else { status = "订单已取消"; } } else { status = "等待付款"; } return status;}
console.log(shipStatus({ paid: true, cancelled: false, address: "杭州" }));console.log(shipStatus({ paid: true, cancelled: false, address: "" }));console.log(shipStatus({ paid: false, cancelled: true, address: "杭州" }));
// 输出:// 可发货:寄往杭州// 缺少收货地址// 等待付款import test from "node:test";import assert from "node:assert/strict";import { shipStatus } from "./target";
test("可以发货", () => { assert.equal(shipStatus({ paid: true, cancelled: false, address: "杭州" }), "可发货:寄往杭州");});
test("缺少地址", () => { assert.equal(shipStatus({ paid: true, cancelled: false, address: "" }), "缺少收货地址");});
test("已取消", () => { assert.equal(shipStatus({ paid: true, cancelled: true, address: "杭州" }), "订单已取消");});
test("未付款优先于已取消", () => { assert.equal(shipStatus({ paid: false, cancelled: true, address: "杭州" }), "等待付款");});- 条件语句有两种用意:两条路同等重要用 if/else,一条是罕见例外就用卫语句先返回。
- 卫语句让函数的形状和业务一致:先排除例外,再做正事,正常流程不缩进。
- 卫语句的先后顺序必须和原来嵌套的由外到内一致,否则多种例外同时成立时结果会变。
- 把“满足才做”反写成“不满足就返回”时,先机械地加
not (...),跑过测试再化简,留意NaN这类反转不对称的值。 - 卫语句返回相同结果时接着用合并条件表达式;卫语句太多、都在查类型或查空值时,看第 38、39 章。