Javascript 代码整洁之道

310 阅读10分钟

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. 普通的工程师堆砌代码,优秀的工程师优雅代码,卓越的工程师简化代码。

一、变量

1.使用let声明可变量,使用const声明不变量和常量

反例:

var firstName = 'Sky'

正例:

// 年龄长了一岁
let age = 18
age = age + 1

// 在方法内部的不变参数用驼峰命名
const firstName = 'Sky'
// 公用的常量使用大写下划线命名
const MAX_DEVICE_NUM = 10

2.使用有意义,可读性好的变量名。

  • 如果使用缩写,不要随便创造,请用通用的,否则宁愿用英文全称 反例:
// 以下都是我在项目维护中亲眼看到过的神仙命名
const curTime = moment()
const par = {}
const mess = `Dont't touch me`
const inf = '清明放三天'

正例:

const currentTime = moment()

const params = {}
const parameters = {}

const msg = `Dont't touch me`
const message = `Dont't touch me`

const info = '清明放三天'
const infomation = '清明放三天'

3.使用说明变量,显式优于隐式。

反例:

const time = '11:22:33'
const timeRegexp = /[\d]{2}/g
const timeMatchList = time.match(timeRegexp) // ['11', '22', '33']
saveTime(timeMatchList[0], timeMatchList[1], timeMatchList[2])

正例:

const time = '11:22:33'
const timeRegexp = /[\d]{2}/g
const timeMatchList = time.match(timeRegexp) // ['11', '22', '33']
const [hour, minute, second] = timeMatchList
saveTime(hour, minute, second)

4.避免重复的描述

反例:

const user = {
    userFirstName: 'Sky',
    userFamilyName: 'Lin',
    userAge: 18
}

正例:

const user = {
    firstName: 'Sky',
    familyName: 'Lin',
    age: 18
}

5.封装同一功能下的变量

反例:

// 分页参数
const current = 0
const size = 0
const total = 0
const pages = 0

正例:

const pagination = {
    current: 0,
    size: 0,
    total: 0,
    pages: 0
}

6.使用小驼峰命名

反例:

const _url = 'https://www.qq.com'
const MaxNum = 100
const phonenumber = '15889912345'

正例:

const url = 'https://www.qq.com'
const maxNum = 100
const phoneNumber = '15889912345'

7.涉及专有名词的变量命名(这里采用微软的命名规则)

  • 如果单词缩写是两个字母的,则全大写
  • 如果单词缩写超过两个字母,则第一个字母大写,其余字母小写 反例:
const websiteIp = '192.168.1.1'
const webHTML = `<html>...</html>` 
const musicVIPLevel = 5 

正例:

const websiteIP = '192.168.1.1'
const webHtml = `<html>...</html>` 
const musicVipLevel = 5

8.变量类型为boolean,变量前加上is,has,can,should这样的词会更明确

反例:

const worker = {
    overtime: true,
    tencentVedioVip: false,
    house: fasle,
    car: false
}

正例:

// 没车没房没腾讯视频会员可以加班的打工人
const worker = {
    canOvertime: true,
    isTencentVedioVip: false,
    hasHouse: fasle,
    hasCar: fasle
}

7.别用魔法数

反例:

function selectCoursesBySex(sex) {
    if(sex === 1) {
        return '足球'
    }
    if(sex === 2) {
        return '舞蹈' 
    }
    return '算命'
}

正例:

const SEX = {
    MALE: 1,
    FEMALE: 2
}

function selectCoursesBySex(sex) {
    if(sex === SEX.MALE) {
        return '足球'
    }
    if(sex === SEX.FEMALE) {
        return '舞蹈' 
    }
    return '算命'
}

8.不要依赖JS的隐式转换。

  • 作为一个开发要有能力去控制自己的代码,要清楚自己的数据,到底是什么类型,坚决用===,而不是== 反例
