代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。
本文并不是代码风格指南,而是关于代码的可读性、复用性、扩展性探讨。
我们将从几个方面展开讨论:
变量
函数
对象和数据结构
类
SOLID
测试
异步
错误处理
代码风格
注释
变量
用有意义且常用的单词命名变量
Bad:
const yyyymmdstr = moment().format('YYYY/MM/DD');
Good:
const currentDate = moment().format('YYYY/MM/DD');
保持统一
可能同一个项目对于获取用户信息,会有三个不一样的命名。应该保持统一,如果你不知道该如何取名,可以去 codelf 搜索,看别人是怎么取名的。
Bad:
1 getUserInfo(); 2 getClientData(); 3 getCustomerRecord();
Good:
getUser()
每个常量都该命名
可以用 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);
可描述
通过一个变量生成了一个新变量,也需要为这个新变量命名,也就是说每个变量当你看到他第一眼你就知道他是干什么的。
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);
直接了当
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: ``js const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((location) => { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch(location); });
1#### 避免无意义的前缀 2 3如果创建了一个对象 car,就没有必要把它的颜色命名为 carColor。 4 5Bad: 6```js 7 const car = { 8 carMake: 'Honda', 9 carModel: 'Accord', 10 carColor: 'Blue' 11 }; 12 13 function paintCar(car) { 14 car.carColor = 'Red'; 15 }
Good:
1 const car = { 2 make: 'Honda', 3 model: 'Accord', 4 color: 'Blue' 5 }; 6 7 function paintCar(car) { 8 car.color = 'Red'; 9 }
使用默认值
Bad:
1 function createMicrobrewery(name) { 2 const breweryName = name || 'Hipster Brew Co.'; 3 // ... 4 }
Good:
1 function createMicrobrewery(name = 'Hipster Brew Co.') { 2 // ... 3 }
函数
参数越少越好
如果参数超过两个,使用 ES2015/ES6 的解构语法,不用考虑参数的顺序。
Bad:
1 function createMenu(title, body, buttonText, cancellable) { 2 // ... 3 }
Good:
1 function createMenu({ title, body, buttonText, cancellable }) { 2 // ... 3 } 4 5 createMenu({ 6 title: 'Foo', 7 body: 'Bar', 8 buttonText: 'Baz', 9 cancellable: true 10 });
只做一件事情
这是一条在软件工程领域流传久远的规则。严格遵守这条规则会让你的代码可读性更好,也更容易重构。如果违反这个规则,那么代码会很难被测试或者重用。
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 }
顾名思义
看函数名就应该知道它是干啥的。
Bad:
1 function addToDate(date, month) { 2 // ... 3 } 4 5 const date = new Date(); 6 7 // 很难知道是把什么加到日期中 8 addToDate(date, 1);
Good:
1 function addMonthToDate(month, date) { 2 // ... 3 } 4 5 const date = new Date(); 6 addMonthToDate(1, date);
只需要一层抽象层
如果函数嵌套过多会导致很难复用以及测试。
Bad:
1 function parseBetterJSAlternative(code) { 2 const REGEXES = [ 3 // ... 4 ]; 5 6 const statements = code.split(' '); 7 const tokens = []; 8 REGEXES.forEach((REGEX) => { 9 statements.forEach((statement) => { 10 // ... 11 }); 12 }); 13 14 const ast = []; 15 tokens.forEach((token) => { 16 // lex... 17 }); 18 19 ast.forEach((node) => { 20 // parse... 21 }); 22 }
Good:
1 function parseBetterJSAlternative(code) { 2 const tokens = tokenize(code); 3 const ast = lexer(tokens); 4 ast.forEach((node) => { 5 // parse... 6 }); 7 } 8 9 function tokenize(code) { 10 const REGEXES = [ 11 // ... 12 ]; 13 14 const statements = code.split(' '); 15 const tokens = []; 16 REGEXES.forEach((REGEX) => { 17 statements.forEach((statement) => { 18 tokens.push( /* ... */ ); 19 }); 20 }); 21 22 return tokens; 23 } 24 25 function lexer(tokens) { 26 const ast = []; 27 tokens.forEach((token) => { 28 ast.push( /* ... */ ); 29 }); 30 31 return ast; 32 }
删除重复代码
很多时候虽然是同一个功能,但由于一两个不同点,让你不得不写两个几乎相同的函数。
要想优化重复代码需要有较强的抽象能力,错误的抽象还不如重复代码。所以在抽象过程中必须要遵循 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 12 render(data); 13 }); 14 } 15 16 function showManagerList(managers) { 17 managers.forEach((manager) => { 18 const expectedSalary = manager.calculateExpectedSalary(); 19 const experience = manager.getExperience(); 20 const portfolio = manager.getMBAProjects(); 21 const data = { 22 expectedSalary, 23 experience, 24 portfolio 25 }; 26 27 render(data); 28 }); 29 }
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 10 switch(employee.type) { 11 case 'develop': 12 data.githubLink = employee.getGithubLink(); 13 break 14 case 'manager': 15 data.portfolio = employee.getMBAProjects(); 16 break 17 } 18 render(data); 19 }) 20 }
对象设置默认属性
Bad:
1 const menuConfig = { 2 title: null, 3 body: 'Bar', 4 buttonText: null, 5 cancellable: true 6 }; 7 8 function createMenu(config) { 9 config.title = config.title || 'Foo'; 10 config.body = config.body || 'Bar'; 11 config.buttonText = config.buttonText || 'Baz'; 12 config.cancellable = config.cancellable !== undefined ? config.cancellable : true; 13 } 14 15 createMenu(menuConfig);
Good:
1 const menuConfig = { 2 title: 'Order', 3 // 'body' key 缺失 4 buttonText: 'Send', 5 cancellable: true 6 }; 7 8 function createMenu(config) { 9 config = Object.assign({ 10 title: 'Foo', 11 body: 'Bar', 12 buttonText: 'Baz', 13 cancellable: true 14 }, config); 15 16 // config 就变成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true} 17 // ... 18 } 19 20 createMenu(menuConfig);
不要传 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 }
避免副作用(第一部分)
函数接收一个值返回一个新值,除此之外的行为我们都称之为副作用,比如修改全局变量、对文件进行 IO 操作等。
当函数确实需要副作用时,比如对文件进行 IO 操作时,请不要用多个函数/类进行文件操作,有且仅用一个函数/类来处理。也就是说副作用需要在唯一的地方处理。
副作用的三大天坑:随意修改可变数据类型、随意分享没有数据结构的状态、没有在统一地方处理副作用。
Bad:
1 // 全局变量被一个函数引用 2 // 现在这个变量从字符串变成了数组,如果有其他的函数引用,会发生无法预见的错误。 3 var name = 'Ryan McDermott'; 4 5 function splitIntoFirstAndLastName() { 6 name = name.split(' '); 7 } 8 9 splitIntoFirstAndLastName(); 10 11 console.log(name); // ['Ryan', 'McDermott'];
Good:
1 var name = 'Ryan McDermott'; 2 var newName = splitIntoFirstAndLastName(name) 3 4 function splitIntoFirstAndLastName(name) { 5 return name.split(' '); 6 } 7 8 console.log(name); // 'Ryan McDermott'; 9 console.log(newName); // ['Ryan', 'McDermott'];
避免副作用(第二部分)
在 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 };
不要写全局方法
在 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 }
比起命令式我更喜欢函数式编程
函数式变编程可以让代码的逻辑更清晰更优雅,方便测试。
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 17 let totalOutput = 0; 18 19 for (let i = 0; i < programmerOutput.length; i++) { 20 totalOutput += programmerOutput[i].linesOfCode; 21 }
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)
封装条件语句
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 5 if (shouldShowSpinner(fsmInstance, listNodeInstance)) { 6 // ... 7 }
尽量别用“非”条件句
Bad:
1 function isDOMNodeNotPresent(node) { 2 // ... 3 } 4 5 if (!isDOMNodeNotPresent(node)) { 6 // ... 7 }
Good:
1 function isDOMNodePresent(node) { 2 // ... 3 } 4 5 if (isDOMNodePresent(node)) { 6 // ... 7 }
避免使用条件语句
Q:不用条件语句写代码是不可能的。
A:绝大多数场景可以用多态替代。
Q:用多态可行,但为什么就不能用条件语句了呢?
A:为了让代码更简洁易读,如果你的函数中出现了条件判断,那么说明你的函数不止干了一件事情,违反了函数单一原则。
Bad:
1 class Airplane { 2 // ... 3 4 // 获取巡航高度 5 getCruisingAltitude() { 6 switch (this.type) { 7 case '777': 8 return this.getMaxAltitude() - this.getPassengerCount(); 9 case 'Air Force One': 10 return this.getMaxAltitude(); 11 case 'Cessna': 12 return this.getMaxAltitude() - this.getFuelExpenditure(); 13 } 14 } 15 }
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 }
避免类型检查(第一部分)
JavaScript 是无类型的,意味着你可以传任意类型参数,这种自由度很容易让人困扰,不自觉的就会去检查类型。仔细想想是你真的需要检查类型还是你的 API 设计有问题?
Bad:
1 function travelToTexas(vehicle) { 2 if (vehicle instanceof Bicycle) { 3 vehicle.pedal(this.currentLocation, new Location('texas')); 4 } else if (vehicle instanceof Car) { 5 vehicle.drive(this.currentLocation, new Location('texas')); 6 } 7 }
Good:
1 function travelToTexas(vehicle) { 2 vehicle.move(this.currentLocation, new Location('texas')); 3 }
避免类型检查(第二部分)
如果你需要做静态类型检查,比如字符串、整数等,推荐使用 TypeScript,不然你的代码会变得又臭又长。
Bad:
1 function combine(val1, val2) { 2 if (typeof val1 === 'number' && typeof val2 === 'number' || 3 typeof val1 === 'string' && typeof val2 === 'string') { 4 return val1 + val2; 5 } 6 7 throw new Error('Must be of type String or Number'); 8 }
Good:
1 function combine(val1, val2) { 2 return val1 + val2; 3 }
不要过度优化
现代浏览器已经在底层做了很多优化,过去的很多优化方案都是无效的,会浪费你的时间,想知道现代浏览器优化了哪些内容,请点这里。
Bad:
1 // 在老的浏览器中,由于 `list.length` 没有做缓存,每次迭代都会去计算,造成不必要开销。 2 // 现代浏览器已对此做了优化。 3 for (let i = 0, len = list.length; i < len; i++) { 4 // ... 5 }
Good:
1 for (let i = 0; i < list.length; i++) { 2 // ... 3 }
删除弃用代码
很多时候有些代码已经没有用了,但担心以后会用,舍不得删。
如果你忘了这件事,这些代码就永远存在那里了。
放心删吧,你可以在代码库历史版本中找他它。
Bad:
1 function oldRequestModule(url) { 2 // ... 3 } 4 5 function newRequestModule(url) { 6 // ... 7 } 8 9 const req = newRequestModule; 10 inventoryTracker('apples', req, 'www.inventory-awesome.io');
Good:
1 function newRequestModule(url) { 2 // ... 3 } 4 5 const req = newRequestModule; 6 inventoryTracker('apples', req, 'www.inventory-awesome.io');
对象和数据结构
用 get、set 方法操作数据
这样做可以带来很多好处,比如在操作数据时打日志,方便跟踪错误;在 set 的时候很容易对数据进行校验…
Bad:
1 function makeBankAccount() { 2 // ... 3 4 return { 5 balance: 0, 6 // ... 7 }; 8 } 9 10 const account = makeBankAccount(); 11 account.balance = 100;
Good:
1 function makeBankAccount() { 2 // 私有变量 3 let balance = 0; 4 5 function getBalance() { 6 return balance; 7 } 8 9 function setBalance(amount) { 10 // ... 在更新 balance 前,对 amount 进行校验 11 balance = amount; 12 } 13 14 return { 15 // ... 16 getBalance, 17 setBalance, 18 }; 19 } 20 21 const account = makeBankAccount(); 22 account.setBalance(100);
使用私有变量
可以用闭包来创建私有变量
Bad:
1 const Employee = function(name) { 2 this.name = name; 3 }; 4 5 Employee.prototype.getName = function getName() { 6 return this.name; 7 }; 8 9 const employee = new Employee('John Doe'); 10 console.log(`Employee name: ${employee.getName()}`); 11 // Employee name: John Doe 12 delete employee.name; 13 console.log(`Employee name: ${employee.getName()}`); 14 // Employee name: undefined
Good:
1 function makeEmployee(name) { 2 return { 3 getName() { 4 return name; 5 }, 6 }; 7 } 8 9 const employee = makeEmployee('John Doe'); 10 console.log(`Employee name: ${employee.getName()}`); 11 // Employee name: John Doe 12 delete employee.name; 13 console.log(`Employee name: ${employee.getName()}`); 14 // Employee name: John Doe
类
使用 class
在 ES2015/ES6 之前,没有类的语法,只能用构造函数的方式模拟类,可读性非常差。
Bad:
1 // 动物 2 const Animal = function(age) { 3 if (!(this instanceof Animal)) { 4 throw new Error('Instantiate Animal with `new`'); 5 } 6 7 this.age = age; 8 }; 9 10 Animal.prototype.move = function move() {}; 11 12 // 哺乳动物 13 const Mammal = function(age, furColor) { 14 if (!(this instanceof Mammal)) { 15 throw new Error('Instantiate Mammal with `new`'); 16 } 17 18 Animal.call(this, age); 19 this.furColor = furColor; 20 }; 21 22 Mammal.prototype = Object.create(Animal.prototype); 23 Mammal.prototype.constructor = Mammal; 24 Mammal.prototype.liveBirth = function liveBirth() {}; 25 26 // 人类 27 const Human = function(age, furColor, languageSpoken) { 28 if (!(this instanceof Human)) { 29 throw new Error('Instantiate Human with `new`'); 30 } 31 32 Mammal.call(this, age, furColor); 33 this.languageSpoken = languageSpoken; 34 }; 35 36 Human.prototype = Object.create(Mammal.prototype); 37 Human.prototype.constructor = Human; 38 Human.prototype.speak = function speak() {};
Good:
1 // 动物 2 class Animal { 3 constructor(age) { 4 this.age = age 5 }; 6 move() {}; 7 } 8 9 // 哺乳动物 10 class Mammal extends Animal{ 11 constructor(age, furColor) { 12 super(age); 13 this.furColor = furColor; 14 }; 15 liveBirth() {}; 16 } 17 18 // 人类 19 class Human extends Mammal{ 20 constructor(age, furColor, languageSpoken) { 21 super(age, furColor); 22 this.languageSpoken = languageSpoken; 23 }; 24 speak() {}; 25 }
链式调用
这种模式相当有用,可以在很多库中发现它的身影,比如 jQuery、Lodash 等。它让你的代码简洁优雅。实现起来也非常简单,在类的方法最后返回 this 可以了。
Bad:
1 class Car { 2 constructor(make, model, color) { 3 this.make = make; 4 this.model = model; 5 this.color = color; 6 } 7 8 setMake(make) { 9 this.make = make; 10 } 11 12 setModel(model) { 13 this.model = model; 14 } 15 16 setColor(color) { 17 this.color = color; 18 } 19 20 save() { 21 console.log(this.make, this.model, this.color); 22 } 23 } 24 25 const car = new Car('Ford','F-150','red'); 26 car.setColor('pink'); 27 car.save();
Good:
1 class Car { 2 constructor(make, model, color) { 3 this.make = make; 4 this.model = model; 5 this.color = color; 6 } 7 8 setMake(make) { 9 this.make = make; 10 return this; 11 } 12 13 setModel(model) { 14 this.model = model; 15 return this; 16 } 17 18 setColor(color) { 19 this.color = color; 20 return this; 21 } 22 23 save() { 24 console.log(this.make, this.model, this.color); 25 return this; 26 } 27 } 28 29 const car = new Car('Ford','F-150','red') 30 .setColor('pink'); 31 .save();
不要滥用继承
很多时候继承被滥用,导致可读性很差,要搞清楚两个类之间的关系,继承表达的一个属于关系,而不是包含关系,比如 Human->Animal vs. User->UserDetails
Bad:
1 class Employee { 2 constructor(name, email) { 3 this.name = name; 4 this.email = email; 5 } 6 7 // ... 8 } 9 10 // TaxData(税收信息)并不是属于 Employee(雇员),而是包含关系。 11 class EmployeeTaxData extends Employee { 12 constructor(ssn, salary) { 13 super(); 14 this.ssn = ssn; 15 this.salary = salary; 16 } 17 18 // ... 19 }
Good:
1 class EmployeeTaxData { 2 constructor(ssn, salary) { 3 this.ssn = ssn; 4 this.salary = salary; 5 } 6 7 // ... 8 } 9 10 class Employee { 11 constructor(name, email) { 12 this.name = name; 13 this.email = email; 14 } 15 16 setTaxData(ssn, salary) { 17 this.taxData = new EmployeeTaxData(ssn, salary); 18 } 19 // ... 20 }
SOLID
SOLID 是几个单词首字母组合而来,分别表示 单一功能原则、开闭原则、里氏替换原则、接口隔离原则以及依赖反转原则。
单一功能原则
如果一个类干的事情太多太杂,会导致后期很难维护。我们应该厘清职责,各司其职减少相互之间依赖。
Bad:
1 class UserSettings { 2 constructor(user) { 3 this.user = user; 4 } 5 6 changeSettings(settings) { 7 if (this.verifyCredentials()) { 8 // ... 9 } 10 } 11 12 verifyCredentials() { 13 // ... 14 } 15 }
Good:
1 class UserAuth { 2 constructor(user) { 3 this.user = user; 4 } 5 verifyCredentials() { 6 // ... 7 } 8 } 9 10 class UserSetting { 11 constructor(user) { 12 this.user = user; 13 this.auth = new UserAuth(this.user); 14 } 15 changeSettings(settings) { 16 if (this.auth.verifyCredentials()) { 17 // ... 18 } 19 } 20 } 21 }
开闭原则
“开”指的就是类、模块、函数都应该具有可扩展性,“闭”指的是它们不应该被修改。也就是说你可以新增功能但不能去修改源码。
Bad:
1 class AjaxAdapter extends Adapter { 2 constructor() { 3 super(); 4 this.name = 'ajaxAdapter'; 5 } 6 } 7 8 class NodeAdapter extends Adapter { 9 constructor() { 10 super(); 11 this.name = 'nodeAdapter'; 12 } 13 } 14 15 class HttpRequester { 16 constructor(adapter) { 17 this.adapter = adapter; 18 } 19 20 fetch(url) { 21 if (this.adapter.name === 'ajaxAdapter') { 22 return makeAjaxCall(url).then((response) => { 23 // 传递 response 并 return 24 }); 25 } else if (this.adapter.name === 'httpNodeAdapter') { 26 return makeHttpCall(url).then((response) => { 27 // 传递 response 并 return 28 }); 29 } 30 } 31 } 32 33 function makeAjaxCall(url) { 34 // 处理 request 并 return promise 35 } 36 37 function makeHttpCall(url) { 38 // 处理 request 并 return promise 39 }
Good:
1 class AjaxAdapter extends Adapter { 2 constructor() { 3 super(); 4 this.name = 'ajaxAdapter'; 5 } 6 7 request(url) { 8 // 处理 request 并 return promise 9 } 10 } 11 12 class NodeAdapter extends Adapter { 13 constructor() { 14 super(); 15 this.name = 'nodeAdapter'; 16 } 17 18 request(url) { 19 // 处理 request 并 return promise 20 } 21 } 22 23 class HttpRequester { 24 constructor(adapter) { 25 this.adapter = adapter; 26 } 27 28 fetch(url) { 29 return this.adapter.request(url).then((response) => { 30 // 传递 response 并 return 31 }); 32 } 33 }
里氏替换原则
名字很唬人,其实道理很简单,就是子类不要去重写父类的方法。
Bad:
1 // 长方形 2 class Rectangle { 3 constructor() { 4 this.width = 0; 5 this.height = 0; 6 } 7 8 setColor(color) { 9 // ... 10 } 11 12 render(area) { 13 // ... 14 } 15 16 setWidth(width) { 17 this.width = width; 18 } 19 20 setHeight(height) { 21 this.height = height; 22 } 23 24 getArea() { 25 return this.width * this.height; 26 } 27 } 28 29 // 正方形 30 class Square extends Rectangle { 31 setWidth(width) { 32 this.width = width; 33 this.height = width; 34 } 35 36 setHeight(height) { 37 this.width = height; 38 this.height = height; 39 } 40 } 41 42 function renderLargeRectangles(rectangles) { 43 rectangles.forEach((rectangle) => { 44 rectangle.setWidth(4); 45 rectangle.setHeight(5); 46 const area = rectangle.getArea(); 47 rectangle.render(area); 48 }); 49 } 50 51 const rectangles = [new Rectangle(), new Rectangle(), new Square()]; 52 renderLargeRectangles(rectangles);
Good:
1 class Shape { 2 setColor(color) { 3 // ... 4 } 5 6 render(area) { 7 // ... 8 } 9 } 10 11 class Rectangle extends Shape { 12 constructor(width, height) { 13 super(); 14 this.width = width; 15 this.height = height; 16 } 17 18 getArea() { 19 return this.width * this.height; 20 } 21 } 22 23 class Square extends Shape { 24 constructor(length) { 25 super(); 26 this.length = length; 27 } 28 29 getArea() { 30 return this.length * this.length; 31 } 32 } 33 34 function renderLargeShapes(shapes) { 35 shapes.forEach((shape) => { 36 const area = shape.getArea(); 37 shape.render(area); 38 }); 39 } 40 41 const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; 42 renderLargeShapes(shapes);
接口隔离原则
JavaScript 几乎没有接口的概念,所以这条原则很少被使用。官方定义是“客户端不应该依赖它不需要的接口”,也就是接口最小化,把接口解耦。
Bad:
1 class DOMTraverser { 2 constructor(settings) { 3 this.settings = settings; 4 this.setup(); 5 } 6 7 setup() { 8 this.rootNode = this.settings.rootNode; 9 this.animationModule.setup(); 10 } 11 12 traverse() { 13 // ... 14 } 15 } 16 17 const $ = new DOMTraverser({ 18 rootNode: document.getElementsByTagName('body'), 19 animationModule() {} // Most of the time, we won't need to animate when traversing. 20 // ... 21 });
Good:
1 class DOMTraverser { 2 constructor(settings) { 3 this.settings = settings; 4 this.options = settings.options; 5 this.setup(); 6 } 7 8 setup() { 9 this.rootNode = this.settings.rootNode; 10 this.setupOptions(); 11 } 12 13 setupOptions() { 14 if (this.options.animationModule) { 15 // ... 16 } 17 } 18 19 traverse() { 20 // ... 21 } 22 } 23 24 const $ = new DOMTraverser({ 25 rootNode: document.getElementsByTagName('body'), 26 options: { 27 animationModule() {} 28 } 29 });
依赖反转原则
说就两点:
高层次模块不能依赖低层次模块,它们依赖于抽象接口。
抽象接口不能依赖具体实现,具体实现依赖抽象接口。
总结下来就两个字,解耦。
Bad:
1 // 库存查询 2 class InventoryRequester { 3 constructor() { 4 this.REQ_METHODS = ['HTTP']; 5 } 6 7 requestItem(item) { 8 // ... 9 } 10 } 11 12 // 库存跟踪 13 class InventoryTracker { 14 constructor(items) { 15 this.items = items; 16 17 // 这里依赖一个特殊的请求类,其实我们只是需要一个请求方法。 18 this.requester = new InventoryRequester(); 19 } 20 21 requestItems() { 22 this.items.forEach((item) => { 23 this.requester.requestItem(item); 24 }); 25 } 26 } 27 28 const inventoryTracker = new InventoryTracker(['apples', 'bananas']); 29 inventoryTracker.requestItems();
Good:
1 // 库存跟踪 2 class InventoryTracker { 3 constructor(items, requester) { 4 this.items = items; 5 this.requester = requester; 6 } 7 8 requestItems() { 9 this.items.forEach((item) => { 10 this.requester.requestItem(item); 11 }); 12 } 13 } 14 15 // HTTP 请求 16 class InventoryRequesterHTTP { 17 constructor() { 18 this.REQ_METHODS = ['HTTP']; 19 } 20 21 requestItem(item) { 22 // ... 23 } 24 } 25 26 // webSocket 请求 27 class InventoryRequesterWS { 28 constructor() { 29 this.REQ_METHODS = ['WS']; 30 } 31 32 requestItem(item) { 33 // ... 34 } 35 } 36 37 // 通过依赖注入的方式将请求模块解耦,这样我们就可以很轻易的替换成 webSocket 请求。 38 const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterHTTP()); 39 inventoryTracker.requestItems();
测试
随着项目变得越来越庞大,时间线拉长,有的老代码可能半年都没碰过,如果此时上线,你有信心这部分代码能正常工作吗?测试的覆盖率和你的信心是成正比的。
PS: 如果你发现你的代码很难被测试,那么你应该优化你的代码了。
单一化
Bad:
1 import assert from 'assert'; 2 3 describe('MakeMomentJSGreatAgain', () => { 4 it('handles date boundaries', () => { 5 let date; 6 7 date = new MakeMomentJSGreatAgain('1/1/2015'); 8 date.addDays(30); 9 assert.equal('1/31/2015', date); 10 11 date = new MakeMomentJSGreatAgain('2/1/2016'); 12 date.addDays(28); 13 assert.equal('02/29/2016', date); 14 15 date = new MakeMomentJSGreatAgain('2/1/2015'); 16 date.addDays(28); 17 assert.equal('03/01/2015', date); 18 }); 19 });
Good:
1 import assert from 'assert'; 2 3 describe('MakeMomentJSGreatAgain', () => { 4 it('handles 30-day months', () => { 5 const date = new MakeMomentJSGreatAgain('1/1/2015'); 6 date.addDays(30); 7 assert.equal('1/31/2015', date); 8 }); 9 10 it('handles leap year', () => { 11 const date = new MakeMomentJSGreatAgain('2/1/2016'); 12 date.addDays(28); 13 assert.equal('02/29/2016', date); 14 }); 15 16 it('handles non-leap year', () => { 17 const date = new MakeMomentJSGreatAgain('2/1/2015'); 18 date.addDays(28); 19 assert.equal('03/01/2015', date); 20 }); 21 });
异步
不再使用回调
不会有人愿意去看嵌套回调的代码,用 Promises 替代回调吧。
Bad:
1 import { get } from 'request'; 2 import { writeFile } from 'fs'; 3 4 get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => { 5 if (requestErr) { 6 console.error(requestErr); 7 } else { 8 writeFile('article.html', response.body, (writeErr) => { 9 if (writeErr) { 10 console.error(writeErr); 11 } else { 12 console.log('File written'); 13 } 14 }); 15 } 16 });
Good:
1 get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') 2 .then((response) => { 3 return writeFile('article.html', response); 4 }) 5 .then(() => { 6 console.log('File written'); 7 }) 8 .catch((err) => { 9 console.error(err); 10 });
Async/Await 比起 Promises 更简洁
Bad:
1 import { get } from 'request-promise'; 2 import { writeFile } from 'fs-promise'; 3 4 get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') 5 .then((response) => { 6 return writeFile('article.html', response); 7 }) 8 .then(() => { 9 console.log('File written'); 10 }) 11 .catch((err) => { 12 console.error(err); 13 });
Good:
1 import { get } from 'request-promise'; 2 import { writeFile } from 'fs-promise'; 3 4 async function getCleanCodeArticle() { 5 try { 6 const response = await get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'); 7 await writeFile('article.html', response); 8 console.log('File written'); 9 } catch(err) { 10 console.error(err); 11 } 12 }
错误处理
不要忽略抛异常
Bad:
1 try { 2 functionThatMightThrow(); 3 } catch (error) { 4 console.log(error); 5 }
Good:
1 try { 2 functionThatMightThrow(); 3 } catch (error) { 4 // 这一种选择,比起 console.log 更直观 5 console.error(error); 6 // 也可以在界面上提醒用户 7 notifyUserOfError(error); 8 // 也可以把异常传回服务器 9 reportErrorToService(error); 10 // 其他的自定义方法 11 }
不要忘了在 Promises 抛异常
Bad:
1 getdata() 2 .then((data) => { 3 functionThatMightThrow(data); 4 }) 5 .catch((error) => { 6 console.log(error); 7 });
Good:
1 getdata() 2 .then((data) => { 3 functionThatMightThrow(data); 4 }) 5 .catch((error) => { 6 // 这一种选择,比起 console.log 更直观 7 console.error(error); 8 // 也可以在界面上提醒用户 9 notifyUserOfError(error); 10 // 也可以把异常传回服务器 11 reportErrorToService(error); 12 // 其他的自定义方法 13 });
代码风格
代码风格是主观的,争论哪种好哪种不好是在浪费生命。市面上有很多自动处理代码风格的工具,选一个喜欢就行了,我们来讨论几个非自动处理的部分。
常量大写
Bad:
1 const DAYS_IN_WEEK = 7; 2 const daysInMonth = 30; 3 4 const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 5 const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles']; 6 7 function eraseDatabase() {} 8 function restore_database() {} 9 10 class animal {} 11 class Alpaca {}
Good:
1 const DAYS_IN_WEEK = 7; 2 const DAYS_IN_MONTH = 30; 3 4 const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 5 const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles']; 6 7 function eraseDatabase() {} 8 function restoreDatabase() {} 9 10 class Animal {} 11 class Alpaca {}
先声明后调用
就像我们看报纸文章一样,从上到下看,所以为了方便阅读把函数声明写在函数调用前面。
Bad:
1 class PerformanceReview { 2 constructor(employee) { 3 this.employee = employee; 4 } 5 6 lookupPeers() { 7 return db.lookup(this.employee, 'peers'); 8 } 9 10 lookupManager() { 11 return db.lookup(this.employee, 'manager'); 12 } 13 14 getPeerReviews() { 15 const peers = this.lookupPeers(); 16 // ... 17 } 18 19 perfReview() { 20 this.getPeerReviews(); 21 this.getManagerReview(); 22 this.getSelfReview(); 23 } 24 25 getManagerReview() { 26 const manager = this.lookupManager(); 27 } 28 29 getSelfReview() { 30 // ... 31 } 32 } 33 34 const review = new PerformanceReview(employee); 35 review.perfReview();
Good:
1 class PerformanceReview { 2 constructor(employee) { 3 this.employee = employee; 4 } 5 6 perfReview() { 7 this.getPeerReviews(); 8 this.getManagerReview(); 9 this.getSelfReview(); 10 } 11 12 getPeerReviews() { 13 const peers = this.lookupPeers(); 14 // ... 15 } 16 17 lookupPeers() { 18 return db.lookup(this.employee, 'peers'); 19 } 20 21 getManagerReview() { 22 const manager = this.lookupManager(); 23 } 24 25 lookupManager() { 26 return db.lookup(this.employee, 'manager'); 27 } 28 29 getSelfReview() { 30 // ... 31 } 32 } 33 34 const review = new PerformanceReview(employee); 35 review.perfReview();
注释
只有业务逻辑需要注释
代码注释不是越多越好。
Bad:
1 function hashIt(data) { 2 // 这是初始值 3 let hash = 0; 4 5 // 数组的长度 6 const length = data.length; 7 8 // 循环数组 9 for (let i = 0; i < length; i++) { 10 // 获取字符代码 11 const char = data.charCodeAt(i); 12 // 修改 hash 13 hash = ((hash << 5) - hash) + char; 14 // 转换为32位整数 15 hash &= hash; 16 } 17 }
Good:
1 function hashIt(data) { 2 let hash = 0; 3 const length = data.length; 4 5 for (let i = 0; i < length; i++) { 6 const char = data.charCodeAt(i); 7 hash = ((hash << 5) - hash) + char; 8 9 // 转换为32位整数 10 hash &= hash; 11 } 12 }
删掉注释的代码
git 存在的意义就是保存你的旧代码,所以注释的代码赶紧删掉吧。
Bad:
1 doStuff(); 2 // doOtherStuff(); 3 // doSomeMoreStuff(); 4 // doSoMuchStuff();
Good:
doStuff();
javascript
不要记日记
记住你有 git!,git log 可以帮你干这事。
Bad:
1 /** 2 * 2016-12-20: 删除了 xxx 3 * 2016-10-01: 改进了 xxx 4 * 2016-02-03: 删除了第12行的类型检查 5 * 2015-03-14: 增加了一个合并的方法 6 */ 7 function combine(a, b) { 8 return a + b; 9 }
Good:
1 function combine(a, b) { 2 return a + b; 3 }
注释不需要高亮
注释高亮,并不能起到提示的作用,反而会干扰你阅读代码。
Bad:
1 //////////////////////////////////////////////////////////////////////////////// 2 // Scope Model Instantiation 3 //////////////////////////////////////////////////////////////////////////////// 4 $scope.model = { 5 menu: 'foo', 6 nav: 'bar' 7 }; 8 9 //////////////////////////////////////////////////////////////////////////////// 10 // Action setup 11 //////////////////////////////////////////////////////////////////////////////// 12 const actions = function() { 13 // ... 14 };
Good:
1 $scope.model = { 2 menu: 'foo', 3 nav: 'bar' 4 }; 5 6 const actions = function() { 7 // ... 8 };