“这很难,是因为它很复杂,还是因为对它不熟悉?熟悉度是接受更复杂代码的充分理由吗?”
发现一篇好文,看完之后写代码瞬间好理解了很多!!!翻译一下全文。
写不令人疑惑的 JavaScript 代码
原文地址:
这是关于降低 JavaScript 代码复杂性的一系列文章的第三部分。在以前的文章中,我们认为。这不是一个准确或全面的指标,但它可以成为一个有用的指南。然后,我们研究了。在这篇文章中,我们将注意力转向条件句。
不幸的是,我们不能完全摆脱条件句,这将意味着对大多数代码库进行彻底的重新设计(尽管技术上是可能的)。但是,我们可以改变我们写条件句的方式,使它们不那么复杂。我们将研究两种处理 if 语句的策略。之后,我们将把注意力转向转换语句。
没有 else 的 if 语句
重构条件句的第一种方法是去掉 else。我们写代码就好像 JavaScript 里面没有 else 语句一样。这似乎是一件奇怪的事情,但是大多数时候,我们根本不需要 else。
想象一下我们正在开发一个网站,我们通过AJAX加载。加载数据后,我们有一些用于呈现菜单的代码:
function renderMenu(menuData) { let menuHTML = ''; if ((menuData === null) || (!Array.isArray(menuData))) { menuHTML = ''; } else if (menuData.length === 0) { menuHTML = ' '; } else { menuHTML = ` `; } return menuHTML;}复制代码
这个代码可以工作。但是一旦我们确定没有要渲染的 notifications,再看其他语句有什么意义呢?为什么不直接 return menuHTML
呢?让我们重构一下,看看它是什么样子:
function renderMenu(menuData) { if ((menuData === null) || (!Array.isArray(menuData))) { return ''; } if (menuData.length === 0) { return ' '; } return ' ';}复制代码
所以,我们已经修改了代码,这样,如果我们碰到了一个 edge case
,我们就返回一些东西,然后离开那里。对于代码阅读者来说,如果你只关心这个 edge case
,就没有必要再阅读下去了。我们知道在 if 语句之后不可能有任何相关的代码。不需要小心翼翼的向下细看和检查。
这个代码的另一个好处是“主”路径(返回一个 list)已经降低了缩进级别。这使得更容易看出这是代码中预期的“常规”路径。if 语句用于处理主路径的【例外】。这使得我们代码的意图更加清晰。
这种不使用 else 的策略称之为“提前 return、总是 return”。总的来说,我发现它使代码更清晰,有时可以减少计算量。例如,在上一篇文章中,我们看了 find()
:
function find(predicate, arr) { for (let item of arr) { if (predicate(item)) { return item; } }}复制代码
在 find()
函数中,我们一找到要查找的项目,就返回退出循环,这使得代码更加高效。
- 早点返回,经常返回
- 去除 else 是一个好的开始,但是仍然会给我们留下很多缩进。更好的策略是采用三元运算符
不要害怕三元运算符
三元运算符名声不好,说它在降低代码可读性。三元运算符确实让代码难以阅读。但是,与传统的 if 语句相比,三元运算符有着巨大的优势。为了说明为什么我们必须深入研究 if 语句的作用。让我们来看一个例子:
let foo;if (bar === 'some value') { foo = baz;}else { foo = bar;}复制代码
这很简单。但是,如果我们将这些块包装在立即调用函数表达式( IIFEs )中会发生什么呢?
let foo;if (bar === 'some value') (function () { foo = baz;}())else (function () { foo = qux;}());复制代码
到目前为止,我们什么也没有改变,两个代码示例都做了同样的事情。但是请注意,IIFE 没有返回任何东西。这意味着它是不纯的。这是意料之中的,因为我们只是复制了原始的 if 语句。我们能把这些 IIFEs 函数重构为纯函数吗?事实上,我们不能。至少,每个块没有一个函数。我们不能这样做的原因是 if 语句不返回任何内容。,但是目前,我们必须接受这一点,除非我们早点 return,否则 if 语句也变得不纯。例如需要做点什么事,要么存个变量,要么在条件句里面产生一点副作用,除非我们可以早点 return。
如果我们将一个函数包装在整个 if 语句中会怎么样?我们可以让包装函数变得更纯吗?让我们试试。首先,我们将整个 if 语句包装在 IIFE 中:
let foo = null;(function () { if (bar === 'some value') { foo = baz; } else { foo = qux; }})();复制代码
然后我们通过将条件语句包在一个立即执行函数中返回结果:
let foo = (function () { if (bar === 'some value') { return baz; } else { return qux; }})();复制代码
这是一个改进,因为我们不再改变任何变量。我们的 LIFE 对 foo 一无所知。但是它仍然在访问其范围之外的变量: bar、baz 和 qux。让我们先处理 baz 和 qux。我们将使它们成为函数的参数(注意最后一行) :
let foo = (function (returnForTrue, returnForFalse) { if (bar === 'some value') { return returnForTrue; } else { return returnForFalse; }})(baz, qux);复制代码
最后,我们需要处理 bar。我们也可以把它作为一个变量传入,但是我们总是把它和“某个值”相比较。如果我们将整个条件语句作为一个参数,我们可以增加一点灵活性:
let foo = (function (returnForTrue, returnForFalse, condition) { if (condition) { return returnForTrue; } else { return returnForFalse; }})(baz, qux, (bar === 'some value'));复制代码
现在,我们可以独立地将我们的 function 移出(并且去掉了 else) :
function conditional(returnForTrue, returnForFalse, condition) { if (condition) { return returnForTrue; } return returnForFalse;}let foo = conditional(baz, qux, (bar === 'some value'));复制代码
我们做了什么?我们已经为设定值的 if 语句创建了一个抽象。如果我们愿意,我们可以用这种方式重构(几乎)所有的 if 语句,只要它们设置了一个值。因此,我们没有到处使用 if 语句,而是使用了纯函数调用。我们将删除一堆缩进并改进代码。
但是……我们并不真正需要有 conditional()
。我们已经有了三元运算符,它执行完全相同的操作:
let foo = (bar === 'some value') ? baz : qux;复制代码
三元运算符简洁,并内置于语言中。我们不需要编写或导入特殊函数来获得所有相同的能力。唯一真正的缺点是你不能真正使用curry()
和 compose()
搭配三元运算。所以,试试看。看看你是否可以用三元运算重构你的 if 语句。至少你将获得一个关于如何构造代码的新视角。
移除 switches
JavaScript 还有另一个条件结构,和 if 语句一样。switch 语句是另一种引入缩进和复杂性的控制结构。过一会儿,我们将研究如何编写没有 switch 的语句。但是首先,我想对他们说几句好话:
switch 语句是 JavaScript 中最接近模式匹配的东西。模式匹配是件好事。模式匹配是计算机科学家推荐我们使用的,而不是 if语句。因此,是可以很好地使用 switch 语句的。
switch 语句还允许您定义对多种情况的单个响应。这同样类似于其他语言中的模式匹配。在某些情况下,这可能非常方便。switch 语句也不总是不好的。
尽管这样,但在许多情况下,我们应该重构 switch 语句。让我们看一个例子,我们有三种不同类型的通知:
- 有人引用了他们写的一篇论文
- 有人开始“跟踪”他们的工作
- 有人在帖子中提到了他们
我们有不同的图标和文本格式,每种通知有不同的显示:
let notificationPtrn;switch (notification.type) { case 'citation': notificationPtrn = 'You received a citation from { {actingUser}}.'; break; case 'follow': notificationPtrn = '{ {actingUser}} started following your work'; break; case 'mention': notificationPtrn = '{ {actingUser}} mentioned you in a post.'; break; default: // Well, this should never happen}// Do something with notificationPtrn复制代码
switch 语句有点讨厌的一件事是,忘记一次 break
太容易了。但是如果我们把它变成一个函数,我们可以使用以前的“提前 return,经常 return”技巧。这意味着我们可以摆脱 break
语句:
function getnotificationPtrn(n) { switch (n.type) { case 'citation': return 'You received a citation from { {actingUser}}.'; case 'follow': return '{ {actingUser}} started following your work'; case 'mention': return '{ {actingUser}} mentioned you in a post.'; default: // Well, this should never happen }}let notificationPtrn = getNotificationPtrn(notification);复制代码
这好多了。我们现在有了一个纯函数,而不是改变一个变量。但是,我们也可以使用一个 plain ol' JavaScript object (POJO) 来获得相同的结果:
function getNotificationPtrn(n) { const textOptions = { citation: 'You received a citation from { {actingUser}}.', follow: '{ {actingUser}} started following your work', mention: '{ {actingUser}} mentioned you in a post.', } return textOptions[n.type];}复制代码
这产生了与 getnotificationPtrn
相同的结果。它更紧凑。但是这更简单吗?
我们所做的是用数据替换控制结构。这比听起来更重要。现在,如果我们愿意,我们可以让 TextOptions
成为 GetNotification()
的一个参数。例如:
const textOptions = { citation: 'You received a citation from { {actingUser}}.', follow: '{ {actingUser}} started following your work', mention: '{ {actingUser}} mentioned you in a post.',}function getNotificationPtrn(txtOptions, n) { return txtOptions[n.type];}const notificationPtrn = getNotificationPtrn(txtOptions, notification);复制代码
这可能不太有趣。但是现在考虑一下,TextOptions
是一个变量。这个变量不再需要硬编码。我们可以将它移动到 JSON 配置文件中,或者从服务器获取它。如果愿意,我们现在可以增加或者删除选项。我们可以合并不同的选项。这个版本中的缩进也少得多…
但是,您可能已经注意到,这些代码都没有处理我们未知类型的情况。在 switch 语句中,我们有 default 选项。如果遇到未知类型,我们可以用它来抛出错误。或者我们可以向用户返回一个的消息。例如:
function getNotificationPtrn(n) { switch (n.type) { case 'citation': return 'You received a citation from { {actingUser}}.'; case 'follow': return '{ {actingUser}} started following your work'; case 'mention': return '{ {actingUser}} mentioned you in a post.'; default: throw new Error('You’ve received some sort of notification we don’t know about.'; }}复制代码
我们处理了未知的情况,但是我们又使用了 switch 语句。我们能在 POJO 中处理这个问题吗?
一种选择是使用 if 语句:
function getNotificationPtrn(txtOptions, n) { if (typeof txtOptions[n.type] === 'undefined') { return 'You’ve received some sort of notification we don’t know about.'; } return txtOptions[n.type];}复制代码
但是我们正试图减少我们的 if 语句。所以这也不理想。相反,我们将利用 JavaScript 的松散类型,结合一些布尔逻辑。如果 OR 表达式的第一部分是错误的,JavaScript 将只检查第二部分(||)。如果在对象中找不到类型,则类型将是 undefined
的。JavaScript将把 undefined
解释为 false
。所以,我们像这样使用OR表达式:
function getNotificationPtrn(txtOptions, n) { return txtOptions[n.type] || 'You’ve received some sort of notification we don’t know about.';}复制代码
此外,我们也可以将默认值作为参数:
const defaultTxt = 'You’ve received some sort of notification we don’t know about.';function getNotificationPtrn(defaultTxt, txtOptions, n) { return txtOptions[n.type] || defaultTxt;}const notificationPtrn = getNotificationPtrn(defaultTxt, txtOptions, notification.type);复制代码
现在,这种方法比 switch
语句好吗?一如往常,答案是“这取决于...”。有些人可能会认为这个版本对于初学者来说很难阅读。这是一个合理的担忧。为了理解正在发生的事情,你必须了解 JavaScript 是如何强制值变成布尔值的。但是要问的问题是,“这很难,是因为它很复杂,还是因为对它不熟悉?熟悉度是接受更复杂代码的充分理由吗?“
但是可以减少代码的复杂度吗?让我们看看我们创建的最后一个函数。如果我们把它的名字改成更通用的名字(并调整最后一个参数)会怎么样?
function optionOrDefault(defaultOption, optionsObject, switchValue) { return optionsObject[switchValue] || defaultOption;}复制代码
然后,我们可以像这样构建 getNotificationPtrn
函数:
const dflt = 'You’ve received some sort of notification we don’t know about.';const textOptions = { citation: 'You received a citation from { {actingUser}}.', follow: '{ {actingUser}} started following your work', mention: '{ {actingUser}} mentioned you in a post.',}function getNotificationPtrn(notification) { return optionOrDefault(dflt, textOptions, notification.type);}复制代码
我们现在有一个非常明确的概念。文本选项和默认消息现在是纯数据。它们不再嵌入控制结构中。我们还有一个方便的函数 optionOrDefault()
,用于构建类似类型的构造。数据与选择显示哪个选项的任务完全分开。
当我们处理返回静态值时,这个模式很方便。根据我的经验,在大约 60-70% 的情况下,它可以取代 switch 语句。但是如果我们想做一些更有趣的事情呢?想象一下,如果我们的 options 对象包含函数而不是字符串,会发生什么?这篇文章已经太长了,所以我们不在这里深入讨论细节。但是这很值得考虑。
现在,像往常一样,小心使用你的大脑。OptionOrDefault()
这样的函数可以替换许多 switch
语句。但不是全部。在某些情况下,使用 switch
语句更有意义。没关系。
总结
重构条件比移除循环要更有用点。这部分是因为我们以许多不同的方式使用它们。然而,循环主要(但不总是)与数组一起使用。但是我们可以应用一些简单的模式来减少条件句之间的纠缠。它们包括:“提前 return”,“使用三元运算符”,以及“用对象替换 switch 语句”。这些不是银弹,而是用于对抗复杂情况的便利武器。