首页 / 文章

通过示例学习可读性强的 JavaScript:十次前后对比重构案例

十项针对 JavaScript 和 React 的小型代码优化,涵盖命名、参数对象、错误处理及格式化等方面,旨在让后续开发者更易于阅读和修改代码。

发布者:REACTAPP.TOP阅读约 7 分钟1583 词

来源: 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. 为后续维护者着想编写代码

这一原则将其他所有原则联系在一起。下一个打开你文件的人可能是新同事、团队成员,或是未来的自己,而未来的你已经忘记了今天所想的全部内容。

在提交代码之前,先思考:如果六个月后你重新打开这个文件,还能看懂吗?如果不能,那就进行简化——给容易混淆的变量重新命名,拆分过大的函数,删除因相同原因而重复存在的代码,并优化结构。其根本目的是减少理解代码所需的思考量。更多类似习惯的内容,请参阅让后续开发者更易修改代码的习惯。

总结

整洁的代码并不意味着完美的代码。由于截止日期、需求变动以及项目发展,每个团队都难免会写出一些杂乱的代码。重要的是方向:每一次修改都应该让代码变得更清晰、更简单、更易于维护。

  • 变量名应能体现其代表的含义。
  • 函数应仅承担一项功能,只有当存在相同的修改理由时才应消除重复代码。
  • 使用对象比冗长的位置参数列表更佳。
  • 可读性优于简洁性,但在重写时需保持行为一致。
  • 明确处理错误,并对决策过程进行注释而非具体实现细节。
  • 将业务规则置于标记语言之外,让工具负责格式处理。