Clean JavaScript

372 阅读16分钟

一段优美规范的 javascript 代码,会令人心情愉悦,易于理解、维护和扩展;而遇到糟糕的代码,恨不得把那个他暴打一顿再删了重写。

接下来,本文将通过 bad 和 good 代码示例进行优良比对。

并不是每一个原则都必须严格遵守,更不会得到普遍认同,这只是指导方案。

变量 Variables

语义化名称 ✅

Bad:

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

Good:

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

CONST 常量 ✅

Bad:

// What the heck is 86400000 for?
setTimeout(blastOff, 86400000);

Good:

// Declare them as capitalized named constants.
const MILLISECONDS_IN_A_DAY = 60 * 60 * 24 * 1000; //86400000;

setTimeout(blastOff, MILLISECONDS_IN_A_DAY);

解释型变量 ✅

Bad:

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

Good:

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

// so we will know the two paramters means
saveCityZipCode(city, zipCode);

默认参数,而非短路求值 ✅

默认参数通常比短路求值(Short-circuit evaluation)更简洁。

好处是,如果使用默认参数,函数将只为未定义的参数提供默认值。其他 “falsy” 值(如''、""、false、null、0和NaN)将不会替换为默认值。

Bad:

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

Good:

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

避免冗余上下文 ❌

在 class/object 中,不必要再提及主体。

Bad:

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

function paintCar(car) {
  car.carColor = "Red";
}

Good:

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

function paintCar(car) {
  car.color = "Red";
}

函数 Functions

参数少于 3 位 ✅

限制函数参数的数量是非常重要的,这样测试起来更容易。有 3 个以上的参数函数需要独立对它进行测试,这很糟糕。

理想情况是 1 到 2 位参数。若你的函数需更多参数,在大多数情况下,一个对象作为参数就足够了。

Bad:

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

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

Good:

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

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

职责单一 ✅

这是目前软件开发中最重要的规则。如果函数多个职责,那么它很难复用、测试和推断;单一职责的函数清晰且容易重构。

Bad:

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

Good:

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

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

顾名思义 ✅

Bad:

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

const date = new Date();

// It's hard to tell from the function name what is added
addToDate(date, 1);

Good:

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

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

Object.assign 设置默认对象值 ✅

Bad:

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);

Good:

const menuConfig = {
  title: "Order",
  // User did not include 'body' key
  buttonText: "Send",
  cancellable: true
};

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

createMenu(menuConfig);

函数式编程 ✅

函数式编程更容易理解和测试。

Bad:

const programmerOutput = [
  {
    name: "Uncle Bobby",
    linesOfCode: 500
  },
  {
    name: "Suzie Q",
    linesOfCode: 1500
  },
  {
    name: "Jimmy Gosling",
    linesOfCode: 150
  },
  {
    name: "Gracie Hopper",
    linesOfCode: 1000
  }
];

let totalOutput = 0;

for (let i = 0; i < programmerOutput.length; i++) {
  totalOutput += programmerOutput[i].linesOfCode;
}

Good:

const programmerOutput = [
  {
    name: "Uncle Bobby",
    linesOfCode: 500
  },
  {
    name: "Suzie Q",
    linesOfCode: 1500
  },
  {
    name: "Jimmy Gosling",
    linesOfCode: 150
  },
  {
    name: "Gracie Hopper",
    linesOfCode: 1000
  }
];

const totalOutput = programmerOutput.reduce(
  (totalLines, output) => totalLines + output.linesOfCode,
  0
);

避免重复代码 ❌

代码重复意味着,如果改动一处逻辑,那么所有重复处都需要改变。我们应将它们很好地抽象出来。

Bad:

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);
  });
}

Good:

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);
  });
}

避免 flag 作为参数 ❌

如果你的函数通过 flag 做了不止一件事,这是不好的。若 flag 为 boolean 值,那么请将它们拆开。

Bad:

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

Good:

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

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

避免副作用 ❌

如果函数除了接收参数、返回值外,还进行了其他操作就会产生副作用。

一个副作用可能是,修改一些全局变量、入参,或者不小心把你所有的钱都汇给了一个陌生人。

