浅谈JavaScript 代码简洁之道


Posted in Javascript onJanuary 09, 2019

测试代码质量的唯一方式:别人看你代码时说 f * k 的次数。

代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。

本文并不是代码风格指南,而是关于代码的可读性、复用性、扩展性探讨。

我们将从几个方面展开讨论:

  • 变量
  • 函数
  • 对象和数据结构
  • SOLID
  • 测试
  • 异步
  • 错误处理
  • 代码风格
  • 注释

变量

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

Bad:

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

Good:

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

保持统一

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

Bad:

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

Good:

getUser()

每个常量都该命名

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

Bad:

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

Good:

const MILLISECOND_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECOND_IN_A_DAY);

可描述

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

Bad:

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

Good:

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

直接了当

Bad:

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

Good:

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

避免无意义的前缀

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

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

使用默认值

Bad:

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

Good:

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

函数

参数越少越好

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

Bad:

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

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

// 很难知道是把什么加到日期中
addToDate(date, 1);

Good:

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

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

只需要一层抽象层

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

Bad:

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

Good:

function parseBetterJSAlternative(code) {
 const tokens = tokenize(code);
 const ast = lexer(tokens);
 ast.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 lexer(tokens) {
 const ast = [];
 tokens.forEach((token) => {
 ast.push( /* ... */ );
 });

 return ast;
}

删除重复代码

很多时候虽然是同一个功能,但由于一两个不同点,让你不得不写两个几乎相同的函数。

要想优化重复代码需要有较强的抽象能力,错误的抽象还不如重复代码。所以在抽象过程中必须要遵循 SOLID 原则(SOLID 是什么?稍后会详细介绍)。

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 'develop':
 data.githubLink = employee.getGithubLink();
 break
 case 'manager':
 data.portfolio = employee.getMBAProjects();
 break
 }
 render(data);
 })
}

对象设置默认属性

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',
 // 'body' key 缺失
 buttonText: 'Send',
 cancellable: true
};

function createMenu(config) {
 config = Object.assign({
 title: 'Foo',
 body: 'Bar',
 buttonText: 'Baz',
 cancellable: true
 }, config);

 // config 就变成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
 // ...
}

createMenu(menuConfig);

不要传 flag 参数

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

Bad:

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

Good:

function createFile(name) {
 fs.create(name);
}
function createFileTemplate(name) {
 createFile(`./temp/${name}`)
}

避免副作用(第一部分)

函数接收一个值返回一个新值,除此之外的行为我们都称之为副作用,比如修改全局变量、对文件进行 IO 操作等。

当函数确实需要副作用时,比如对文件进行 IO 操作时,请不要用多个函数/类进行文件操作,有且仅用一个函数/类来处理。也就是说副作用需要在唯一的地方处理。

副作用的三大天坑:随意修改可变数据类型、随意分享没有数据结构的状态、没有在统一地方处理副作用。

Bad:

// 全局变量被一个函数引用
// 现在这个变量从字符串变成了数组,如果有其他的函数引用,会发生无法预见的错误。
var name = 'Ryan McDermott';

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

splitIntoFirstAndLastName();

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

var name = 'Ryan McDermott';
var newName = splitIntoFirstAndLastName(name)

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

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

避免副作用(第二部分)

在 JavaScript 中,基本类型通过赋值传递,对象和数组通过引用传递。以引用传递为例:

假如我们写一个购物车,通过 addItemToCart() 方法添加商品到购物车,修改 购物车数组。此时调用 purchase() 方法购买,由于引用传递,获取的 购物车数组 正好是最新的数据。

看起来没问题对不对?

如果当用户点击购买时,网络出现故障, purchase() 方法一直在重复调用,与此同时用户又添加了新的商品,这时网络又恢复了。那么 purchase() 方法获取到 购物车数组 就是错误的。

为了避免这种问题,我们需要在每次新增商品时,克隆 购物车数组 并返回新的数组。

Bad:

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

Good:

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

不要写全局方法

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

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:

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
 }
];
let totalOutput = programmerOutput
 .map(output => output.linesOfCode)
 .reduce((totalLines, lines) => totalLines + lines, 0)

