变量
使用有意义,可读性好的变量名
对功能类似的变量名采用统一的命名风格
1//bad2getUserInfo()3getClientData()4getCustomerRecord()56//good7getUser()
不要有魔术变量,并且变量使用易于检索的名字
代码是要有解释性的
1//bad2const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/3saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2])45//good6const ADDRESS = 'One Infinite Loop, Cupertino 95014'7var cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/8var match = ADDRESS.match(cityStateRegex)9var city = match[1]10var state = match[2]11saveCityState(city, state)
避免重复的描述
当类/对象名已经有意义时,对其变量进行命名不需要再次重复
1//bad2var Car = {3 carMake: 'Honda',4 carModel: 'Accord',5 carColor: 'Blue',6}78function paintCar(car) {9 car.carColor = 'Red'10}1112//good13var Car = {14 make: 'Honda',15 model: 'Accord',16 color: 'Blue',17}1819function paintCar(car) {20 car.color = 'Red'21}
利用参数默认值代替短路语法或者条件语句
1//bad2function createMicrobrewery(name) {3 const breweryName = name || 'Hipster Brew Co.' // ...4}5//good6function createMicrobrewery(name = 'Hipster Brew Co.') {7 // ...8}
函数
函数参数(2 个或更少)
在需要很多参数的时候就可以使用一个对象来处理
1//bad2function createMenu(title, body, buttonText, cancellable) {3 // ...4}56//good7function createMenu({ title, body, buttonText, cancellable }) {8 // ...9}1011createMenu({12 title: 'Foo',13 body: 'Bar',14 buttonText: 'Baz',15 cancellable: true,16})
一个函数只做一件事
目前这是软件工程中最重要的原则。
1//bad2function emailClients(clients) {3 clients.forEach((client) => {4 const clientRecord = database.lookup(client)5 if (clientRecord.isActive()) {6 email(client)7 }8 })9}1011//good12function emailActiveClients(clients) {13 clients.filter(isActiveClient).forEach(email)14}1516function isActiveClient(client) {17 const clientRecord = database.lookup(client)18 return clientRecord.isActive()19}
函数名称要是解释性的,说明它做的事
如果你有多个层次的抽象,那么你的函数通常做了太多事情,此时应该拆分函数使其易于复用和易于测试。
1//bad2function parseBetterJSAlternative(code) {3 const REGEXES = [4 // ...5 ]67 const statements = code.split(' ')8 const tokens = []9 REGEXES.forEach((REGEX) => {10 statements.forEach((statement) => {11 // ...12 })13 })1415 const ast = []16 tokens.forEach((token) => {17 // lex...18 })1920 ast.forEach((node) => {21 // parse...22 })23}2425//good26function parseBetterJSAlternative(code) {27 const tokens = tokenize(code)28 const ast = lexer(tokens)29 ast.forEach((node) => {30 // parse...31 })32}3334function tokenize(code) {35 const REGEXES = [36 // ...37 ]3839 const statements = code.split(' ')40 const tokens = []41 REGEXES.forEach((REGEX) => {42 statements.forEach((statement) => {43 tokens.push(/* ... */)44 })45 })4647 return tokens48}4950function lexer(tokens) {51 const ast = []52 tokens.forEach((token) => {53 ast.push(/* ... */)54 })5556 return ast57}
删除重复代码
任何情况下,都不要有重复的代码。没有任何原因,它很可能是阻碍你成为专业开发者的最糟糕的一件事。重复代码意味着你要修改某些逻辑的时候要修改不止一个地方的代码。
1//bad2function showDeveloperList(developers) {3 developers.forEach(developer => {4 const expectedSalary = developer.calculateExpectedSalary()5 const experience = developer.getExperience()6 const githubLink = developer.getGithubLink()7 const data = {8 expectedSalary,9 experience,10 githubLink11 }1213 render(data)14 })15}1617function showManagerList(managers) {18 managers.forEach(manager => {19 const expectedSalary = manager.calculateExpectedSalary()20 const experience = manager.getExperience()21 const portfolio = manager.getMBAProjects()22 const data = {23 expectedSalary,24 experience,25 portfolio26 }2728 render(data)29 })30}3132//good33function showEmployeeList(employees) {34 employees.forEach(employee => {35 const expectedSalary = employee.calculateExpectedSalary()36 const experience = employee.getExperience()3738 const data = {39 expectedSalary,40 experience41 }4243 switch (employee.type) {44 case 'manager':45 data.portfolio = employee.getMBAProjects()46 breakcase 'developer':47 data.githubLink = employee.getGithubLink()48 break49 }5051 render(data)52 })53}
用 Object.assign 设置默认对象
1//bad2const menuConfig = {3 title: null,4 body: 'Bar',5 buttonText: null,6 cancellable: true,7}89function createMenu(config) {10 config.title = config.title || 'Foo'11 config.body = config.body || 'Bar'12 config.buttonText = config.buttonText || 'Baz'13 config.cancellable = config.cancellable !== undefined ? config.cancellable : true14}1516createMenu(menuConfig)1718//good19const menuConfig = {20 title: 'Order',21 // User did not include 'body' key22 buttonText: 'Send',23 cancellable: true,24}2526function createMenu(config) {27 config = Object.assign(28 {29 title: 'Foo',30 body: 'Bar',31 buttonText: 'Baz',32 cancellable: true,33 },34 config35 )3637 // config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}// ...38}3940createMenu(menuConfig)
不要把标记用作函数参数
标记告诉你的用户这个函数做的事情不止一件。但是函数应该只做一件事。如果你的函数中会根据某个布尔参数产生不同的分支,那就拆分这个函数。
1//bad2function createFile(name, temp) {3 if (temp) {4 fs.create('./temp/' + name)5 } else {6 fs.create(name)7 }8}910//good11function createTempFile(name) {12 fs.create('./temp/' + name)13}1415function createFile(name) {16 fs.create(name)17}
避免副作用
如果一个函数不是获取一个输入的值并返回其它值,它就有可能产生副作用。
1//bad2// 下面的函数使用了全局变量。3// 如果有另一个函数在使用 name,现在可能会因为 name 变成了数组而不能正常运行。4var name = 'Ryan McDermott'56function splitIntoFirstAndLastName() {7 name = name.split(' ')8}910splitIntoFirstAndLastName()1112console.log(name) // ['Ryan', 'McDermott'];1314//good15function splitIntoFirstAndLastName(name) {16 return name.split(' ')17}1819var name = 'Ryan McDermott'20var newName = splitIntoFirstAndLastName(name)2122console.log(name) // 'Ryan McDermott';23console.log(newName) // ['Ryan', 'McDermott'];
总是返回一个新的对象或者数组,而不是在基础上修改
1//bad2const addItemToCart = (cart, item) => {3 cart.push({ item, date: Date.now() })4}56//good7const addItemToCart = (cart, item) => {8 return [...cart, { item, date: Date.now() }]9}
不要使用全局函数
1//bad2Array.prototype.diff = function diff(comparisonArray) {3 const hash = new Set(comparisonArray)4 return this.filter((elem) => !hash.has(elem))5}67//good8class SuperArray extends Array {9 diff(comparisonArray) {10 const hash = new Set(comparisonArray)11 return this.filter((elem) => !hash.has(elem))12 }13}
喜欢上更好的函数式编程而不是命令式编程 封装条件
1//bad2if (fsm.state === 'fetching' && isEmpty(listNode)) {3 // ...4}56//good7function shouldShowSpinner(fsm, listNode) {8 return fsm.state === 'fetching' && isEmpty(listNode)9}1011if (shouldShowSpinner(fsmInstance, listNodeInstance)) {12 // ...13}
避免否定条件
1//bad2function isDOMNodeNotPresent(node) {3 // ...4}56if (!isDOMNodeNotPresent(node)) {7 // ...89//good10function isDOMNodePresent(node) {11 // ...12}1314if (isDOMNodePresent(node)) {15 // ...16}
避免条件
这似乎是个不可能完成的任务。大多数人第一次听到这个的时候会说,“没有 if 语句我该怎么办?”回答是在多数情况下都可以使用多态来实现相同的任务。第二个问题通常是,“那太好了,不过我为什么要这么做呢?”答案在于我们之前了解过整洁的概念:一个函数应该只做一件事情。如果你的类和函数有 if 语句,就意味着你的函数做了更多的事。记住,只做一件事。
1//bad2class Airplane {3 //...4 getCruisingAltitude() {5 switch (this.type) {6 case '777':7 return getMaxAltitude() - getPassengerCount()8 case 'Air Force One':9 return getMaxAltitude()10 case 'Cessna':11 return getMaxAltitude() - getFuelExpenditure()12 }13 }14}1516//good17class Airplane {18 // ...19}2021class Boeing777 extends Airplane {22 // ...23 getCruisingAltitude() {24 return this.getMaxAltitude() - this.getPassengerCount()25 }26}2728class AirForceOne extends Airplane {29 // ...30 getCruisingAltitude() {31 return this.getMaxAltitude()32 }33}3435class Cessna extends Airplane {36 // ...37 getCruisingAltitude() {38 return this.getMaxAltitude() - this.getFuelExpenditure()39 }40}
使用 Array.includes 处理多重条件
1//bad2function test(fruit) {3 if (fruit == 'apple' || fruit == 'strawberry') {4 console.log('red')5 }6}78//good9function test(fruit) {10 // 把条件提取到数组中const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries']1112 if (redFruits.includes(fruit)) {13 console.log('red')14 }15}
少写嵌套,尽早返回 //当发现无效条件时尽早返回
1function test(fruit, quantity) {2 const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries']34 // 条件 1:尽早抛出错误if (!fruit) throw new Error('No fruit!')56 // 条件2:必须为红色if (redFruits.includes(fruit)) {7 console.log('red')89 // 条件 3:必须是大量存在if (quantity > 10) {10 console.log('big quantity')11 }12 }13}
相较于 switch,Map / Object 也许是更好的选择
1//根据颜色打印出各种水果2//bad3function test(color) {4 // 使用 switch case 来找到对应颜色的水果switch (color) {5 case 'red':6 return ['apple', 'strawberry']7 case 'yellow':8 return ['banana', 'pineapple']9 case 'purple':10 return ['grape', 'plum']11 default:12 return []13 }14}1516//测试结果17test(null) // []18test('yellow') // ['banana', 'pineapple']1920//good2122//1.通过对象字面量来实现23// 使用对象字面量来找到对应颜色的水果24const fruitColor = {25 red: ['apple', 'strawberry'],26 yellow: ['banana', 'pineapple'],27 purple: ['grape', 'plum']28}2930function test(color) {31 return fruitColor[color] || []32}3334//2. 使用 Map 来找到对应颜色的水果35const fruitColor = new Map()36 .set('red', ['apple', 'strawberry'])37 .set('yellow', ['banana', 'pineapple'])38 .set('purple', ['grape', 'plum'])3940function test(color) {41 return fruitColor.get(color) || []42}4344//3. 使用 Array.filter 实现同样的效果45const fruits = [46 { name: 'apple', color: 'red' },47 { name: 'strawberry', color: 'red' },48 { name: 'banana', color: 'yellow' },49 { name: 'pineapple', color: 'yellow' },50 { name: 'grape', color: 'purple' },51 { name: 'plum', color: 'purple' }52]5354function test(color) {55 // 使用 Array filter 来找到对应颜色的水果5657 return fruits.filter(f => f.color == color)58}
使用 Array.every 和 Array.some 来处理全部/部分满足条件
1//检查是否所有的水果都是红色的2//bad3const fruits = [4 { name: 'apple', color: 'red' },5 { name: 'banana', color: 'yellow' },6 { name: 'grape', color: 'purple' }7]89function test() {10 let isAllRed = true1112 // 条件:所有的水果都必须是红色for (let f of fruits) {13 if (!isAllRed) break14 isAllRed = f.color == 'red'15 }1617 console.log(isAllRed) // false18}1920//good21const fruits = [22 { name: 'apple', color: 'red' },23 { name: 'banana', color: 'yellow' },24 { name: 'grape', color: 'purple' }25]2627function test() {28 // 条件:(简短形式)所有的水果都必须是红色const isAllRed = fruits.every(f => f.color == 'red')2930 console.log(isAllRed) // false31}3233//想要检查是否有至少一个水果是红色的,我们可以使用 Array.some 仅用一行代码就实现出来。34const fruits = [35 { name: 'apple', color: 'red' },36 { name: 'banana', color: 'yellow' },37 { name: 'grape', color: 'purple' }38]3940function test() {41 // 条件:至少一个水果是红色的const isAnyRed = fruits.some(f => f.color == 'red')4243 console.log(isAnyRed) // true44}
删除不用的代码
不用的代码和重复的代码一样糟糕。在代码库中保留无用的代码是毫无道理的事情。如果某段代码用不到,那就删掉它!如果你以后需要它,仍然可以从代码库的历史版本中找出来。
对象和数据结构
类
单一职责原则(SRP)
正如《代码整洁之道》所说,“不应该有超过一个原因来改变类”。往一个类里塞进许多功能是件诱人的事情,就像在坐飞机的时候只带一个手提箱一样。这带来的问题是,你的类不会在概念上有凝聚力,会有很多因素造成对它的改变。让你的类需要改变的次数最少是件非常重要的事情。这是因为如果一个类里塞入了太多功能,你只修改它的一部分,可能会让人难以理解它为何会影响代码库中其它相关模块。
1//bad2class UserSettings {3 constructor(user) {4 this.user = user5 }67 changeSettings(settings) {8 if (this.verifyCredentials(user)) {9 // ...10 }11 }1213 verifyCredentials(user) {14 // ...15 }16}1718//good19class UserAuth {20 constructor(user) {21 this.user = user22 }2324 verifyCredentials() {25 // ...26 }27}2829class UserSettings {30 constructor(user) {31 this.user = user32 this.auth = new UserAuth(user)33 }3435 changeSettings(settings) {36 if (this.auth.verifyCredentials()) {37 // ...38 }39 }40}
开放封装原则(OCP) 正如 Bertrand Meyer 所说,“软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。”这是什么意思呢?这个原则基本上规定了你应该允许用户扩展你的模块,但不需要打开 .js 源代码文件来进行编辑。
1//bad2class AjaxRequester {3 constructor() {4 // 如果我们需要另一个 HTTP 方法,比如 DELETE,该怎么办?// 我们必须打开这个文件然后手工把它加进去this.HTTP_METHODS = ['POST', 'PUT', 'GET']5 }67 get(url) {8 // ...9 }10}1112//good13class AjaxRequester {14 constructor() {15 this.HTTP_METHODS = ['POST', 'PUT', 'GET']16 }1718 get(url) {19 // ...20 }2122 addHTTPMethod(method) {23 this.HTTP_METHODS.push(method)24 }25}
多用 ES6 类语法,少用 ES5 构造函数语法 使用方法链 在类中,只需要在每个函数结束前返回 this,就实现了链式调用的类方法。
1//bad2class Car {3 constructor() {4 this.make = 'Honda'this.model = 'Accord'this.color = 'white'5 }67 setMake(make) {8 this.name = name9 }1011 setModel(model) {12 this.model = model13 }1415 setColor(color) {16 this.color = color17 }1819 save() {20 console.log(this.make, this.model, this.color)21 }22}2324let car = new Car()25car.setColor('pink')26car.setMake('Ford')27car.setModel('F-150')28car.save()2930//good31class Car {32 constructor() {33 this.make = 'Honda'this.model = 'Accord'this.color = 'white'34 }3536 setMake(make) {37 this.name = name38 // NOTE: 返回 this 以实现链式调用return this39 }4041 setModel(model) {42 this.model = model43 // NOTE: 返回 this 以实现链式调用return this44 }4546 setColor(color) {47 this.color = color48 // NOTE: 返回 this 以实现链式调用return this49 }5051 save() {52 console.log(this.make, this.model, this.color)53 }54}5556let car = new Car()57 .setColor('pink')58 .setMake('Ford')59 .setModel('F-150')60 .save()
多用组合,少用继承
1//bad2class Employee {3 constructor(name, email) {4 this.name = name5 this.email = email6 }78 // ...9}1011// 这样不好,因为 Employees "拥有" 税务数据。EmployeeTaxData 不是属于 Employee 的一个类型12class EmployeeTaxData extends Employee {13 constructor(ssn, salary) {14 super()15 this.ssn = ssn16 this.salary = salary17 }1819 // ...20}2122//good23class Employee {24 constructor(name, email) {25 this.name = name26 this.email = email27 }2829 setTaxData(ssn, salary) {30 this.taxData = new EmployeeTaxData(ssn, salary)31 }32 // ...33}3435class EmployeeTaxData {36 constructor(ssn, salary) {37 this.ssn = ssn38 this.salary = salary39 }4041 // ...42}
并发(Concurrency) 使用 Promise 而不是回调 async/await 比 Promise 还整洁
1//bad2require('request-promise')3 .get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')4 .then(function (response) {5 return require('fs-promise').writeFile('article.html', response)6 })7 .then(function () {8 console.log('File written')9 })10 .catch(function (err) {11 console.error(err)12 })1314//good15async function getCleanCodeArticle() {16 try {17 var request = await require('request-promise')18 var response = await request.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')19 var fileHandle = await require('fs-promise')2021 await fileHandle.writeFile('article.html', response)22 console.log('File written')23 } catch (err) {24 console.log(err)25 }26}
测试 测试比生产更重要。如果你不进行测试,或者测试的量不够,那你就不能肯定你写的代码不会造成破坏。测试数量依靠你的开发团队来决定,但 100% 覆盖率(所有语句和分支)能让你拥有巨大的信心,也能使程序员们安心。
每次测试一个概念
1//bad2const assert = require('assert')34describe('MakeMomentJSGreatAgain', function () {5 it('handles date boundaries', function () {6 let date78 date = new MakeMomentJSGreatAgain('1/1/2015')9 date.addDays(30)10 date.shouldEqual('1/31/2015')1112 date = new MakeMomentJSGreatAgain('2/1/2016')13 date.addDays(28)14 assert.equal('02/29/2016', date)1516 date = new MakeMomentJSGreatAgain('2/1/2015')17 date.addDays(28)18 assert.equal('03/01/2015', date)19 })20})2122//good23const assert = require('assert')2425describe('MakeMomentJSGreatAgain', function () {26 it('handles 30-day months', function () {27 let date = new MakeMomentJSGreatAgain('1/1/2015')28 date.addDays(30)29 date.shouldEqual('1/31/2015')30 })3132 it('handles leap year', function () {33 let date = new MakeMomentJSGreatAgain('2/1/2016')34 date.addDays(28)35 assert.equal('02/29/2016', date)36 })3738 it('handles non-leap year', function () {39 let date = new MakeMomentJSGreatAgain('2/1/2015')40 date.addDays(28)41 assert.equal('03/01/2015', date)42 })43})
不要把注释掉的代码留在代码库中 版本控制存在的原因就是保存你的历史代码。
避免在源文件中添加版权注释 这是代码文件树顶层的 LICENSE 文件应该干的事情。
全局常量和线程安全(在 let 和 const 之间,建议优先使用 const,尤其是在全局环境) JavaScript 编译器会对 const 进行优化,所以多使用 const,有利于提高程序的运行效率,也就是说 let 和 const 的本质区别,其实是编译器内部的处理不同。
1// bad2var a = 1,3 b = 2,4 c = 356// good7const a = 18const b = 29const c = 31011// best12const [a, b, c] = [1, 2, 3]
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。 使用数组成员对变量赋值时,优先使用解构赋值
1const arr = [1, 2, 3, 4]23// bad4const first = arr[0]5const second = arr[1]67// good8const [first, second] = arr
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用 Object.assign 方法。 使用扩展运算符(…)拷贝数组 使用 Array.from 方法,将类似数组的对象转为数组 箭头函数取代 Function.prototype.bind,不应再用 self/_this/that 绑定 this。
1// bad2const self = this3const boundMethod = function (...params) {4 return method.apply(self, params)5}67// acceptable8const boundMethod = method.bind(this)910// best11const boundMethod = (...params) => method.apply(this, params)
简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。 所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数
1// bad2function divide(a, b, option = false) {}34// good5function divide(a, b, { option = false } = {}) {}
不要在函数体内使用 arguments 变量,使用 rest 运算符(…)代替。 使用 Map,而不是 Object 注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要 key: value 的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
1let map = new Map(arr)23for (let key of map.keys()) {4 console.log(key)5}67for (let value of map.values()) {8 console.log(value)9}1011for (let item of map.entries()) {12 console.log(item[0], item[1])13}
总是用 Class,取代需要 prototype 的操作。 因为 Class 的写法更简洁,更易于理解。
1// bad2function Queue(contents = []) {3 this._queue = [...contents]4}5Queue.prototype.pop = function () {6 const value = this._queue[0]7 this._queue.splice(0, 1)8 return value9}1011// good12class Queue {13 constructor(contents = []) {14 this._queue = [...contents]15 }16 pop() {17 const value = this._queue[0]18 this._queue.splice(0, 1)19 return value20 }21}
使用 import 取代 require Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。
1// bad2const moduleA = require('moduleA')3const func1 = moduleA.func14const func2 = moduleA.func256// good7import { func1, func2 } from 'moduleA'
使用 export 取代 module.exports。
1// commonJS的写法2var React = require('react')34var Breadcrumbs = React.createClass({5 render() {6 return <nav />7 },8})910module.exports = Breadcrumbs1112// ES6的写法13import React from 'react'1415class Breadcrumbs extends React.Component {16 render() {17 return <nav />18 }19}2021export default Breadcrumbs
如果模块只有一个输出值,就使用 export default,如果模块有多个输出值,就不使用 export default,export default 与普通的 export 不要同时使用。
如果模块默认输出一个函数,函数名的首字母应该小写。
1function makeStyleGuide() {}23export default makeStyleGuide
如果模块默认输出一个对象,对象名的首字母应该大写。
1const StyleGuide = {2 es6: {},3}45export default StyleGuide
参考资料: