JavaScript代码整洁( Clean Coding )最佳实践

JavaScript代码整洁( Clean Coding )最佳实践

代码整洁( Clean Coding )是什么意思?

所谓的clean code 字面上就是整洁代码的含义,落实到我们工程师日常coding中就是如何写出看上去干净、逻辑清晰、有一定抽象能力扩展性好的代码。写的代码是方便和同事协作开发,不单单是为机器编写,所有,代码尽可能让人容易理解。

这些文字的定义显得不那么生动形象,看看下图

CleanCode.jpg

左侧的就是clean code,右侧的就是WTF(让人看见想骂xx的意思) code。那么有的人会问clean code也有WTF,是不是还不是真正的clean code,如果你是一个追求极致的人那么这个问题没毛病。但是我想说的是没有什么代码没有0 WTF的,及时所谓非常整洁规范、干净的代码也或多或少是些小毛病。所以我们工程师能够向着0 WTF的方向去努力去优化自己的代码就是成功的。现在我们知道了开发者的目标是什么了,下面来一起看看最佳实践吧!

变量应该如何命名?

使用有意义且准确的名称,不要因为有长的名称而节省几个键盘敲击。如果您遵循此实践,变量会变得方便搜索,当你需要重构或者查找某些关键字的时候会有很大的帮助。

// 错误命名
let d
let elapsed
const ages = arr.map((i) => i.age)

// 正确打开方式
let daysSinceModification
const agesOfUsers = users.map((user) => user.age)

另外,要进行有意义的区分,不要在变量名中添加额外的、不必要的名词,比如它的类型(匈牙利表示法)。

在匈牙利表示法中,变量名以一个或多个小写字母开始,代表变量的类型。后面附以变量的名字,变量名以意义明确的大小写混合字母序列所构成。
// 不建议写法
let nameString
let theUsers

// 建议写法
let name
let users

变量名称应该准确而简练,琅琅上口,让人更容易理解。

进行代码评审时更方便查阅参考。

简而言之,我们需要让命名含义清楚,知名见意。

// 不建议写法
let fName, lName
let cntr

let full = false
if (cart.size > 100) {
  full = true
}

// 建议写法
let firstName, lastName
let counter

const MAX_CART_SIZE = 100
// ...
const isFull = cart.size > MAX_CART_SIZE

函数应该怎么写?

复杂的方法应该抽离出独立的函数

函数应该做一件事。做好这件事。只做这一件事。— Robert C. Martin (Uncle Bob)
// 不建议写法
function getUserRouteHandler (req, res) {
    const { userId } = req.params
    // inline SQL query
    knex('user')
      .where({ id: userId })
      .first()
      .then((user) => res.json(user))
  }
  
  // 建议写法
  // User model (eg. models/user.js)
  const tableName = 'user'
  const User = {
    getOne (userId) {
      return knex(tableName)
        .where({ id: userId })
        .first()
    }
  }
  
  // route handler (eg. server/routes/user/get.js)
  function getUserRouteHandler (req, res) {
    const { userId } = req.params
    User.getOne(userId)
      .then((user) => res.json(user))
  }

正确的函数写法方便在遇到问题时调试。

使用较长有描述意义的命名

函数名应该是动词或动词短语,它需要传达其意图,以及参数的顺序和意图。

一个长的有描述性的的名字比一个短的,不知所谓的名字或一堆描述性注释要好得多。

// DON'T
/**
 * Invite a new user with its email address
 * @param {String} user email address
 */
function inv (user) { /* implementation */ }

// DO
function inviteUser (emailAddress) { /* implementation */ }

避免参数过长

尽量使用单个对象参数和解构赋值,这样更容易处理可选参数。

// DON'T
function getRegisteredUsers (fields, include, fromDate, toDate) { /* implementation */ }
getRegisteredUsers(['firstName', 'lastName', 'email'], ['invitedUsers'], '2016-09-26', '2016-12-13')

// DO
function getRegisteredUsers ({ fields, include, fromDate, toDate }) { /* implementation */ }
getRegisteredUsers({
  fields: ['firstName', 'lastName', 'email'],
  include: ['invitedUsers'],
  fromDate: '2016-09-26',
  toDate: '2016-12-13'
})

