JavaScript 代码简洁之道

代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。
本文并不是代码风格指南,而是关于代码的可读性、复用性、扩展性探讨。
我们将从几个方面展开讨论:

  1. 变量
  2. 函数
  3. 对象和数据结构
  4. SOLID
  5. 测试
  6. 异步
  7. 错误处理
  8. 代码风格
  9. 注释

1. 变量

1.1. 用有意义且常用的单词命名变量

Bad:

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

Good:

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

1.2. 保持统一

可能同一个项目对于获取用户信息,会有三个不一样的命名。应该保持统一,如果你不知道该如何取名,可以去 codelf 搜索,看别人是怎么取名的。
Bad:

  1. getUserInfo();
  2. getClientData();
  3. getCustomerRecord();

Good:

  1. getUser()

1.3. 每个常量都该命名

可以用 buddy.js 或者 ESLint 检测代码中未命名的常量。
Bad:

  1. // 三个月之后你还能知道 86400000 是什么吗?
  2. setTimeout(blastOff, 86400000);

Good:

  1. const MILLISECOND_IN_A_DAY = 86400000;
  2. setTimeout(blastOff, MILLISECOND_IN_A_DAY);

1.4. 可描述

通过一个变量生成了一个新变量,也需要为这个新变量命名,也就是说每个变量当你看到他第一眼你就知道他是干什么的。
Bad:

  1. const ADDRESS = 'One Infinite Loop, Cupertino 95014';
  2. const CITY_ZIP_CODE_REGEX = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
  3. saveCityZipCode(ADDRESS.match(CITY_ZIP_CODE_REGEX)[1],
  4. ADDRESS.match(CITY_ZIP_CODE_REGEX)[2]);

Good:

  1. const ADDRESS = 'One Infinite Loop, Cupertino 95014';
  2. const CITY_ZIP_CODE_REGEX = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
  3. const [, city, zipCode] = ADDRESS.match(CITY_ZIP_CODE_REGEX) || [];
  4. saveCityZipCode(city, zipCode);

1.5. 直接了当

Bad:

  1. const locations = ['Austin', 'New York', 'San Francisco'];
  2. locations.forEach((l) => {
  3. doStuff();
  4. doSomeOtherStuff();
  5. // ...
  6. // ...
  7. // ...
  8. // 需要看其他代码才能确定 'l' 是干什么的。
  9. dispatch(l);
  10. });

Good:

  1. const locations = ['Austin', 'New York', 'San Francisco'];
  2. locations.forEach((location) => {
  3. doStuff();
  4. doSomeOtherStuff();
  5. // ...
  6. // ...
  7. // ...
  8. dispatch(location);
  9. });

1.6. 避免无意义的前缀

如果创建了一个对象 car,就没有必要把它的颜色命名为 carColor。
Bad:

  1. const car = {
  2. carMake: 'Honda',
  3. carModel: 'Accord',
  4. carColor: 'Blue'
  5. };
  6. function paintCar(car) {
  7. car.carColor = 'Red';
  8. }

Good:

  1. const car = {
  2. make: 'Honda',
  3. model: 'Accord',
  4. color: 'Blue'
  5. };
  6. function paintCar(car) {
  7. car.color = 'Red';
  8. }

1.7. 使用默认值

Bad:

  1. function createMicrobrewery(name) {
  2. const breweryName = name || 'Hipster Brew Co.';
  3. // ...
  4. }

Good:

  1. function createMicrobrewery(name = 'Hipster Brew Co.') {
  2. // ...
  3. }

2. 函数

2.1. 参数越少越好

如果参数超过两个,使用 ES2015/ES6 的解构语法,不用考虑参数的顺序。
Bad:

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

Good:

  1. function createMenu({ title, body, buttonText, cancellable }) {
  2. // ...
  3. }
  4. createMenu({
  5. title: 'Foo',
  6. body: 'Bar',
  7. buttonText: 'Baz',
  8. cancellable: true
  9. });

2.2. 只做一件事情

这是一条在软件工程领域流传久远的规则。严格遵守这条规则会让你的代码可读性更好,也更容易重构。如果违反这个规则,那么代码会很难被测试或者重用。
Bad:

  1. function emailClients(clients) {
  2. clients.forEach((client) => {
  3. const clientRecord = database.lookup(client);
  4. if (clientRecord.isActive()) {
  5. email(client);
  6. }
  7. });
  8. }