有两点:

  1. 在某些情况下,你可能真的想要修改输入对象,但是当你采用这种编程实践时,您会发现这些情况非常罕见。大多数东西都可以重构,没有副作用!

  2. 克隆大对象在性能方面可能非常昂贵。幸运的是,这在实践中并不是一个大问题,因为有一些很棒的库允许这种编程方法速度更快,并且不像手动克隆对象和数组那样占用内存。

Bad:

const addItemToCart = (cart, item) => {
  cart.push({ item, date: Date.now() });
};

Good:

const addItemToCart = (cart, item) => {
  return [...cart, { item, date: Date.now() }];
};

避免全局函数 ❌

在 JavaScript 中,污染 global 是一种不好的做法,因为你可能会与另一个库发生冲突,并且库的 API 使用用户在开发中遇到异常之前不会想到。

让我们思考一个例子:如果你想扩展 JavaScript 的原生数组方法,使其具有可以显示两个数组之间差异的 diff 方法,该怎么办?你可以将diff 函数写入 Array.prototype,但它可能会与另一个试图做同样事情的库发生冲突。如果另一个库只是使用 diff 来查找数组的第一个元素和最后一个元素之间的差异呢?这就是为什么只使用 ES2015/ES6 class 简单地扩展原生数组会很好。

Bad:

Array.prototype.diff = function diff(comparisonArray) {
  const hash = new Set(comparisonArray);
  return this.filter(elem => !hash.has(elem));
};

Good:

class SuperArray extends Array {
  diff(comparisonArray) {
    const hash = new Set(comparisonArray);
    return this.filter(elem => !hash.has(elem));
  }
}

避免条件非判断 ❌

Bad:

function isDOMNodeNotPresent(node) {
  // ...
}

if (!isDOMNodeNotPresent(node)) {
  // ...
}

Good:

function isDOMNodePresent(node) {
  // ...
}

if (isDOMNodePresent(node)) {
  // ...
}

对象和数据结构 Objects and Data Structures

使用 getters 和 setters ✅

使用 getter 和 setter 来访问对象上的数据可能比简单地查找对象上的属性要好:

  • get 统一处理返回结果,调整更方便。
  • set 值时,可以方便添加验证。
  • 内部封装功能实现。
  • getset 时,容易捕获日志和错误。
  • 可实现异步获取。

Bad:

function makeBankAccount() {
  // ...

  return {
    balance: 0
    // ...
  };
}

const account = makeBankAccount();
account.balance = 100;

Good:

function makeBankAccount() {
  // this one is private
  let balance = 0;

  // a "getter", made public via the returned object below
  function getBalance() {
    return balance;
  }

  // a "setter", made public via the returned object below
  function setBalance(amount) {
    // ... validate before updating the balance
    balance = amount;
  }

  return {
    // ...
    getBalance,
    setBalance
  };
}

const account = makeBankAccount();
account.setBalance(100);

object 私有成员 ✅

这可以实现闭包。

Bad:

const Employee = function(name) {
  this.name = name;
};

Employee.prototype.getName = function getName() {
  return this.name;
};

const employee = new Employee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined

Good:

function makeEmployee(name) {
  return {
    getName() {
      return name;
    }
  };
}

const employee = makeEmployee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe

类 Class

ES6 class 替代 ES5 function ✅

ES6 的类清晰简单;ES5 类的继承、定义等实现起来代码会很复杂。

Bad:

const Animal = function(age) {
  if (!(this instanceof Animal)) {
    throw new Error("Instantiate Animal with `new`");
  }

  this.age = age;
};

Animal.prototype.move = function move() {};

const Mammal = function(age, furColor) {
  if (!(this instanceof Mammal)) {
    throw new Error("Instantiate Mammal with `new`");
  }

  Animal.call(this, age);
  this.furColor = furColor;
};

Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};

const Human = function(age, furColor, languageSpoken) {
  if (!(this instanceof Human)) {
    throw new Error("Instantiate Human with `new`");
  }

  Mammal.call(this, age, furColor);
  this.languageSpoken = languageSpoken;
};

Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};

Good:

class Animal {
  constructor(age) {
    this.age = age;
  }

  move() {
    /* ... */
  }
}