封装条件语句

Bad:

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

Good:

function shouldShowSpinner(fsm, listNode) {
 return fsm.state === 'fetching' && isEmpty(listNode);
}

if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
 // ...
}

尽量别用“非”条件句

Bad:

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

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

Good:

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

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

避免使用条件语句

Q:不用条件语句写代码是不可能的。

A:绝大多数场景可以用多态替代。

Q:用多态可行,但为什么就不能用条件语句了呢?

A:为了让代码更简洁易读,如果你的函数中出现了条件判断,那么说明你的函数不止干了一件事情,违反了函数单一原则。

Bad:

class Airplane {
 // ...

 // 获取巡航高度
 getCruisingAltitude() {
 switch (this.type) {
  case '777':
  return this.getMaxAltitude() - this.getPassengerCount();
  case 'Air Force One':
  return this.getMaxAltitude();
  case 'Cessna':
  return this.getMaxAltitude() - this.getFuelExpenditure();
 }
 }
}

Good:

class Airplane {
 // ...
}
// 波音777
class Boeing777 extends Airplane {
 // ...
 getCruisingAltitude() {
 return this.getMaxAltitude() - this.getPassengerCount();
 }
}
// 空军一号
class AirForceOne extends Airplane {
 // ...
 getCruisingAltitude() {
 return this.getMaxAltitude();
 }
}
// 赛纳斯飞机
class Cessna extends Airplane {
 // ...
 getCruisingAltitude() {
 return this.getMaxAltitude() - this.getFuelExpenditure();
 }
}

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

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

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

如果你需要做静态类型检查,比如字符串、整数等,推荐使用 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;
}

不要过度优化

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

Bad:

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

Good:

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

删除弃用代码

很多时候有些代码已经没有用了,但担心以后会用,舍不得删。

如果你忘了这件事,这些代码就永远存在那里了。

放心删吧,你可以在代码库历史版本中找他它。

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

对象和数据结构

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

使用私有变量

可以用闭包来创建私有变量

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

在 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() {};
}

链式调用

这种模式相当有用,可以在很多库中发现它的身影,比如 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();

不要滥用继承

很多时候继承被滥用,导致可读性很差,要搞清楚两个类之间的关系,继承表达的一个属于关系,而不是包含关系,比如 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);
 }
 // ...
}

SOLID

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

单一功能原则

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

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()) {
  // ...
 }
 }
}
}

开闭原则

“开”指的就是类、模块、函数都应该具有可扩展性,“闭”指的是它们不应该被修改。也就是说你可以新增功能但不能去修改源码。

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

里氏替换原则

名字很唬人,其实道理很简单,就是子类不要去重写父类的方法。

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

接口隔离原则

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

依赖反转原则

说就两点:

  1. 高层次模块不能依赖低层次模块,它们依赖于抽象接口。
  2. 抽象接口不能依赖具体实现,具体实现依赖抽象接口。

总结下来就两个字,解耦。

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

测试

随着项目变得越来越庞大,时间线拉长,有的老代码可能半年都没碰过,如果此时上线,你有信心这部分代码能正常工作吗?测试的覆盖率和你的信心是成正比的。

PS: 如果你发现你的代码很难被测试,那么你应该优化你的代码了。

单一化

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

异步

不再使用回调

不会有人愿意去看嵌套回调的代码,用 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);
 });

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

错误处理

不要忽略抛异常

Bad:

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

Good:

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

不要忘了在 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);
 // 其他的自定义方法
 });

代码风格

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

常量大写

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

注释

只有业务逻辑需要注释

代码注释不是越多越好。

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

删掉注释的代码

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

Bad:

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

Good:

doStuff();

不要记日记

记住你有 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;
}

注释不需要高亮

注释高亮,并不能起到提示的作用,反而会干扰你阅读代码。

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() {
 // ...
};

翻译自 ryanmcdermott 的 《clean-code-javascript》,本文对原文进行了一些修改。