Good:

  1. function emailActiveClients(clients) {
  2. clients
  3. .filter(isActiveClient)
  4. .forEach(email);
  5. }
  6. function isActiveClient(client) {
  7. const clientRecord = database.lookup(client);
  8. return clientRecord.isActive();
  9. }

2.3. 顾名思义

看函数名就应该知道它是干啥的。
Bad:

  1. function addToDate(date, month) {
  2. // ...
  3. }
  4. const date = new Date();
  5. // 很难知道是把什么加到日期中
  6. addToDate(date, 1);

Good:

  1. function addMonthToDate(month, date) {
  2. // ...
  3. }
  4. const date = new Date();
  5. addMonthToDate(1, date);

2.4. 只需要一层抽象层

如果函数嵌套过多会导致很难复用以及测试。
Bad:

  1. function parseBetterJSAlternative(code) {
  2. const REGEXES = [
  3. // ...
  4. ];
  5. const statements = code.split(' ');
  6. const tokens = [];
  7. REGEXES.forEach((REGEX) => {
  8. statements.forEach((statement) => {
  9. // ...
  10. });
  11. });
  12. const ast = [];
  13. tokens.forEach((token) => {
  14. // lex...
  15. });
  16. ast.forEach((node) => {
  17. // parse...
  18. });
  19. }

Good:

  1. function parseBetterJSAlternative(code) {
  2. const tokens = tokenize(code);
  3. const ast = lexer(tokens);
  4. ast.forEach((node) => {
  5. // parse...
  6. });
  7. }
  8. function tokenize(code) {
  9. const REGEXES = [
  10. // ...
  11. ];
  12. const statements = code.split(' ');
  13. const tokens = [];
  14. REGEXES.forEach((REGEX) => {
  15. statements.forEach((statement) => {
  16. tokens.push( /* ... */ );
  17. });
  18. });
  19. return tokens;
  20. }
  21. function lexer(tokens) {
  22. const ast = [];
  23. tokens.forEach((token) => {
  24. ast.push( /* ... */ );
  25. });
  26. return ast;
  27. }

2.5. 删除重复代码

很多时候虽然是同一个功能,但由于一两个不同点,让你不得不写两个几乎相同的函数。
要想优化重复代码需要有较强的抽象能力,错误的抽象还不如重复代码。所以在抽象过程中必须要遵循 SOLID 原则(SOLID 是什么?稍后会详细介绍)。
Bad:

  1. function showDeveloperList(developers) {
  2. developers.forEach((developer) => {
  3. const expectedSalary = developer.calculateExpectedSalary();
  4. const experience = developer.getExperience();
  5. const githubLink = developer.getGithubLink();
  6. const data = {
  7. expectedSalary,
  8. experience,
  9. githubLink
  10. };
  11. render(data);
  12. });
  13. }
  14. function showManagerList(managers) {
  15. managers.forEach((manager) => {
  16. const expectedSalary = manager.calculateExpectedSalary();
  17. const experience = manager.getExperience();
  18. const portfolio = manager.getMBAProjects();
  19. const data = {
  20. expectedSalary,
  21. experience,
  22. portfolio
  23. };
  24. render(data);
  25. });
  26. }

Good:

  1. function showEmployeeList(employees) {
  2. employees.forEach(employee => {
  3. const expectedSalary = employee.calculateExpectedSalary();
  4. const experience = employee.getExperience();
  5. const data = {
  6. expectedSalary,
  7. experience,
  8. };
  9. switch(employee.type) {
  10. case 'develop':
  11. data.githubLink = employee.getGithubLink();
  12. break
  13. case 'manager':
  14. data.portfolio = employee.getMBAProjects();
  15. break
  16. }
  17. render(data);
  18. })
  19. }

2.6. 对象设置默认属性

Bad:

  1. const menuConfig = {
  2. title: null,
  3. body: 'Bar',
  4. buttonText: null,
  5. cancellable: true
  6. };
  7. function createMenu(config) {
  8. config.title = config.title || 'Foo';
  9. config.body = config.body || 'Bar';
  10. config.buttonText = config.buttonText || 'Baz';
  11. config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
  12. }
  13. createMenu(menuConfig);