class Mammal extends Animal {
  constructor(age, furColor) {
    super(age);
    this.furColor = furColor;
  }

  liveBirth() {
    /* ... */
  }
}

class Human extends Mammal {
  constructor(age, furColor, languageSpoken) {
    super(age, furColor);
    this.languageSpoken = languageSpoken;
  }

  speak() {
    /* ... */
  }
}

使用方法链 ✅

Bad:

class Car {
  constructor(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
  }

  setMake(make) {
    this.make = make;
  }

  setModel(model) {
    this.model = model;
  }

  setColor(color) {
    this.color = color;
  }

  save() {
    console.log(this.make, this.model, this.color);
  }
}

const car = new Car("Ford", "F-150", "red");
car.setColor("pink");
car.save();

Good:

class Car {
  constructor(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
  }

  setMake(make) {
    this.make = make;
    // NOTE: Returning this for chaining
    return this;
  }

  setModel(model) {
    this.model = model;
    // NOTE: Returning this for chaining
    return this;
  }

  setColor(color) {
    this.color = color;
    // NOTE: Returning this for chaining
    return this;
  }

  save() {
    console.log(this.make, this.model, this.color);
    // NOTE: Returning this for chaining
    return this;
  }
}

const car = new Car("Ford", "F-150", "red").setColor("pink").save();

组合优于继承 ✅

你可能会想,“那我这~,应该在什么时候使用继承?”,这取决于你面对的问题,下面给个列表,这时继承比组合更好:

  1. 你的继承表示 is-a 关系,而不是 has-a 关系 (Human->Animal vs User->UserDetails)。
  2. 你可以重用基类代码(人可以像所有动物一样移动)。
  3. 要通过更改基类对派生类进行全局更改(改变所有动物运动时的热量消耗)。

Bad:

class Employee {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  // ...
}

// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
  constructor(ssn, salary) {
    super();
    this.ssn = ssn;
    this.salary = salary;
  }

  // ...
}

Good:

class EmployeeTaxData {
  constructor(ssn, salary) {
    this.ssn = ssn;
    this.salary = salary;
  }

  // ...
}

class Employee {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  setTaxData(ssn, salary) {
    this.taxData = new EmployeeTaxData(ssn, salary);
  }
  // ...
}

面向对象 SOLID

单一功能原则 Single Responsibility Principle (SRP)

正如 Clean Code 中所述,“一个类改变的原因永远不要超过一个”。把一个有很多功能的类挤在一起是很诱人的,比如当你只能带一个行李箱上飞机的时候。这样做的问题是,你的类在概念上不是内聚的,它很可能改变。尽量减少更改类的时间非常重要。这一点很重要,因为如果一个类中有太多的功能,而你修改了其中的一部分,那么很难想象这将如何影响代码库中的其他依赖模块。

Bad:

class UserSettings {
  constructor(user) {
    this.user = user;
  }

  changeSettings(settings) {
    if (this.verifyCredentials()) {
      // ...
    }
  }

  verifyCredentials() {
    // ...
  }
}

Good:

class UserAuth {
  constructor(user) {
    this.user = user;
  }

  verifyCredentials() {
    // ...
  }
}

class UserSettings {
  constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
  }

  changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
      // ...
    }
  }
}

开闭原则 Open/Closed Principle (OCP)

正如 Bertrand Meyer 所说:“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。” 这是什么意思呢?这个原则基本上是说,你应该允许用户在不更改现有代码的情况下添加新功能。

Bad:

class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = "ajaxAdapter";
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = "nodeAdapter";
  }
}

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    if (this.adapter.name === "ajaxAdapter") {
      return makeAjaxCall(url).then(response => {
        // transform response and return
      });
    } else if (this.adapter.name === "nodeAdapter") {
      return makeHttpCall(url).then(response => {
        // transform response and return
      });
    }
  }
}

function makeAjaxCall(url) {
  // request and return promise
}

function makeHttpCall(url) {
  // request and return promise
}

Good:

class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = "ajaxAdapter";
  }

  request(url) {
    // request and return promise
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = "nodeAdapter";
  }

  request(url) {
    // request and return promise
  }
}

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    return this.adapter.request(url).then(response => {
      // transform response and return
    });
  }
}

