ryanmcdermott/clean-code-javascript

GitHub Trending (daily) 工具

摘要

一本基于罗伯特·C·马丁《Clean Code》原则的指南,教你编写清洁、可读且易于维护的JavaScript代码,涵盖变量、函数、类、测试等内容。

为JavaScript适配的清洁代码概念
查看原文
查看缓存全文

缓存时间: 2026/07/02 11:33

ryanmcdermott/clean-code-javascript

来源:https://github.com/ryanmcdermott/clean-code-javascript

干净代码 JavaScript

目录

  1. 简介
  2. 变量
  3. 函数
  4. 对象与数据结构
  5. SOLID
  6. 测试
  7. 并发
  8. 错误处理
  9. 格式
  10. 注释
  11. 翻译

简介

软件质量评估的幽默图片:根据阅读代码时骂人的次数来衡量

软件工程原则来自 Robert C. Martin 的著作《Clean Code》(https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882),并针对 JavaScript 进行了改编。这不是一份风格指南,而是一份指导如何用 JavaScript 编写可读、可复用且可重构(https://github.com/ryanmcdermott/3rs-of-software-architecture)软件的指南。

并非每条原则都必须严格遵守,更不会得到所有人的一致认可。它们只是指导方针,但也是《Clean Code》作者多年集体经验所总结出来的。

我们的软件工程工艺才刚过 50 年,我们仍在学习大量东西。当软件架构像建筑学本身一样古老时,也许我们才会有更严格的规则可循。目前,让这些指南作为评估你和团队所编写的 JavaScript 代码质量的试金石吧。

还有一件事:知道这些不会立刻让你成为更好的软件开发者,而且与它们共事多年也不意味着你不会犯错。每段代码都始于初稿,就像湿粘土被塑造成最终形态。最后,我们通过同伴审查来雕琢掉瑕疵。不要因为初稿需要改进而自责,要改进的是代码本身!

变量

使用有意义的、可发音的变量名

差:

const yyyymmdstr = moment().format("YYYY/MM/DD");  

好:

const currentDate = moment().format("YYYY/MM/DD");  

⬆ 返回顶部

对同一类型的变量使用相同的词汇

差:

getUserInfo();  
getClientData();  
getCustomerRecord();  

好:

getUser();  

⬆ 返回顶部

使用可搜索的名称

我们读的代码会比写的多得多。确保我们写的代码可读且可搜索非常重要。如果变量名对理解程序不起作用,就会伤害读者。让你的名称易于搜索。像 buddy.js(https://github.com/danielstjules/buddy.js)和 ESLint(https://github.com/eslint/eslint/blob/660e0918933e6e7fede26bc675a0763a6b357c94/docs/rules/no-magic-numbers.md)这样的工具可以帮助识别未命名的常量。

差:

// 86400000 到底是干嘛的?  
setTimeout(blastOff, 86400000);  

好:

// 将它们声明为大写的命名常量  
const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;  
setTimeout(blastOff, MILLISECONDS_PER_DAY);  

⬆ 返回顶部

使用解释性变量

差:

const address = "One Infinite Loop, Cupertino 95014";  
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;  
saveCityZipCode(  
  address.match(cityZipCodeRegex)[1],  
  address.match(cityZipCodeRegex)[2]  
);  

好:

const address = "One Infinite Loop, Cupertino 95014";  
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;  
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];  
saveCityZipCode(city, zipCode);  

⬆ 返回顶部

避免心理映射

显式优于隐式。

差:

const locations = ["Austin", "New York", "San Francisco"];  
locations.forEach(l => {  
  doStuff();  
  doSomeOtherStuff();  
  // ...  
  // ...  
  // ...  
  // 等等,`l` 到底代表什么?  
  dispatch(l);  
});  

好:

const locations = ["Austin", "New York", "San Francisco"];  
locations.forEach(location => {  
  doStuff();  
  doSomeOtherStuff();  
  // ...  
  // ...  
  // ...  
  dispatch(location);  
});  

⬆ 返回顶部

不要添加不必要的上下文

如果你的类/对象名已经表明了含义,就不要在变量名中重复它。

差:

const Car = {  
  carMake: "Honda",  
  carModel: "Accord",  
  carColor: "Blue"  
};  

function paintCar(car, color) {  
  car.carColor = color;  
}  

好:

const Car = {  
  make: "Honda",  
  model: "Accord",  
  color: "Blue"  
};  

function paintCar(car, color) {  
  car.color = color;  
}  

⬆ 返回顶部

使用默认参数代替短路或条件判断

默认参数通常比短路更简洁。注意:如果你使用默认参数,函数只会在参数为 undefined 时提供默认值。其他“假值”如 ''""falsenull0NaN 不会被默认值替代。

差:

function createMicrobrewery(name) {  
  const breweryName = name || "Hipster Brew Co.";  
  // ...  
}  

好:

function createMicrobrewery(name = "Hipster Brew Co.") {  
  // ...  
}  

⬆ 返回顶部

函数

函数参数(理想情况下少于或等于 2 个)

限制函数参数的数量极为重要,因为它能让函数更易于测试。超过三个参数会导致组合爆炸,你需要针对每个独立参数测试大量不同的情况。理想情况是一个或两个参数,应尽量避免三个。超过三个应该合并。通常,如果你有两个以上参数,说明函数试图做的事情太多。如果并非如此,大多数情况下一个更上层的对象就足以作为参数。

由于 JavaScript 允许你在不编写大量类模板的情况下创建对象,当你发现自己需要大量参数时,可以使用一个对象。为了让函数期望的属性一目了然,你可以使用 ES2015/ES6 的解构语法。这有几个优点:

  1. 当有人查看函数签名时,可以立即清楚正在使用哪些属性。
  2. 它可以用来模拟命名参数。
  3. 解构还会克隆传递给函数的参数对象中的指定原始值。这有助于防止副作用。注意:从参数对象解构出来的对象和数组不会被克隆。
  4. 代码检查工具可以警告你未使用的属性,否则不可能做到这一点。

差:

function createMenu(title, body, buttonText, cancellable) {  
  // ...  
}  

createMenu("Foo", "Bar", "Baz", true);  

好:

function createMenu({ title, body, buttonText, cancellable }) {  
  // ...  
}  

createMenu({  
  title: "Foo",  
  body: "Bar",  
  buttonText: "Baz",  
  cancellable: true  
});  

⬆ 返回顶部

函数应该只做一件事

这绝对是软件工程中最重要的规则。当函数做多件事时,它们就更难组合、测试和推理。当你能将函数隔离到只做一个动作时,就能轻松重构,代码也会更清晰。如果你从本指南中只学到这一条,你已经领先许多开发者了。

差:

function emailClients(clients) {  
  clients.forEach(client => {  
    const clientRecord = database.lookup(client);  
    if (clientRecord.isActive()) {  
      email(client);  
    }  
  });  
}  

好:

function emailActiveClients(clients) {  
  clients.filter(isActiveClient).forEach(email);  
}  

function isActiveClient(client) {  
  const clientRecord = database.lookup(client);  
  return clientRecord.isActive();  
}  

⬆ 返回顶部

函数名应该说明它做什么

差:

function addToDate(date, month) {  
  // ...  
}  

const date = new Date();  
// 从函数名很难看出加了什么  
addToDate(date, 1);  

好:

function addMonthToDate(month, date) {  
  // ...  
}  

const date = new Date();  
addMonthToDate(1, date);  

⬆ 返回顶部

函数应该只包含一层抽象

当你有超过一层抽象时,函数通常做太多事情。拆解函数可以提高复用性并简化测试。

差:

function parseBetterJSAlternative(code) {  
  const REGEXES = [  
    // ...  
  ];  

  const statements = code.split(" ");  
  const tokens = [];  
  REGEXES.forEach(REGEX => {  
    statements.forEach(statement => {  
      // ...  
    });  
  });  

  const ast = [];  
  tokens.forEach(token => {  
    // lex...  
  });  

  ast.forEach(node => {  
    // parse...  
  });  
}  

好:

function parseBetterJSAlternative(code) {  
  const tokens = tokenize(code);  
  const syntaxTree = parse(tokens);  
  syntaxTree.forEach(node => {  
    // parse...  
  });  
}  

function tokenize(code) {  
  const REGEXES = [  
    // ...  
  ];  

  const statements = code.split(" ");  
  const tokens = [];  
  REGEXES.forEach(REGEX => {  
    statements.forEach(statement => {  
      tokens.push(/* ... */);  
    });  
  });  

  return tokens;  
}  

function parse(tokens) {  
  const syntaxTree = [];  
  tokens.forEach(token => {  
    syntaxTree.push(/* ... */);  
  });  

  return syntaxTree;  
}  

⬆ 返回顶部

消除重复代码

尽最大努力避免重复代码。重复代码很糟糕,因为如果你需要修改某个逻辑,就必须在多个地方改动。想象一下你经营一家餐馆,要跟踪库存:所有西红柿、洋葱、大蒜、香料等。如果你有多个清单记录这些,那么当你制作一道包含西红柿的菜肴时,所有清单都必须更新。如果你只有一个清单,那就只有一个地方需要更新!

通常,重复代码是由于两个或更多略有不同的事物,它们有很多共同点,但差异迫使你编写两个或更多做相同事情的独立函数。消除重复代码意味着创建一个抽象,能够用一个函数/模块/类处理这一组不同事物。正确抽象至关重要,这就是为什么你应该遵循《类》一节中阐述的 SOLID 原则。糟糕的抽象可能比重复代码更糟糕,所以要小心!话虽如此,如果你能做出好的抽象,那就去做吧!不要重复自己,否则每次想改一个东西时,你都会发现需要更新多个地方。

差:

function showDeveloperList(developers) {  
  developers.forEach(developer => {  
    const expectedSalary = developer.calculateExpectedSalary();  
    const experience = developer.getExperience();  
    const githubLink = developer.getGithubLink();  
    const data = {  
      expectedSalary,  
      experience,  
      githubLink  
    };  

    render(data);  
  });  
}  

function showManagerList(managers) {  
  managers.forEach(manager => {  
    const expectedSalary = manager.calculateExpectedSalary();  
    const experience = manager.getExperience();  
    const portfolio = manager.getMBAProjects();  
    const data = {  
      expectedSalary,  
      experience,  
      portfolio  
    };  

    render(data);  
  });  
}  

好:

function showEmployeeList(employees) {  
  employees.forEach(employee => {  
    const expectedSalary = employee.calculateExpectedSalary();  
    const experience = employee.getExperience();  
    const data = {  
      expectedSalary,  
      experience  
    };  

    switch (employee.type) {  
      case "manager":  
        data.portfolio = employee.getMBAProjects();  
        break;  
      case "developer":  
        data.githubLink = employee.getGithubLink();  
        break;  
    }  

    render(data);  
  });  
}  

⬆ 返回顶部

使用 Object.assign 设置默认对象

差:

const menuConfig = {  
  title: null,  
  body: "Bar",  
  buttonText: null,  
  cancellable: true  
};  

function createMenu(config) {  
  config.title = config.title || "Foo";  
  config.body = config.body || "Bar";  
  config.buttonText = config.buttonText || "Baz";  
  config.cancellable = config.cancellable !== undefined ? config.cancellable : true;  
}  

createMenu(menuConfig);  

好:

const menuConfig = {  
  title: "Order",  
  // 用户没有包含 'body' 键  
  buttonText: "Send",  
  cancellable: true  
};  

function createMenu(config) {  
  let finalConfig = Object.assign(  
    {  
      title: "Foo",  
      body: "Bar",  
      buttonText: "Baz",  
      cancellable: true  
    },  
    config  
  );  
  return finalConfig  
  // config 现在等于: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}  
  // ...  
}  

createMenu(menuConfig);  

⬆ 返回顶部

不要将标志作为函数参数

标志告诉用户这个函数做了不止一件事。函数应该只做一件事。如果函数根据布尔值走不同的代码路径,请拆分为多个函数。

差:

function createFile(name, temp) {  
  if (temp) {  
    fs.create(`./temp/${name}`);  
  } else {  
    fs.create(name);  
  }  
}  

好:

function createFile(name) {  
  fs.create(name);  
}  

function createTempFile(name) {  
  createFile(`./temp/${name}`);  
}  

⬆ 返回顶部

避免副作用(第一部分)

如果一个函数除了接收一个值并返回另一个值之外还做了其他事情,它就产生了副作用。副作用可能包括写入文件、修改全局变量,或者意外地把所有钱转给陌生人。当然,程序中有时确实需要副作用。就像前面的例子,你可能需要写入文件。你需要做的是集中处理副作用的位置。不要让多个函数和类写入同一个特定文件。用一个服务来做这件事,且只有一个。

主要目的是避免常见的陷阱,比如在没有任何结构的情况下在对象之间共享状态,使用任何东西都可以写入的可变数据类型,以及不集中处理副作用发生的位置。如果你能做到这一点,你会比绝大多数其他程序员更快乐。

差:

// 被后面函数引用的全局变量  
// 如果有另一个函数也使用了这个 name,它现在变成了数组,可能会导致问题  
let name = "Ryan McDermott";  

function splitIntoFirstAndLastName() {  
  name = name.split(" ");  
}  

splitIntoFirstAndLastName();  

console.log(name); // ['Ryan', 'McDermott'];  

好:

function splitIntoFirstAndLastName(name) {  
  return name.split(" ");  
}  

const name = "Ryan McDermott";  
const newName = splitIntoFirstAndLastName(name);  

console.log(name); // 'Ryan McDermott';  
console.log(newName); // ['Ryan', 'McDermott'];  

⬆ 返回顶部

避免副作用(第二部分)

在 JavaScript 中,有些值是不可变的,有些是可变的。对象和数组是两种可变值,所以当它们作为参数传递给函数时要小心处理。JavaScript 函数可以更改对象的属性或修改数组的内容,这可能轻易导致其他地方的 bug。

假设有一个函数接受一个表示购物车的数组参数。如果这个函数修改了购物车数组——例如添加一个要购买的商品——那么任何其他使用同一个 cart 数组的函数都会受到这个添加的影响。这可能很好,但也可能很糟糕。

设想一个糟糕的情况:用户点击“购买”按钮,调用一个 purchase 函数,该函数发起网络请求并将 cart 数组发送到服务器。由于网络连接不好,purchase 函数必须不断重试请求。现在,如果在网络请求开始之前,用户不小心点击了某个他们不想要的商品上的“添加到购物车”按钮,会发生什么?如果发生这种情况,然后网络请求开始了,那么购买函数就会发送那个……

(由于内容被截断,此处为原文结束。但根据上下文,下一条将继续阐述。)

相似文章

@vintcessun: 1个让无数团队头疼的问题终于有了解法。写JavaScript最怕代码一天后自己都看不懂,变量命名随性、函数又长又杂。这个项目把《Clean Code》的工程原则适配到JS,每个原则都配bad/good对比,告诉你为什么那样改。说白了,它解…

X AI KOLs Timeline

该项目将《Clean Code》的工程原则适配到JavaScript,提供每个原则的bad/good对比,帮助开发者写出可读、可复用、可重构的代码,解决团队协作中代码腐烂的问题。