Good:

  1. const menuConfig = {
  2. title: 'Order',
  3. // 'body' key 缺失
  4. buttonText: 'Send',
  5. cancellable: true
  6. };
  7. function createMenu(config) {
  8. config = Object.assign({
  9. title: 'Foo',
  10. body: 'Bar',
  11. buttonText: 'Baz',
  12. cancellable: true
  13. }, config);
  14. // config 就变成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
  15. // ...
  16. }
  17. createMenu(menuConfig);

2.7. 不要传 flag 参数

通过 flag 的 true 或 false,来判断执行逻辑,违反了一个函数干一件事的原则。
Bad:

  1. function createFile(name, temp) {
  2. if (temp) {
  3. fs.create(`./temp/${name}`);
  4. } else {
  5. fs.create(name);
  6. }
  7. }

Good:

  1. function createFile(name) {
  2. fs.create(name);
  3. }
  4. function createFileTemplate(name) {
  5. createFile(`./temp/${name}`)
  6. }

2.8. 避免副作用(第一部分)

函数接收一个值返回一个新值,除此之外的行为我们都称之为副作用,比如修改全局变量、对文件进行 IO 操作等。
当函数确实需要副作用时,比如对文件进行 IO 操作时,请不要用多个函数/类进行文件操作,有且仅用一个函数/类来处理。也就是说副作用需要在唯一的地方处理。
副作用的三大天坑:随意修改可变数据类型、随意分享没有数据结构的状态、没有在统一地方处理副作用。
Bad:

  1. // 全局变量被一个函数引用
  2. // 现在这个变量从字符串变成了数组,如果有其他的函数引用,会发生无法预见的错误。
  3. var name = 'Ryan McDermott';
  4. function splitIntoFirstAndLastName() {
  5. name = name.split(' ');
  6. }
  7. splitIntoFirstAndLastName();
  8. console.log(name); // ['Ryan', 'McDermott'];

Good:

  1. var name = 'Ryan McDermott';
  2. var newName = splitIntoFirstAndLastName(name)
  3. function splitIntoFirstAndLastName(name) {
  4. return name.split(' ');
  5. }
  6. console.log(name); // 'Ryan McDermott';
  7. console.log(newName); // ['Ryan', 'McDermott'];

2.9. 避免副作用(第二部分)

在 JavaScript 中,基本类型通过赋值传递,对象和数组通过引用传递。以引用传递为例:
假如我们写一个购物车,通过 addItemToCart() 方法添加商品到购物车,修改 购物车数组。此时调用 purchase() 方法购买,由于引用传递,获取的 购物车数组 正好是最新的数据。
看起来没问题对不对?
如果当用户点击购买时,网络出现故障, purchase() 方法一直在重复调用,与此同时用户又添加了新的商品,这时网络又恢复了。那么 purchase() 方法获取到 购物车数组 就是错误的。
为了避免这种问题,我们需要在每次新增商品时,克隆 购物车数组 并返回新的数组。
Bad:

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

Good:

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

2.10. 不要写全局方法

在 JavaScript 中,永远不要污染全局,会在生产环境中产生难以预料的 bug。举个例子,比如你在 Array.prototype 上新增一个 diff 方法来判断两个数组的不同。而你同事也打算做类似的事情,不过他的 diff 方法是用来判断两个数组首位元素的不同。很明显你们方法会产生冲突,遇到这类问题我们可以用 ES2015/ES6 的语法来对 Array 进行扩展。
Bad:

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

Good:

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

2.11. 比起命令式我更喜欢函数式编程

函数式变编程可以让代码的逻辑更清晰更优雅,方便测试。
Bad:

  1. const programmerOutput = [
  2. {
  3. name: 'Uncle Bobby',
  4. linesOfCode: 500
  5. }, {
  6. name: 'Suzie Q',
  7. linesOfCode: 1500
  8. }, {
  9. name: 'Jimmy Gosling',
  10. linesOfCode: 150
  11. }, {
  12. name: 'Gracie Hopper',
  13. linesOfCode: 1000
  14. }
  15. ];
  16. let totalOutput = 0;
  17. for (let i = 0; i < programmerOutput.length; i++) {
  18. totalOutput += programmerOutput[i].linesOfCode;
  19. }

