通过示例学习可读性强的 JavaScript:十次前后对比重构案例
十项针对 JavaScript 和 React 的小型代码优化,涵盖命名、参数对象、错误处理及格式化等方面,旨在让后续开发者更易于阅读和修改代码。
来源: 10 Clean Code Principles I Wish I Knew When I Started Coding
让某个功能正常运行只是工作的一半。另一半则是要确保团队成员,或者六个月后的你自己,能够读懂那段代码、安全地对其进行修改,且不会误解其含义。本文通过十个具体的 JavaScript 和 React 代码优化前后对比案例进行讲解,每个案例都说明了优化的理由以及该建议不再适用的情况。
1. 按照变量的实际含义为其命名
简短且晦涩的名称会迫使读者去其他地方查找相关信息。在这段代码中,没有任何提示能说明 d 是什么,a 又包含哪个属性。
const d = 30;
const x = users.filter(u => u.a);
为这两个变量重新命名后,其用途就一目了然了。该数字现在表示测试时长:
const trialPeriodDays = 30;
而这段过滤代码现在看起来就像是在描述活跃用户的情况:
const activeUsers = users.filter(user => user.isActive);
一个好的名称能够说明该值的含义;而像data这样的通用名称几乎无法传达任何信息。
const data = getData();
能够体现领域概念的名称能提供更多有用信息:
const customerOrders = getCustomerOrders();
如果觉得需要添加注释来解释某个变量,那通常意味着该名称本身就应该包含这些信息。
2. 为每个函数分配单一职责
一个同时负责验证输入、处理数据、写入数据库、发送邮件以及记录操作的函数很难理解,也难以测试,因为每次测试都必须同时处理所有这些功能。下图展示了将各项职责分别嵌入其中的函数结构。
function createUser(user) {
// validate user
// format data
// save to database
// send email
// log activity
}
将其拆分为有明确名称的步骤后,顶层函数便变成了易于理解的概要。它首先从验证开始:
function createUser(user) {
validateUser(user);
接着是格式化、保存以及发送欢迎邮件等操作,每项都由独立的函数负责:
const formattedUser = formatUser(user); saveUser(formattedUser); sendWelcomeEmail(formattedUser);
}
现在,createUser()看起来就像是一系列步骤的列表,而且每个辅助函数都可以单独进行测试。不过这样做会有间接性的缺点:需要了解详细内容的读者必须逐一查看每个辅助函数,因此这些函数的名称必须足够精确,以便尽量减少这种情况的发生。
3. 消除因相同原因而产生的重复代码
重复出现的逻辑是提取为独立函数的合适候选对象。此处相同的税率计算公式出现了三次。
const total1 = price1 + price1 * 0.15;
const total2 = price2 + price2 * 0.15;
const total3 = price3 + price3 * 0.15;
将其提取为函数后,该规则就有了明确的名称和统一的归属位置:
function calculatePriceWithTax(price) {
return price + price * 0.15;
}
之后,每个调用该函数的地方只需直接应用该规则即可:
const total1 = calculatePriceWithTax(price1);
const total2 = calculatePriceWithTax(price2);
const total3 = calculatePriceWithTax(price3);
如果比率从15%变为18%,只需修改一行代码,而无需在整个项目中逐一查找。关键是要消除有实际意义的重复代码,而非仅仅去除那些外观相似的代码行。目前完全相同但因不同原因会发生变化的两段代码最好保持分离;将它们合并会导致本应独立发展的部分相互关联。关于何时应该使用共享代码的文章深入探讨了这种权衡。
4. 用对象替代冗长的位置参数列表
位置参数在其被调用的地方会隐藏其含义。通过查看这样的调用方式,如果不打开函数定义,就无法理解25或true的具体意义。
createUser(
"Shayan",
"shayan@example.com",
25,
"developer",
true,
"Pakistan"
);
使用具有命名属性的单一对象,可以让每个值都具备自描述性:
createUser({
name: "Shayan",
email: "shayan@example.com",
age: 25,
role: "developer",
isActive: true,
country: "Pakistan"
});
这种风格在 API 数据、配置对象以及 React props 中早已成为标准,因此在大型应用中使用时会显得更为自然。
5. 优先选择清晰易懂的代码而非花哨复杂的代码
将逻辑压缩到一行中可能会增加阅读难度。下面的代码通过简写的属性名串联起了多个可选的调用。
const result = users?.filter(u => u.a)?.map(u => u.n);
将其拆分为两个步骤,并为每个步骤赋予描述性名称,这样就能让每一步的转换都一目了然。首先,筛选出活跃用户:
const activeUsers = users.filter(user => user.isActive);
然后,提取这些用户的姓名:
const activeUserNames = activeUsers.map(user => user.name);
虽然多了一行代码,但读者一眼就能理解。需要注意一个行为差异:原代码使用了可选链操作符,因此能够容忍users为null或undefined的情况,而重写后的代码在遇到这种情况时会抛出错误。如果users确实可能缺失,应明确处理该情况(例如使用默认值[]),而非通过一系列?.操作来隐藏问题。设计代码的准则应是让其清晰易懂,而非追求炫酷效果。
6. 考虑失败情况,而不仅仅是正常流程
实际系统总会出故障:API会返回错误,用户会提交无效输入,网络会中断,数据库也会离线。以下代码假设一切都会顺利进行。
const response = await fetch("/api/users");
const users = await response.json();
一个关键点是:fetch仅会在网络故障时拒绝请求;HTTP 404或500状态仍会成功处理。改进后的版本将请求包裹在try语句中:
try {
const response = await fetch("/api/users");
接着检查response.ok,若状态码表示失败则抛出异常,同时解析请求体并捕获所有可能出现的错误:
if (!response.ok) {
throw new Error("Failed to fetch users");
} const users = await response.json(); return users;
} catch (error) {
console.error("Unable to load users:", error);
}
这样就能让故障显而易见,也便于调试。需要注意的是,这里的catch块仅会记录日志然后隐式返回undefined,因此调用者无法区分故障与空结果。在实际代码中,应在记录日志后重新抛出异常,或返回调用者可以检查的明确值,以便在真正能够处理错误的地方进行处理。
7. 注释要说明原因,而非具体操作
注释很有用;多余的注释则是噪音。这条注释只是重复说明了该行代码的实际功能。
// Increment counter by 1
counter++;
好的注释能够解释代码本身无法表达的决策,比如为何要故意延迟请求:
// Delay the request to avoid triggering the API rate limit.
setTimeout(fetchUsers, 1000);
一条实用的原则:让代码说明它做了什么,用注释来说明为什么要这么做。
8. 避免在JSX中加入业务规则
在React组件中,很容易将过滤和计算操作直接写进标记代码中。这段渲染代码在遍历产品之前就内联应用了两个过滤条件。
<div>
{products
.filter(product => product.stock > 0)
.filter(product => product.price < 100)
.map(product => (
<ProductCard product={product} />
))}
</div>
将规则放入命名变量中,可以区分哪些产品是有效的以及这些产品该如何显示:
const availableProducts = products.filter(
product => product.stock > 0 && product.price < 100
);
这样,JSX就只负责描述界面结构了:
<div>
{availableProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
注意,重构后的版本还添加了key属性,而原始版本缺少这一属性,React需要它来正确追踪列表项。
9. 利用工具确保格式一致
在代码库中风格混杂之前,格式问题似乎并不重要。这里有三人以三种不同的方式声明同一个变量:驼峰式、
const userName = "Shayan";
下划线式,
const user_name = "Shayan";
以及无空格或分号的帕斯卡式:
const UserName="Shayan"
这三种写法都能正常运行,但格式不一致会在他人阅读或搜索代码时带来麻烦。像Prettier和ESLint这样的工具可以解决这个问题:Prettier会自动格式化代码,而ESLint则能强制遵循命名规范并标记常见错误。
10. 为后续维护者着想编写代码
这一原则将其他所有原则联系在一起。下一个打开你文件的人可能是新同事、团队成员,或是未来的自己,而未来的你已经忘记了今天所想的全部内容。
在提交代码之前,先思考:如果六个月后你重新打开这个文件,还能看懂吗?如果不能,那就进行简化——给容易混淆的变量重新命名,拆分过大的函数,删除因相同原因而重复存在的代码,并优化结构。其根本目的是减少理解代码所需的思考量。更多类似习惯的内容,请参阅让后续开发者更易修改代码的习惯。
总结
整洁的代码并不意味着完美的代码。由于截止日期、需求变动以及项目发展,每个团队都难免会写出一些杂乱的代码。重要的是方向:每一次修改都应该让代码变得更清晰、更简单、更易于维护。
- 变量名应能体现其代表的含义。