ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
![](https://box.kancloud.cn/a8883896f37eb4357391bc3b1c76a851_781x260.jpeg) ``` function getTotalOutstandingAndSendBill() { const result = customer.invoices.reduce((total, each) => each.amount + total, 0); sendBill(); return result; } ``` ![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) ``` function totalOutstanding() { return customer.invoices.reduce((total, each) => each.amount + total, 0); } function sendBill() { emailGateway.send(formatBill(customer)); } ``` ### 动机 如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很有价值的东西。我可以任意调用这个函数,也可以把调用动作搬到调用函数的其他地方。这种函数的测试也更容易。简而言之,需要操心的事情少多了。 明确表现出“有副作用”与“无副作用”两种函数之间的差异,是个很好的想法。下面是一条好规则:任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离(Command-Query Separation)\[mf-cqs\]。有些程序员甚至将此作为一条必须遵守的规则。就像对待任何东西一样,我并不绝对遵守它,不过我总是尽量遵守,而它也回报我很好的效果。 如果遇到一个“既有返回值又有副作用”的函数,我就会试着将查询动作从修改动作中分离出来。 你也许已经注意到了:我使用“看得到的副作用”这种说法。有一种常见的优化办法是:将查询所得结果缓存于某个字段中,这样一来后续的重复查询就可以大大加快速度。虽然这种做法改变了对象中缓存的状态,但这一修改是察觉不到的,因为不论如何查询,总是获得相同结果。 ### 做法 - 复制整个函数,将其作为一个查询来命名。 > 如果想不出好名字,可以看看函数返回的是什么。查询的结果会被填入一个变量,这个变量的名字应该能对函数如何命名有所启发。 - 从新建的查询函数中去掉所有造成副作用的语句。 - 执行静态检查。 - 查找所有调用原函数的地方。如果调用处用到了该函数的返回值,就将其改为调用新建的查询函数,并在下面马上再调用一次原函数。每次修改之后都要测试。 - 从原函数中去掉返回值。 - 测试。 完成重构之后,查询函数与原函数之间常会有重复代码,可以做必要的清理。 ### 范例 有这样一个函数:它会遍历一份恶棍(miscreant)名单,检查一群人(people)里是否混进了恶棍。如果发现了恶棍,该函数会返回恶棍的名字,并拉响警报。如果人群中有多名恶棍,该函数也只汇报找出的第一名恶棍(我猜这就已经够了)。 ``` function alertForMiscreant (people) {  for (const p of people) {   if (p === "Don") {    setOffAlarms();    return "Don";   }   if (p === "John") {    setOffAlarms();    return "John";   }  }  return ""; } ``` 首先我复制整个函数,用它的查询部分功能为其命名。 ``` function findMiscreant (people) {  for (const p of people) {   if (p === "Don") {    setOffAlarms();    return "Don";   }   if (p === "John") {    setOffAlarms();    return "John";   }  }  return ""; } ``` 然后在新建的查询函数中去掉副作用。 ``` function findMiscreant (people) {  for (const p of people) {   if (p === "Don") {    setOffAlarms();    return "Don";   }   if (p === "John") {    setOffAlarms();    return "John";   }  }  return ""; } ``` 然后找到所有原函数的调用者,将其改为调用新建的查询函数,并在其后调用一次修改函数(也就是原函数)。于是代码 `const found = alertForMiscreant(people);`就变成了 ``` const found = findMiscreant(people); alertForMiscreant(people); ``` 现在可以从修改函数中去掉所有返回值了。 ``` function alertForMiscreant (people) {  for (const p of people) {   if (p === "Don") {    setOffAlarms();    return;   }   if (p === "John") {    setOffAlarms();    return;   }  }  return; } ``` 现在,原来的修改函数和新建的查询函数之间有大量的重复代码,我可以使用替换算法(195),让修改函数使用查询函数。 ``` function alertForMiscreant (people) { if (findMiscreant(people) !== "") setOffAlarms(); } ```