Good:

  1. const programmerOutput = [
  2. {
  3. name: 'Uncle Bobby',
  4. linesOfCode: 500
  5. }, {
  6. name: 'Suzie Q',
  7. linesOfCode: 1500
  8. }, {
  9. name: 'Jimmy Gosling',
  10. linesOfCode: 150
  11. }, {
  12. name: 'Gracie Hopper',
  13. linesOfCode: 1000
  14. }
  15. ];
  16. let totalOutput = programmerOutput
  17. .map(output => output.linesOfCode)
  18. .reduce((totalLines, lines) => totalLines + lines, 0)

2.12. 封装条件语句

Bad:

  1. if (fsm.state === 'fetching' && isEmpty(listNode)) {
  2. // ...
  3. }

Good:

  1. function shouldShowSpinner(fsm, listNode) {
  2. return fsm.state === 'fetching' && isEmpty(listNode);
  3. }
  4. if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
  5. // ...
  6. }

2.13. 尽量别用“非”条件句

Bad:

  1. function isDOMNodeNotPresent(node) {
  2. // ...
  3. }
  4. if (!isDOMNodeNotPresent(node)) {
  5. // ...
  6. }

Good:

  1. function isDOMNodePresent(node) {
  2. // ...
  3. }
  4. if (isDOMNodePresent(node)) {
  5. // ...
  6. }

2.14. 避免使用条件语句

Q:不用条件语句写代码是不可能的。
A:绝大多数场景可以用多态替代。
Q:用多态可行,但为什么就不能用条件语句了呢?
A:为了让代码更简洁易读,如果你的函数中出现了条件判断,那么说明你的函数不止干了一件事情,违反了函数单一原则。
Bad:

  1. class Airplane {
  2. // ...
  3. // 获取巡航高度
  4. getCruisingAltitude() {
  5. switch (this.type) {
  6. case '777':
  7. return this.getMaxAltitude() - this.getPassengerCount();
  8. case 'Air Force One':
  9. return this.getMaxAltitude();
  10. case 'Cessna':
  11. return this.getMaxAltitude() - this.getFuelExpenditure();
  12. }
  13. }
  14. }

Good:

  1. class Airplane {
  2. // ...
  3. }
  4. // 波音777
  5. class Boeing777 extends Airplane {
  6. // ...
  7. getCruisingAltitude() {
  8. return this.getMaxAltitude() - this.getPassengerCount();
  9. }
  10. }
  11. // 空军一号
  12. class AirForceOne extends Airplane {
  13. // ...
  14. getCruisingAltitude() {
  15. return this.getMaxAltitude();
  16. }
  17. }
  18. // 赛纳斯飞机
  19. class Cessna extends Airplane {
  20. // ...
  21. getCruisingAltitude() {
  22. return this.getMaxAltitude() - this.getFuelExpenditure();
  23. }
  24. }

2.15. 避免类型检查(第一部分)

JavaScript 是无类型的,意味着你可以传任意类型参数,这种自由度很容易让人困扰,不自觉的就会去检查类型。仔细想想是你真的需要检查类型还是你的 API 设计有问题?
Bad:

function travelToTexas(vehicle) {
  if (vehicle instanceof Bicycle) {
    vehicle.pedal(this.currentLocation, new Location('texas'));
  } else if (vehicle instanceof Car) {
    vehicle.drive(this.currentLocation, new Location('texas'));
  }
}

Good:

function travelToTexas(vehicle) {
  vehicle.move(this.currentLocation, new Location('texas'));
}

2.16. 避免类型检查(第二部分)

如果你需要做静态类型检查,比如字符串、整数等,推荐使用 TypeScript,不然你的代码会变得又臭又长。
Bad:

function combine(val1, val2) {
  if (typeof val1 === 'number' && typeof val2 === 'number' ||
      typeof val1 === 'string' && typeof val2 === 'string') {
    return val1 + val2;
  }

  throw new Error('Must be of type String or Number');
}

Good:

function combine(val1, val2) {
  return val1 + val2;
}

2.17. 不要过度优化

现代浏览器已经在底层做了很多优化,过去的很多优化方案都是无效的,会浪费你的时间,想知道现代浏览器优化了哪些内容,请点这里。
Bad:

// 在老的浏览器中,由于 `list.length` 没有做缓存,每次迭代都会去计算,造成不必要开销。
// 现代浏览器已对此做了优化。
for (let i = 0, len = list.length; i < len; i++) {
  // ...
}

Good:

for (let i = 0; i < list.length; i++) {
  // ...
}