里氏替换原则 Liskov Substitution Principle (LSP)

派生类(子类)中的对象可以在程序中代替其基类(超类)中的对象。

对此的最佳解释是,如果你有一个父类和一个子类,那么父类和子类的同一功能调用,结果一致。这可能令人困惑,所以让我们看一下经典的正方形-矩形例子。从数学上讲,一个正方形就是一个矩形,但是如果你通过继承使用 is-a 关系来建模它,你会发现这是错误的。

Bad 示例中,setWidth 与 setHeight 实现不一致,最终导致结果不一致。

Bad:

class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }

  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = width;
  }

  setHeight(height) {
    this.width = height;
    this.height = height;
  }
}

function renderLargeRectangles(rectangles) {
  rectangles.forEach(rectangle => {
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
    rectangle.render(area);
  });
}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

Good:

class Shape {
  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(length) {
    super();
    this.length = length;
  }

  getArea() {
    return this.length * this.length;
  }
}

function renderLargeShapes(shapes) {
  shapes.forEach(shape => {
    const area = shape.getArea();
    shape.render(area);
  });
}

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

接口隔离原则 Interface Segregation Principle (ISP)

JavaScript 没有接口,所以这个原则不像其他原则那么严格。然而,即使 JavaScript 缺少类型系统,这一点也很重要。

ISP 声明:“客户(client)不应被迫使用对其而言无用的方法或功能。” 由于 duck typing(鸭子类型) 的存在,接口在 JavaScript 中是隐式契约。

Bad:

class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.settings.animationModule.setup();
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName("body"),
  animationModule() {} // Most of the time, we won't need to animate when traversing.
  // ...
});

Good:

class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.options = settings.options;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.setupOptions();
  }

  setupOptions() {
    if (this.options.animationModule) {
      // ...
    }
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName("body"),
  options: {
    animationModule() {}
  }
});

依赖反转原则 Dependency Inversion Principle (DIP)

该原则规定:

  1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
  2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

一开始可能有点难理解,但如果你使用过 AngularJS,你就会看到依赖注入(Dependency Injection, DI)的形式实现了这个原则。尽管它们不是相同的概念,DIP 防止高级模块了解其低级模块的实现并设置它们。它可以通过 DI 实现这一点。这样做的一个巨大好处是它减少了模块之间的耦合。耦合是一种非常糟糕的开发模式,因为它使代码难以重构。

Bad:

class InventoryRequester {
  constructor() {
    this.REQ_METHODS = ["HTTP"];
  }

  requestItem(item) {
    // ...
  }
}

class InventoryTracker {
  constructor(items) {
    this.items = items;

    // BAD: We have created a dependency on a specific request implementation.
    // We should just have requestItems depend on a request method: `request`
    this.requester = new InventoryRequester();
  }

  requestItems() {
    this.items.forEach(item => {
      this.requester.requestItem(item);
    });
  }
}

const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();

Good:

class InventoryTracker {
  constructor(items, requester) {
    this.items = items;
    this.requester = requester;
  }

  requestItems() {
    this.items.forEach(item => {
      this.requester.requestItem(item);
    });
  }
}

class InventoryRequesterV1 {
  constructor() {
    this.REQ_METHODS = ["HTTP"];
  }

  requestItem(item) {
    // ...
  }
}

class InventoryRequesterV2 {
  constructor() {
    this.REQ_METHODS = ["WS"];
  }

  requestItem(item) {
    // ...
  }
}

// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
  ["apples", "bananas"],
  new InventoryRequesterV2()
);
inventoryTracker.requestItems();

测试 Testing

测试比发布更重要。如果你没有测试或测试不足,那么每次你发布代码时都不能确定你没有破坏任何东西。决定什么构成足够的测试量是由你的团队决定的,但是拥有 100% 的覆盖率(所有的声明和分支)是你获得高度自信和开发人员安心的方式。这意味着除了拥有一个优秀的测试框架外,您还需要使用一个良好的覆盖工具。

没有理由不编写测试用例。有很多好的 JS 测试框架,所以找一个你的团队喜欢的。当你找到一个适合你的团队的方法时,你的目标就是为你引入的每个新特性/模块编写测试用例。如果你的首选方法是测试驱动开发(TDD),那很好,但重点是要确保在启动任何功能或重构现有功能之前,你已经达到了覆盖目标。