const myMoney = '2'
const yourMoney = 2
if(myMoney == yourMoney) { //do something }
if(yourMoney){ //do something }

正例

const moneyStr = '2'
const myMoney = parseInt(moneyStr)
const yourMoney = 2
if(myMoney === yourMoney) { //do something }
if(yourMoney !== 0){ //do something }

二、函数

1.函数参数 (理想情况下应不超过 2 个)

  • 限制函数参数数量很有必要,这么做使得在测试函数时更加轻松。过多的参数将导致难以采用有效的测试用例对函数的各个参数进行测试。应避免三个以上参数的函数
  • 通常情况下,参数超过两个意味着函数功能过于复杂,这时需要重新优化你的函数。当确实需要多个参数时,大多情况下可以考虑这些参数封装成一个对象
  • JS 定义对象非常方便,当需要多个参数时,可以使用一个对象进行替代

反例:

function initUser(firstName, familyName, age) {
    // do something
}
const firstName = 'Sky'
const familyName = 'Lin'
const age = 18
initUser(firstName, familyName, age)

正例:

function initUser(user) {
    // do something
}
const user = {
    firstName: 'Sky',
    familyName: 'Lin',
    age: 18
}
initUser(user)

2.函数名应用【动词+名词】规则命名,明确表明其功能

  • 纯名词命名不好,方法应该表达【做什么】而不是【什么】
  • 用英文单词,别用拼音,除非是baidu这种英文和拼音一样的名词 反例:
function customerList(params) {}
function message(str) {}
function queryYuanGongById(id) {}

正例:

function queryCustomerList(params) {}
function sendMessage(message) {}
function queryStaffById(id) {}
  • 为作用域大的名字采用更长的名字,作用域小的使用短名字
  • 别害怕长名称,长而具有描述性的名称比短而令人费解的名称好
  • 最好的方法注释就是好的命名
  • 使用更专业的词,比如不用get而使用query、fetch或者download 反例:
import { queryCustomerList } from '@/api'
async function queryCustomer(params) {
    try {
        const response = await queryCustomerList(params)
    } catch(e) {
        console.log(e)
    }
}

// 企业微信=qywx,是我职业生涯以来见过最烂的命名之一,官方名称是WeCom
function getQywxUserInfo(params) {}

正例:

import { queryCustomer } from '@/api'
async function queryCustomerList(params) {
    try {
        const response = await queryCustomer(params)
    } catch(e) {
        console.log(e)
    }
}

function queryWeComUserInfo(params) {}

3.函数功能的单一性

  • 这是软件功能中最重要的原则之一。功能不单一的函数将导致难以重构、测试和理解。功能单一的函数易于重构,并使代码更加干净。 反例:
const MSG_STATUS = {
    INIT: 0,
    SENDING: 1,
    DONE: 2,
    FAILED: 3
}

function massPhoneMessage(messages) {
    messages.every(msg => {
        const { status, content } = msg
        if(status === MSG_STATUS.SENDING || status === MSG_STATUS.DONE) {
            return true
        }
        
        // 失败和新建的消息,需要发短信
        sendMessage(content)
        return true
    })
}

正例:

function massPhoneMessage(messages) {
    messages.every(sendMessageIfNeed)
}

function sendMessageIfNeed(msg) {
    const { status, content } = msg
    if(status === MSG_STATUS.SENDING || status === MSG_STATUS.DONE) {
        return true
    }

    // 失败和新建的消息,需要发短信
    sendMessage(content)
    return true
}

4.函数应该只做一层抽象

  • 当函数的需要的抽象多于一层时通常意味着函数功能过于复杂,需将其进行分解以提高其可重用性和可测试性。 反例:
async function submit(userInfo) {
    // 校验
    const { name, age, examTime } = userInfo
    if(name === null || name === '') {
        message.error('请输入姓名!')
        return
    }
    if(age < 18) {
        message.error('未满18岁不能申请驾照!')
        return
    }
    if(examTime === null) {
        message.error('请选择考试时间!')
        return
    }
    
    // 构造参数
    const examTimeStr = examTime.format('YYYY-MM-DD HH:mm:ss')
    const params = {
        name: name.trim(),
        examTimeStr,
        age
    }
    
    // 提交数据
    try {
        const response = await registerDrivingLicenseTest(params)
        // ...
    } catch(e) {
        console.log(e)
    }
}

正例:

async function submit(userInfo) {
    if(!validateUserInfo(userInfo)){
        return
    }
    
    const params = dealParams(userInfo)
    // 提交数据
    try {
        const response = await registerDrivingLicenseTest(params)
        // ...
    } catch(e) {
        console.log(e)
    }
}

function validateUserInfo({ name, age, examTime }) {
    if(name === null || name === '') {
        message.error('请输入姓名!')
        return false
    }
    if(age < 18) {
        message.error('未满18岁不能申请驾照!')
        return false
    }
    if(examTime === null) {
        message.error('请选择考试时间!')
        return false
    }
    return true
}

function dealParams({ name, age, examTime }) {
    const examTimeStr = examTime.format('YYYY-MM-DD HH:mm:ss')
    return {
        name: name.trim(),
        examTimeStr,
        age
    }
}

5.移除重复的代码,超过三行重复代码就应该封装成方法

  • 方法不应该有100行那么长,20行封顶最好 反例:
async function addUserInfo(user) {
    const { name, age } = user
    if(name === null || name ===''){
        message.error('请输入姓名!')
        return
    }
    if(age > 18){
        message.error('申请年龄需要大于18岁!')
        return
    }
    
    try {
        const response = await addUser(user)
        // ...
    } catch(e) {
        console.log(e)
    }
}

async function updateUserInfo(user) {
    const { name, age } = user
    if(name === null || name ===''){
        message.error('请输入姓名!')
        return
    }
    if(age > 18){
        message.error('申请年龄需要大于18岁!')
        return
    }
    
    try {
        const response = await updateUser(user)
        // ...
    } catch(e) {
        console.log(e)
    }
}

正例:

function validateUserInfo({ name, age }) {
    if(name === null || name === '') {
        message.error('请输入姓名!')
        return false
    }
    if(age < 18) {
        message.error('未满18岁不能申请驾照!')
        return false
    }
    return true
}

async function addUserInfo(user) {
     if(!validateUserInfo(user)){
        return
    }
    
    try {
        const response = await addUser(user)
        // ...
    } catch(e) {
        console.log(e)
    }
}

async function updateUserInfo(user) {
     if(!validateUserInfo(user)){
        return
    }
    
    try {
        const response = await updateUser(user)
        // ...
    } catch(e) {
        console.log(e)
    }
}

6.采用默认参数精简代码

反例

function initUser(name, age) {
    const user = {
        name: name || '',
        age: age || 1
    }
}

正例

function initUser(name = '', age = 1) {
    const user = {
        name,
        age
    }
}

7.不要使用标记(Flag)作为函数参数

  • 这通常意味着函数的功能的单一性已经被破坏。此时应考虑对函数进行再次划分。 反例
function createFile(name, temp) {
  if (temp) {
    fs.create('./temp/' + name)
  } else {
    fs.create(name)
  }
}

正例

function createTempFile(name) {
  fs.create('./temp/' + name)
}

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

8.采用函数式编程

  • 函数式的编程具有更干净且便于测试的特点。 反例
function calculateTotalScore(courses) {
    let totalScore = 0
    for(let i = 0; i < courses.length; i++) {
        const { score } = courses[i]
        totalScore += score
    }
    console.log(totalScore)
}

正例

function calculateTotalScore(courses) {
    const totalScore = courses.reduce((total, next) =>{
        return total + next
    } , 0)
    console.log(totalScore)
}
8.1.Array.forEach()
  • 方法为每个数组元素调用一次函数(回调函数),返回值是undefined
const courses = [{score:90, name:'语文'}, {score:100, name:'数学'}]
courses.forEach(course => {
    const {score, name} = course
    console.log(`${name}:${score}`)
})
// 输出
// 语文:90
// 数学:100

8.2.Array.map()

  • 通过对每个数组元素执行函数来创建新数组,返回值是新数组
  • 该方法不会对没有值的数组元素执行函数
  • 该方法不会更改原始数组
const newCourses = courses.map(course => {
    const {score, name} = course
    return {
        score: score + 10,
        name
    }
})
// courses = [{score:90, name:'语文'}, {score:100, name:'数学'}]
// newCourses = [{score:100, name:'语文'}, {score:110, name:'数学'}]

8.3.Array.filter()

  • 方法创建一个包含通过测试的数组元素的新数组,返回值是新数组
const score95 = 95
const newCourses = courses.filter(course => {
    return course.score > score95
})
// courses = [{score:90, name:'语文'}, {score:100, name:'数学'}]
// newCourses = [{score:100, name:'数学'}]

8.4.Array.reduce()

  • 方法在每个数组元素上运行函数,以生成(减少它)单个值,返回值是最后计算结果
  • 方法在数组中从左到右工作
  • 方法不会改变原始数组
const totalScore = courses.reduce((total, nextCourse) =>{
    return total + nextCourse.score
} , 0)
console.log(totalScore) // 190

8.5.Array.every()

  • 方法检查所有数组值是否通过测试,全部测试为true,返回值为true,否则返回false
  • 任何一个元素执行测试返回false时,整个方法会中断并返回false,此特性可以解决forEach方法无法break的劣势
const score80 = 80
const isAllCourseScoreMoreThan80 = courses.every(course => course.score > score80)
console.log(isAllCourseScoreMoreThan80) // true

8.6.Array.some()

  • 方法检查某些数组值是否通过了测试,有一个测试返回为true,返回值为true,否则返回false
  • 任何一个元素执行测试返回true时,整个方法会中断并返回true,此特性可以解决forEach方法无法break的劣势
const score100 = 100
const hasCourseScoreEqual100 = courses.some(course => course.score === score100)
console.log(hasCourseScoreEqual100) // true

9.尽量不要嵌套if和for,因为代码可读性会变得很差

  • ifwhile等控制语句其中代码块应该只有一行,也就是一个函数调用语句
  • 函数的缩进层次不应该多于两层 反例
// 当看到项目里有类似代码逻辑时,我内心是崩溃的
function evaluateScoreLevel(score) {
    const { math, english, chinese} = score
    const standardScore = 90
    const evaluateResult = {
        math:'',
        english: '',
        chinese: ''
    }
    if(math >= standardScore) {
        evaluateResult.math = 'A'
        
        if(english >= standardScore){
            evaluateResult.english = 'A'
            
            if(chinese >= standardScore){
                evaluateResult.chinese = 'A'
            } else {
                evaluateResult.chinese = 'B'
            } 
        } else {
            evaluateResult.english = 'B'
            
            if(chinese >= standardScore){
                evaluateResult.chinese = 'A'
            } else {
                evaluateResult.chinese = 'B'
            }
        } 
    } else {
        evaluateResult.math = 'B'
        
        if(english >= standardScore){
            evaluateResult.english = 'A'
            
            if(chinese >= standardScore){
                evaluateResult.chinese = 'A'
            } else {
                evaluateResult.chinese = 'B'
            }
        } else {
            evaluateResult.english = 'B'
            
            if(chinese >= standardScore){
                evaluateResult.chinese = 'A'
            } else {
                evaluateResult.chinese = 'B'
            }
        }
    }
    return evaluateResult
}

const yourScore = { math:100, english:95, chinese:98 }
evaluateScoreLevel(yourScore) // {math: 'A', english: 'A', chinese: 'A'}

正例

function evaluateScoreLevel(score) {
    const { math, english, chinese} = score
    const standardScore = 90
    const mathLevel = isGreaterThanScore(math, standardScore)? 'A':'B'
    const englishLevel = isGreaterThanScore(english, standardScore)? 'A':'B'
    const chineseLevel = isGreaterThanScore(chinese, standardScore)? 'A':'B'
    return {
        math: mathLevel,
        english: englishLevel,
        chinese: chineseLevel
    }
}

function isGreaterThanScore(sourceScore, targetScore) {
    return sourceScore >= targetScore
}

const yourScore = { math:100, english:95, chinese:98 }
evaluateScoreLevel(yourScore) // {math: 'A', english: 'A', chinese: 'A'}

10.提前退出原则

  • 把放在同一个判断里的多个判断条件拆成多个单个的判断条件,可以增加可读性
  • 别用else,可以简化判断逻辑 反例
// 评三好学生
function evaluateMeritStudent(studentInfo) {
    const { math, english, chinese, hasDonation } = studentInfo
    const standardScore = 90
    if(math >= standardScore && english >= standardScore && chinese >= standardScore && hasDonation){
        // do something
        return true
    }
    return false
}

正例

function evaluateMeritStudent(studentInfo) {
    const { math, english, chinese, hasDonation } = studentInfo
    const standardScore = 90
    if(!hasDonation) {
        return false
    }
    if(math < standardScore) {
        return false
    }
    if(english < standardScore) {
        return false
    }
    if(chinese < standardScore) {
        return false
    }
    // do something 
    return true
}

11.避免副作用

  • 当函数产生了除了“接受一个值并返回一个结果”之外的行为时,称该函数产生了副作用,比如写文件、修改全局变量
  • 可以简单理解为在执行逻辑时,应该返回新值,而不是在原变量上直接修改 反例
const userInfo = {
    firstName: 'Sky',
    familyName: 'Lin'
}
function copyUser(user, firstName) {
    user.firstName = firstName
    return user
}
const newUser = copyUser(userInfo, 'Panda')
console.log(userInfo.firstName) // Panda
console.log(newUser.firstName) // Panda

正例

const userInfo = {
    firstName: 'Sky',
    familyName: 'Lin'
}
function copyUser(user, firstName) {
    return Object.assign({}, user, { firstName })
}
const newUser = copyUser(userInfo, 'Panda')
console.log(userInfo.firstName) // Sky
console.log(newUser.firstName) // Panda

12.删除无效代码

  • 不再被调用的代码应及时删除
  • 启动你的eslint检查,它会告诉你那些变量和函数是unused的

13.避免过度优化

  • 现代的浏览器在运行时会对代码自动进行优化。有时人为对代码进行优化可能是在浪费时间 反例
// 这里使用变量len是因为在老式浏览器中,直接使用正例中的方式会导致每次循环均重复计算list.length的值,
// 而在现代浏览器中会自动完成优化,这一行为是没有必要的
for (let i = 0, length = list.length; i < length; i++) {
  // ...
}

正例

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

14.避免类型判断

反例

function move(vehicle) {
    if(vehicle instanceof 'Horse') {
        vehicle.run()
    }
    if(vehicle instanceof 'Car') {
        vehicle.drive()
    }
    if(vehicle instanceof 'Plane') {
        vehicle.fly()
    }
}

正例

class Vehicle{
    move(){}
}
class Horse extends Vehicle{}
class Car extends Vehicle{}
class Plane extends Vehicle{}
const horse = new Horse()
const car = new Car()
const plane = new Plane()
horse.move()
car.move()
plane.move()

四、并发

1.用 Promise 替代回调

  • 回调不够整洁并会造成大量的嵌套 反例
const request = require('request')
const fs = require('fs')

const url = 'https://en.wikipedia.org/wiki/Robert_Cecil_Martin'
request.get(url, (err, response) => {
  if (err) {
    console.error(err)
  } else {
    fs.writeFile('article.html', response.bodyfunction(err) {
      if (err) {
        console.error(err)
      } else {
        console.log('File written')
      }
    })
  }
})

正例

const requset = require('request-promise')
const fs = require('fs-promise')

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

2.Async/Await 是较 Promise 更好的选择

  • Promise 是较回调而言更好的一种选择,但 ES7 中的 async/await 更胜过 Promise
  • 在能使用 ES7 特性的情况下可以尽量使用他们替代 Promise 反例
const requset = require('request-promise')
const fs = require('fs-promise')

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

正例

const requset = require('request-promise')
const fs = require('fs-promise')

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

五、错误处理

1.别忘了捕获错误

  • 对捕获的错误不做任何处理是没有意义的。
  • 代码中 try/catch 的意味着你认为这里可能出现一些错误,你应该对这些可能的错误存在相应的处理方案。 反例
try {
  functionThatMightThrow()
} catch (error) {
  console.log(error)
}

正例

try {
  functionThatMightThrow()
} catch (error) {
  console.log(error)
  notifyUserOfError(error)
  reportErrorToService(error)
}

六、格式化

  • 格式化是一件主观的事。如同这里的许多规则一样,这里并没有一定/立刻需要遵守的规则。可以在这里完成格式的自动化。

1.大小写一致

反例

var DAYS_IN_WEEK = 7
var daysInMonth = 30

var songs = ['Back In Black''Stairway to Heaven''Hey Jude']
var Artists = ['ACDC''Led Zeppelin''The Beatles']

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

class animal {}
class Alpaca {}

正例

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

2.调用函数的函数和被调函数应放在较近的位置

  • 当函数间存在相互调用的情况时,应将两者置于较近的位置
  • 理想情况下,应将调用其他函数的函数写在被调用函数的上方

七、注释

1.只对存在一定业务逻辑复杂性的代码进行注释

  • 注释要说明为什么这么设计,而不是说这里做了什么
  • 注释并不是必须的,好的代码是能够让人一目了然,不用过多无谓的注释
  • 好代码 > 烂代码 + 好注释 > 烂代码 + 烂注释 > 烂代码 + 无注释

2.不要在代码库中遗留被注释掉的代码

  • 版本控制的存在是有原因的。让旧代码存在于你的 history 里 反例
doStuff()
// doOtherStuff()
// doSomeMoreStuff()
// doSoMuchStuff()

正例

doStuff()

八、参考文献

1.《JavaScript 代码整洁之道》

2.《代码整洁之道(一)最佳实践小结》