2.18. 删除弃用代码

很多时候有些代码已经没有用了,但担心以后会用,舍不得删。
如果你忘了这件事,这些代码就永远存在那里了。
放心删吧,你可以在代码库历史版本中找他它。
Bad:

function oldRequestModule(url) {
  // ...
}

function newRequestModule(url) {
  // ...
}

const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

Good:

function newRequestModule(url) {
  // ...
}

const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

2.19. 判断数组长度是否为零

// bad
if (arr.length !== 0) {
    // todo
}

// good
if (arr.length) {
    // todo
}

// bad
if (arr.length === 0) {
    // todo
}

// good
if (!arr.length) {
    // todo
}

2.20. 函数参数校验

// bad
let findStudentByAge = (arr, age) => {
    if (!age) throw new Error('参数不能为空')
    return arr.filter(num => num === age)
}

// good
let checkoutType = () => {
    throw new Error('参数不能为空')
}
let findStudentByAge = (arr, age = checkoutType()) =>
    arr.filter(num => num === age)

3. 对象和数据结构

3.1. 用 get、set 方法操作数据

这样做可以带来很多好处,比如在操作数据时打日志,方便跟踪错误;在 set 的时候很容易对数据进行校验…
Bad:

function makeBankAccount() {
  // ...

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

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

Good:

function makeBankAccount() {
  // 私有变量
  let balance = 0;

  function getBalance() {
    return balance;
  }

  function setBalance(amount) {
    // ... 在更新 balance 前,对 amount 进行校验
    balance = amount;
  }

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

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

3.2. 使用私有变量

可以用闭包来创建私有变量
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

4. 类

4.1. 使用 class

在 ES2015/ES6 之前,没有类的语法,只能用构造函数的方式模拟类,可读性非常差。
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() {};
}

4.2. 链式调用

这种模式相当有用,可以在很多库中发现它的身影,比如 jQuery、Lodash 等。它让你的代码简洁优雅。实现起来也非常简单,在类的方法最后返回 this 可以了。
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;
    return this;
  }

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

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

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

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

4.3. 不要滥用继承

很多时候继承被滥用,导致可读性很差,要搞清楚两个类之间的关系,继承表达的一个属于关系,而不是包含关系,比如 Human->Animal vs. User->UserDetails
Bad:

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

  // ...
}

// TaxData(税收信息)并不是属于 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);
  }
  // ...
}

5. SOLID

SOLID 是几个单词首字母组合而来,分别表示 单一功能原则、开闭原则、里氏替换原则、接口隔离原则以及依赖反转原则。

5.1. 单一功能原则

如果一个类干的事情太多太杂,会导致后期很难维护。我们应该厘清职责,各司其职减少相互之间依赖。
Bad:

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

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

  verifyCredentials() {
    // ...
  }
}

Good:

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

class UserSetting {
  constructor(user) {
    this.user = user;
    this.auth = new UserAuth(this.user);
  }
  changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
      // ...
    }
  }
}
}

5.2. 开闭原则

“开”指的就是类、模块、函数都应该具有可扩展性,“闭”指的是它们不应该被修改。也就是说你可以新增功能但不能去修改源码。
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) => {
        // 传递 response 并 return
      });
    } else if (this.adapter.name === 'httpNodeAdapter') {
      return makeHttpCall(url).then((response) => {
        // 传递 response 并 return
      });
    }
  }
}

function makeAjaxCall(url) {
  // 处理 request 并 return promise
}

function makeHttpCall(url) {
  // 处理 request 并 return promise
}

Good:

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

  request(url) {
    // 处理 request 并 return promise
  }
}

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

  request(url) {
    // 处理 request 并 return promise
  }
}

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

  fetch(url) {
    return this.adapter.request(url).then((response) => {
      // 传递 response 并 return
    });
  }
}

5.3. 里氏替换原则

名字很唬人,其实道理很简单,就是子类不要去重写父类的方法。
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(); 
    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);

5.4. 接口隔离原则

JavaScript 几乎没有接口的概念,所以这条原则很少被使用。官方定义是“客户端不应该依赖它不需要的接口”,也就是接口最小化,把接口解耦。
Bad:

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

  setup() {
    this.rootNode = this.settings.rootNode;
    this.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() {}
  }
});

5.5. 依赖反转原则