单一概念测试 ✅

Bad:

import assert from "assert";

describe("MomentJS", () => {
  it("handles date boundaries", () => {
    let date;

    date = new MomentJS("1/1/2015");
    date.addDays(30);
    assert.equal("1/31/2015", date);

    date = new MomentJS("2/1/2016");
    date.addDays(28);
    assert.equal("02/29/2016", date);

    date = new MomentJS("2/1/2015");
    date.addDays(28);
    assert.equal("03/01/2015", date);
  });
});

Good:

import assert from "assert";

describe("MomentJS", () => {
  it("handles 30-day months", () => {
    const date = new MomentJS("1/1/2015");
    date.addDays(30);
    assert.equal("1/31/2015", date);
  });

  it("handles leap year", () => {
    const date = new MomentJS("2/1/2016");
    date.addDays(28);
    assert.equal("02/29/2016", date);
  });

  it("handles non-leap year", () => {
    const date = new MomentJS("2/1/2015");
    date.addDays(28);
    assert.equal("03/01/2015", date);
  });
});

并发 Concurrency

使用 Async/Await > Promise > callback ✅

callback 会导致回调地狱!Promise 需要 then。

Bad:

import { get } from "request";
import { writeFile } from "fs";

get(
  "https://en.wikipedia.org/wiki/Robert_Cecil_Martin",
  (requestErr, response, body) => {
    if (requestErr) {
      console.error(requestErr);
    } else {
      writeFile("article.html", body, writeErr => {
        if (writeErr) {
          console.error(writeErr);
        } else {
          console.log("File written");
        }
      });
    }
  }
);

Good:

import { get } from "request-promise";
import { writeFile } from "fs-extra";

get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
  .then(body => {
    return writeFile("article.html", body);
  })
  .then(() => {
    console.log("File written");
  })
  .catch(err => {
    console.error(err);
  });

Great:

import { get } from "request-promise";
import { writeFile } from "fs-extra";

async function getCleanCodeArticle() {
  try {
    const body = await get(
      "https://en.wikipedia.org/wiki/Robert_Cecil_Martin"
    );
    await writeFile("article.html", body);
    console.log("File written");
  } catch (err) {
    console.error(err);
  }
}

getCleanCodeArticle()

异常处理 Error Handling

抛出异常很不错!它们意味着运行时已经成功地识别出程序中什么时候出现了问题,它通过停止当前堆栈上的函数执行、终止进程(在Node中)并在控制台上用堆栈跟踪来通知你。

不要忽略捕获异常 ❌

对捕获的异常什么都不做,那么对你排查与修复并没有帮助。将错误记录到控制台(console.log)也好不到哪里去,因为它经常会在控制台的大量信息中丢失。如果你在 try/catch 中包装了任何代码,这意味着你认为可能会在那里发生错误,因此你应该有一个计划,或创建一个代码路径,以便在错误发生时排查定位使用。

Bad:

try {
  functionThatMightThrow();
} catch (error) {
  console.log(error);
}

Good:

try {
  functionThatMightThrow();
} catch (error) {
  // One option (more noisy than console.log):
  console.error(error);
  // Another option:
  notifyUserOfError(error);
  // Another option:
  reportErrorToService(error);
  // OR do all three!
}

不要忽略 promise 的 rejected ❌

与上面提到的 try/catch 原因一致。

Bad:

getdata()
  .then(data => {
    functionThatMightThrow(data);
  })
  .catch(error => {
    console.log(error);
  });

Good:

getdata()
  .then(data => {
    functionThatMightThrow(data);
  })
  .catch(error => {
    // One option (more noisy than console.log):
    console.error(error);
    // Another option:
    notifyUserOfError(error);
    // Another option:
    reportErrorToService(error);
    // OR do all three!
  });

格式 Formatting

代码格式是主观的。与本文的许多规则一样,不需必须遵守的硬性规则。重点是不要争论格式。有大量的工具可以实现自动化;重点是格式风格统一。