Javascript 相关文章推荐
什么是DOM(Document Object Model)文档对象模型
Mar 05 Javascript
js控制iframe的高度/宽度让其自适应内容
Apr 09 Javascript
ExtJS4 表格的嵌套 rowExpander应用
May 02 Javascript
javascript Deferred和递归次数限制实例
Oct 21 Javascript
Javascript实现禁止输入中文或英文的例子
Dec 09 Javascript
JQuery控制radio选中和不选中方法总结
Apr 15 Javascript
JavaScript中switch语句的用法详解
Jun 03 Javascript
高效Web开发的10个jQuery代码片段
Jul 22 Javascript
jQuery插件版本冲突的处理方法分析
Jan 16 Javascript
Javascript 一些需要注意的细节(必看篇)
Jul 08 Javascript
Vue使用Proxy监听所有接口状态的方法实现
Jun 07 Javascript
Javascript生成器(Generator)的介绍与使用
Jan 31 Javascript
react组件从搭建脚手架到在npm发布的步骤实现
Jan 09 #Javascript
微信小程序公用参数与公用方法用法示例
Jan 09 #Javascript
微信小程序实现的日期午别医生排班表功能示例
Jan 09 #Javascript
Windows下Node爬虫神器Puppeteer安装记
Jan 09 #Javascript
jQuery简单实现根据日期计算星期几的方法
Jan 09 #jQuery
jQuery实现根据身份证号获取生日、年龄、性别等信息的方法
Jan 09 #jQuery
爬虫利器Puppeteer实战
Jan 09 #Javascript
You might like
Apache环境下PHP利用HTTP缓存协议原理解析及应用分析
2010/02/16 PHP
PHP连接sql server 2005环境配置及问题解决
2014/08/08 PHP
ThinkPHP中I(),U(),$this-&gt;post()等函数用法
2014/11/22 PHP
PHP+ajax分页实例简析
2015/12/07 PHP
在WordPress中使用PHP脚本来判断访客来自什么国家
2015/12/10 PHP
Laravel5框架添加自定义辅助函数的方法
2018/08/01 PHP
Microsoft Ajax Minifier 压缩javascript的方法
2010/03/05 Javascript
js格式化金额可选是否带千分位以及保留精度
2014/01/28 Javascript
nodejs npm package.json中文文档
2014/09/04 NodeJs
JQuery中模拟image的ajaxPrefilter与ajaxTransport处理
2015/06/19 Javascript
JS中使用apply、bind实现为函数或者类传入动态个数的参数
2016/04/26 Javascript
jQuery使用each方法与for语句遍历数组示例
2016/06/16 Javascript
JavaScript限定范围拖拽及自定义滚动条应用(3)
2017/05/17 Javascript
jQuery.Ajax()的data参数类型详解
2017/07/23 jQuery
jQuery 开发之EasyUI 添加数据的实例
2017/09/26 jQuery
React中使用UEditor百度富文本的方法
2018/08/22 Javascript
vue项目接口域名动态获取操作
2020/08/13 Javascript
jQuery实现鼠标拖动图片功能
2021/03/04 jQuery
python基于windows平台锁定键盘输入的方法
2015/03/05 Python
使用Python中的greenlet包实现并发编程的入门教程
2015/04/16 Python
python3实现名片管理系统
2020/11/29 Python
基于python实现百度翻译功能
2019/05/09 Python
PyCharm 专业版安装图文教程
2020/02/20 Python
Python 实现一个计时器
2020/07/28 Python
css3 实现元素弧线运动的示例代码
2020/04/24 HTML / CSS
C&A巴西网上商店:时尚、衣服、手机和鞋子
2020/06/07 全球购物
Linux的主要特性
2016/09/03 面试题
会计专业个人求职信范文
2014/01/08 职场文书
财务情况说明书范文
2014/05/06 职场文书
保护环境建议书400字
2014/05/13 职场文书
大学生标准自荐书
2014/06/15 职场文书
安全员岗位职责
2015/02/10 职场文书
家长反馈意见及建议
2015/06/03 职场文书
八年级英语教学反思
2016/02/15 职场文书
初中政治教师教学反思
2016/02/23 职场文书
python实现学员管理系统(面向对象版)
2022/06/05 Python