说就两点:
高层次模块不能依赖低层次模块,它们依赖于抽象接口。
抽象接口不能依赖具体实现,具体实现依赖抽象接口。
总结下来就两个字,解耦。
Bad:

// 库存查询
class InventoryRequester {
  constructor() {
    this.REQ_METHODS = ['HTTP'];
  }

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

// 库存跟踪
class InventoryTracker {
  constructor(items) {
    this.items = items;

    // 这里依赖一个特殊的请求类,其实我们只是需要一个请求方法。
    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);
    });
  }
}

// HTTP 请求
class InventoryRequesterHTTP {
  constructor() {
    this.REQ_METHODS = ['HTTP'];
  }

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

// webSocket 请求
class InventoryRequesterWS {
  constructor() {
    this.REQ_METHODS = ['WS'];
  }

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

// 通过依赖注入的方式将请求模块解耦,这样我们就可以很轻易的替换成 webSocket 请求。
const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterHTTP());
inventoryTracker.requestItems();

6. 测试

随着项目变得越来越庞大,时间线拉长,有的老代码可能半年都没碰过,如果此时上线,你有信心这部分代码能正常工作吗?测试的覆盖率和你的信心是成正比的。
PS: 如果你发现你的代码很难被测试,那么你应该优化你的代码了。

6.1. 单一化

Bad:

import assert from 'assert';

describe('MakeMomentJSGreatAgain', () => {
  it('handles date boundaries', () => {
    let date;

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

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

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

Good:

import assert from 'assert';

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

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

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

7. 异步

7.1. 不再使用回调

不会有人愿意去看嵌套回调的代码,用 Promises 替代回调吧。
Bad:

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

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

Good:

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

7.2. Async/Await 比起 Promises 更简洁

Bad:

import { get } from 'request-promise';
import { writeFile } from 'fs-promise';

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

Good:

import { get } from 'request-promise';
import { writeFile } from 'fs-promise';

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

8. 错误处理

8.1. 不要忽略抛异常

Bad:

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

Good:

try {
  functionThatMightThrow();
} catch (error) {
  // 这一种选择,比起 console.log 更直观
  console.error(error);
  // 也可以在界面上提醒用户
  notifyUserOfError(error);
  // 也可以把异常传回服务器
  reportErrorToService(error);
  // 其他的自定义方法
}

8.2. 不要忘了在 Promises 抛异常

Bad:

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

Good:

getdata()
  .then((data) => {
    functionThatMightThrow(data);
  })
  .catch((error) => {
    // 这一种选择,比起 console.log 更直观
    console.error(error);
    // 也可以在界面上提醒用户
    notifyUserOfError(error);
    // 也可以把异常传回服务器
    reportErrorToService(error);
    // 其他的自定义方法
  });

9. 代码风格

代码风格是主观的,争论哪种好哪种不好是在浪费生命。市面上有很多自动处理代码风格的工具,选一个喜欢就行了,我们来讨论几个非自动处理的部分。

9.1. 常量大写

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 {}

9.2. 先声明后调用

就像我们看报纸文章一样,从上到下看,所以为了方便阅读把函数声明写在函数调用前面。
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();

10. 注释

10.1. 只有业务逻辑需要注释

代码注释不是越多越好。
Bad:

function hashIt(data) {
  // 这是初始值
  let hash = 0;

  // 数组的长度
  const length = data.length;

  // 循环数组
  for (let i = 0; i < length; i++) {
    // 获取字符代码
    const char = data.charCodeAt(i);
    // 修改 hash
    hash = ((hash << 5) - hash) + char;
    // 转换为32位整数
    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;

    // 转换为32位整数
    hash &= hash;
  }
}

10.2. 删掉注释的代码

git 存在的意义就是保存你的旧代码,所以注释的代码赶紧删掉吧。
Bad:

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

Good:

doStuff();

javascript
不要记日记
记住你有 git!,git log 可以帮你干这事。
Bad:

/**
 * 2016-12-20: 删除了 xxx
 * 2016-10-01: 改进了 xxx
 * 2016-02-03: 删除了第12行的类型检查
 * 2015-03-14: 增加了一个合并的方法
 */
function combine(a, b) {
  return a + b;
}

Good:

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

10.3. 注释不需要高亮

注释高亮,并不能起到提示的作用,反而会干扰你阅读代码。
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() {
  // ...
};