对于那些不属于自动格式化范围的东西(缩进、制表符与空格、双引号与单引号等等),可以在这里找到一些指导建议。

一致的大写规则 ✅

JavaScript 是无类型的,所以大写可以告诉你很多关于变量、函数等的信息。这些规则是主观的,所以你的团队可以选择任何他们想要的。关键是,无论你们选择什么,都要始终如一。

Bad:

const DAYS_IN_WEEK = 7;
const daysInMonth = 30;

const songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const Artists = ["ACDC", "Led Zeppelin", "The Beatles"];

function eraseDatabase() {}
function restore_database() {}

class animal {}
class Alpaca {}

Good:

const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;

const SONGS = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const ARTISTS = ["ACDC", "Led Zeppelin", "The Beatles"];

function eraseDatabase() {}
function restoreDatabase() {}

class Animal {}
class Alpaca {}

函数定义与调用位置接近 ✅

如果一个函数调用另一个函数,在源文件中保持这些函数垂直关闭。理想情况下,让函数定义处于函数调用的正上方。我们倾向于自上而下地阅读代码,就像阅读报纸一样。 因此,让你的代码以这种方式读取。

Bad:

class PerformanceReview {
  constructor(employee) {
    this.employee = employee;
  }

  lookupPeers() {
    return db.lookup(this.employee, "peers");
  }

  lookupManager() {
    return db.lookup(this.employee, "manager");
  }

  getPeerReviews() {
    const peers = this.lookupPeers();
    // ...
  }

  perfReview() {
    this.getPeerReviews();
    this.getManagerReview();
    this.getSelfReview();
  }

  getManagerReview() {
    const manager = this.lookupManager();
  }

  getSelfReview() {
    // ...
  }
}

const review = new PerformanceReview(employee);
review.perfReview();

Good:

class PerformanceReview {
  constructor(employee) {
    this.employee = employee;
  }

  perfReview() {
    this.getPeerReviews();
    this.getManagerReview();
    this.getSelfReview();
  }

  getPeerReviews() {
    const peers = this.lookupPeers();
    // ...
  }

  lookupPeers() {
    return db.lookup(this.employee, "peers");
  }

  getManagerReview() {
    const manager = this.lookupManager();
  }

  lookupManager() {
    return db.lookup(this.employee, "manager");
  }

  getSelfReview() {
    // ...
  }
}

const review = new PerformanceReview(employee);
review.perfReview();

注释 Comments

只注释逻辑复杂的内容 ✅

注释是一种道歉,不是必须的。优秀的代码大多自身就是文档。

Bad:

function hashIt(data) {
  // The hash
  let hash = 0;

  // Length of string
  const length = data.length;

  // Loop through every character in data
  for (let i = 0; i < length; i++) {
    // Get character code.
    const char = data.charCodeAt(i);
    // Make the hash
    hash = (hash << 5) - hash + char;
    // Convert to 32-bit integer
    hash &= hash;
  }
}

Good:

function hashIt(data) {
  let hash = 0;
  const length = data.length;

  for (let i = 0; i < length; i++) {
    const char = data.charCodeAt(i);
    hash = (hash << 5) - hash + char;

    // Convert to 32-bit integer
    hash &= hash;
  }
}

不要留注释掉的代码 ❌

代码注释了还不删掉,那要版本控制干嘛?

Bad:

doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();

Good:

doStuff();

不要写日志注释 ❌

请使用版本控制!不需要死代码、注释代码,特别是日志注释。使用 git log 获取历史记录!

Bad:

/**
 * 2016-12-20: Removed monads, didn't understand them (RM)
 * 2016-10-01: Improved using special monads (JP)
 * 2016-02-03: Removed type-checking (LI)
 * 2015-03-14: Added combine with type-checking (JR)
 */
function combine(a, b) {
  return a + b;
}

Good:

function combine(a, b) {
  return a + b;
}

避免位置标记 ❌

它们通常只是添加噪音。让函数和变量名以及适当的缩进和格式为代码提供可视结构。

Bad:

////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
  menu: "foo",
  nav: "bar"
};

////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
  // ...
};

Good:

$scope.model = {
  menu: "foo",
  nav: "bar"
};

const actions = function() {
  // ...
};

参考