减少副作用

尽可能使用没有副作用的纯函数。这样方便使用和测试。

纯函数的概念:一个函数的返回结果只依赖其参数,并且执行过程中没有副作用。
// DON'T
function addItemToCart (cart, item, quantity = 1) {
  const alreadyInCart = cart.get(item.id) || 0
  cart.set(item.id, alreadyInCart + quantity)
  return cart
}

// DO
// not modifying the original cart
function addItemToCart (cart, item, quantity = 1) {
  const cartCopy = new Map(cart)
  const alreadyInCart = cartCopy.get(item.id) || 0
  cartCopy.set(item.id, alreadyInCart + quantity)
  return cartCopy
}

// or by invert the method location
// you can expect that the original object will be mutated
// addItemToCart(cart, item, quantity) -> cart.addItem(item, quantity)
const cart = new Map()
Object.assign(cart, {
  addItem (item, quantity = 1) {
    const alreadyInCart = this.get(item.id) || 0
    this.set(item.id, alreadyInCart + quantity)
    return this
  }
})

自顶向下读代码:向下规则

优先级高的函数应该位于顶层,优先级低的函数应该位于下层。使阅读源代码更自然。

我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。这就叫作向下规则。
// DON'T
// "I need the full name for something..."
function getFullName (user) {
  return `${user.firstName} ${user.lastName}`
}

function renderEmailTemplate (user) {
  // "oh, here"
  const fullName = getFullName(user)
  return `Dear ${fullName}, ...`
}

// DO
function renderEmailTemplate (user) {
  // "I need the full name of the user"
  const fullName = getFullName(user)
  return `Dear ${fullName}, ...`
}

// "I use this for the email template rendering"
function getFullName (user) {
  return `${user.firstName} ${user.lastName}`
}

查询或修改

函数要么做某事(修改),要么回答某事(查询),但不能同时做这两件事。

每个人都喜欢用不同的方式写JavaScript,该怎么办呢?

由于JavaScript是一门动态弱类型的编程语言,很容易在编程过程中出现错误。

在开发过程中使用统一的编码规则和格式插件。例如Eslint

规则越严格,在代码审查中指出错误格式的工作量就越少。应该包括一致的命名,缩进大小,空格的位置,甚至分号。

或者可以使用更严格的TypeScript,TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。

如何写漂亮的异步代码?

尽可能使用 Promises

ES6新增了Promises,从Node.js v4开始,Promises就在Node.js中原生可用。您可以使用Promise链式调用,而不是编写嵌套的回调。

// 避免使用
asyncFunc1((err, result1) => {
  asyncFunc2(result1, (err, result2) => {
    asyncFunc3(result2, (err, result3) => {
      console.lor(result3)
    })
  })
})

// 推荐方式
asyncFuncPromise1()
  .then(asyncFuncPromise2)
  .then(asyncFuncPromise3)
  .then((result) => console.log(result))
  .catch((err) => console.error(err))

大多数软件库都提供传统的回调接口和promise 接口,更推荐使用后者,您甚至可以通过使用es6-promisify.这样的包来包装回调API ,从而将其转换为基于Promise的回调API 。

// 避免使用
const fs = require('fs')

function readJSON (filePath, callback) {
  fs.readFile(filePath, (err, data) => {
    if (err) {
      return callback(err)
    }

    try {
      callback(null, JSON.parse(data))
    } catch (ex) {
      callback(ex)
    }
  })
}

readJSON('./package.json', (err, pkg) => { console.log(err, pkg) })

// 推荐方式
const fs = require('fs')
const promisify = require('es6-promisify')

const readFile = promisify(fs.readFile)
function readJSON (filePath) {
  return readFile(filePath)
    .then((data) => JSON.parse(data))
}

readJSON('./package.json')
  .then((pkg) => console.log(pkg))
  .catch((err) => console.error(err))

使用async/await 来简化调用,这里可以查看 JS基础 - 异步进阶 - async / await

如何编写性能代码?

先编写整洁的代码,然后使用性能分析来寻找性能瓶颈。

译者:Gonwe

来源:blog.risingstack.com

Nodejs ECMAScript Javascript笔记 Promise ES6 Clean Coding
Theme Jasmine by Kent Liao