编写更加健壮的JS

80 阅读10分钟

一个称职的开发人员不仅为了实现功能,更是在为将来的自己和下一个接手的 “Bro”编写代码。基于此,编写易于理解,易于更改,易于拓展的代码是每一个明智开发者的追求

在本文中,重点将放在JavaScript上,但是这些原理同样适用于其他编程语言

每个写法都有各自适应的场景,因此,请根据实际场景去选择合适的做法

变量命名

命名变量应该使其能够揭示其意图

  • Bad:
functin Example() {
  if (!this.haveOffer) { // 是否有offer
    scrollTo(0, 1150)
    return
  }
  if (!this.earlyPractice) { // 是否提前实习
    scrollTo(0, 1150)
    return
  }
  // doSomething...
}

虽然这个if语句添加了注释,但是这个命名没有那么直观,对于这种条件语句是可以固定一种命名格式: isXX ,例如 isOfferValid

  • Good:
functin Example() {
    // 是否有offer
  if (!this.isHasOffer) {
    scrollTo(0, 1150);
    return;
  }
  // 是否提前实习
  if (!this.isEarlyPractice) {
    scrollTo(0, 1150);
    return;
  }
  // doSomething...
}

不应添加多余的单词

  • Bad
let nameValue;
let theProduct;
  • Good
let name;
let product;

不应添加不必要的上下文

  • Bad
const user = {
  userName: "John",
  userSurname: "Doe",
  userAge: "28"
};
  • Good
const user = {
  name: "John",
  surname: "Doe",
  age: "28"
};

默认参数

默认参数仅针对undefined

一般我们会使用默认参数,但是经常有人会忽略函数什么时候才会使用默认参数,先来看下面两个函数的输出结果

const Example = (val, amount = 1) => {
  if(!val){
    return
  }
  console.log(amount)
}

const Example2 = (val, amount) => {
  const temp = amount || 1
  if(!val){
    return
  }
  console.log(temp)
}

Example('val',null); // null
Example2('val',null); // 1 

如果存在疑惑的话,可以看看Example函数经过babel转义后的内容

"use strict"
function Example(val) {
  var amount = arguments.lenght > 1 && arguments[1] !== undefined ? arguments[1] : 1;
  
  if(!val){
    return
  }
  console.log(amount)
}

**函数只有参数为 undefined 时才会使用默认参数,**所以,在使用默认参数并不等于 amount || 1

使用解构与默认参数

当函数的参数是对象,可以使用解构简化逻辑(适用于比较简单的对象)

  • Bad
const Example = (user, amount) => {
 const userInstance = user || {};
 if(!userInstance.name || !userInstance.age){
   return;
 }
 // doSomething...
}
  • Good
const Example = ({ name, age } = {}, amount) => {
  if(!name || !age){
    return;
  }
  // doSomething...
}

拓展一下,如果函数参数超过两个,也建议使用 ES6 的解构语法,不用考虑参数的顺序

// Bad:
function Example( name, age, amount ) {
   // doSomething...
}


// Good:
function Example( { name, age, amount } ) {
   // doSomething...
}
createMenu({
    name: 'nacy',
    age: 14',
    amount: 1',
});

条件式语句的优化

条件式语句的逻辑在平常代码中很常见,但是很多人因为省事,会编写不太简介的条件语句,导致代码可读性很差,且不易拓展,下面看一下如何更好的编写条件式语句

避免使用“非”条件句

在下面例子的情况下,使用“非”条件语句会导致可读性变差

  • Bad
function isNotHasPath(path) {
  // doSomething...
}

if (!isNotHasPath(path)) {
  // doSomething...
}
  • Good
function isHasPath(path) {
  // doSomething...
}

if (isHasPath(path)) {
  // doSomething...
}

提前退出嵌套

if...else 是我们用到最多的一种语法,当只存在一个 if...else 的时候是很好理解的,如果存在多个if...else (也可以使用switch),并且失去控制的话,多个分支和嵌套可能会很痛苦

另一方面,如果嵌套层级较多,理解深处的 return 语句的逻辑将变得困难

  • Bad1
const printAnimalDetails = animal => {
  let result;
  if (animal.type) {
    if (animal.name) {
      if (animal.gender) {
        result = `${name} - ${gender} - ${type}`;
      } else {
        result = "No animal gender";
      }
    } else {
      result = "No animal name";
    }
  } else {
    result = "No animal type";
  }
  return result;
};

当然了,平常应该不会遇到这种代码吧(太极端了),即使对于上述这个简单的逻辑,代码也不好理解,想象一下,如果我们拥有更复杂的逻辑,将会发生什么?很多 if...else 陈述,那修改这段代码就等同于重写

如果将 else 部分进行前置,就可以减少嵌套层级,并且会让代码可读性变好

  • Good1
const printAnimalDetails = ({type, name, gender } = {}) => {
  if(!type) {
    return 'No animal type';
  }
  if(!name) {
    return 'No animal name';
  }
  if(!gender) {
    return 'No animal gender';
  }
  return `${name} - ${gender} - ${type}`;
}

下面这个例子是遇到的比较常见的一种结构...

  • Bad2
function Example(animal, quantity) {
  const animals = ['dog', 'cat', 'hamster', 'turtle'];
  
  if (animal) {
    if (animals.includes(animal)) {
      console.log(`${animal}`);
      if (quantity) {
        console.log('quantity is valid');
      }
    }
  } else {
    console.log('animal is invalid');
  }
}
  • Good2
function Example(animal, quantity) {
  const animals = ['dog', 'cat', 'hamster', 'turtle'];
  
  if (!animal) {
    console.log('animal is invalid');
  }
  if (animals.includes(animal)) {
    console.log(`${animal}`);
    if (quantity) {
      console.log('xxxxx');
    }
    }
}

当然,许多人认为 if...else 语句更容易理解,这有助于他们轻松地遵循程序流程,只不过,在多层嵌套下,提前退出嵌套会更好的提高可读性(如果函数过于复杂,也需要考虑是否可以进行拆分)

Array.includes替代多个条件

  • Bad
function Example(animal) {
  if (animal === 'dog' || animal === 'cat') {
    console.log(`this is a ${animal}`);
    }
    // doSomething...
}

考虑到只匹配两个类型,这样做看起来是可以接受的,但是如果我们要增加另一个匹配条件呢?如果添加更多的 or 语句,代码会变得更难维护。对于上面这种多条件判断的情况,可以使用 Array.includes

  • Good
function Example(animal) {
   const animals = [
     'dog', // 这里可以添加注释
         'cat' // 这里可以添加注释
   ];

   if (animals.includes(animal)) {
     console.log(`this is a ${animal}`);
   }
   // doSomething...
}

此刻,如果要增加更多的类型,只需要添加一个新的数组项而已。同样的,如果这个类型数组被多个地方引用,那就将该数组放在公共的位置供其他代码块使用

使用数组匹配所有条件

  • Bad

下面这段代码是检查数组中是否所有动物都是cat

const animals = [
  { name: 'dog', age: 1 },
  { name: 'cat', age: 2 },
  { name: 'cat', age: 3 }
];

const isCheckAllCat = (animalName) => {
  let isValid = true;
  
  for (let animal of animals) {
    if (!isValid) {
      break;
    }
    isValid = animal.name === animalName;
  }
  
  return isValid;
}

console.log(isCheckAllCat('cat')); // false

我们可以使用 every 方法重写

  • Good
const isCheckAllCat = (animalName) => {
  return animals.every(animal => animal.name === animalName);
}

console.log(isCheckAllCat('renault'));

相应的方法还有 somefind

使用Object(Map)枚举

开发者在修改老代码的时候应该经常遇到下面这种匹配的语句,如果不写注释,根本不知道 P02replaceWith 代表的意思

if (info.haveOffer === 'Y') {...}
if (info.interviewPhase === 'P02'){...}
if (info.feedbackId !== 'socialRecruitmentNotDevelop'){...}
if (attr === 'replaceWith'){...}
if (file.status === 'fail'){...}
if (tab === 'titleCertificate'){...}
  • Bad
getTechInterviewFeedBack (info) {
  if (!info) {
    console.log('info invalid');
    return ''
  }
  if (info.interviewPhase === 'P01') {
    if (info.feedbackId === 'socialRecruitment') {
      return 'first stag interview on social'
    } else if (info.feedbackId === 'schoolRecruiment') {
      return 'first stag interview on school'
    }
  }
  if (info.interviewPhase === 'P02') {
    if (info.feedbackId === 'socialRecruitment') {
      return 'second stag interview on social'
    } else if (info.feedbackId === 'schoolRecruiment') {
      return 'second stag interview on school'
    }
  }
  // 更多的if...else语句...
}

该函数包括很多匹配的语句,首先,我们不知道这些匹配的字符串代表的意思,其次,假如有其他代码块或其他文件使用了相同的字符串进行匹配,又该如何避免改漏?

  • Good
/** 面试阶段 */
const INTERVIEW_PHASE_TYPE_MAP = {
  firstTech: 'P01', // 技术一面
  secondTech: 'P02' // 技术二面
}

/** 反馈类型 */
const FEEDBACK_TYPE_MAP = {
  social: 'socialRecruitment', // 社招
  school: 'schoolRecruiment' // 校招
}

/** 获取技术面的反馈信息 */
getTechInterviewFeedBack (info) {
  const { interviewPhase, feedbackId } = info
  const isInterviewPhaseOnFirstTech = interviewPhase === INTERVIEW_PHASE_TYPE_MAP.firstTech
  const isInterviewPhaseOnSecondTech = interviewPhase === INTERVIEW_PHASE_TYPE_MAP.secondTech
  const isFeedbackFromSocial = feedbackId === FEEDBACK_TYPE_MAP.social
  const isFeedbackFromSchool = feedbackId === FEEDBACK_TYPE_MAP.school

  if (!info) {
    console.log('info invalid');
    return ''
  }
  // 第一次技术面
  if (isInterviewPhaseOnFirstTech) {
    if (isFeedbackFromSocial) {
      return 'first stag interview on social'
    }
    if (isFeedbackFromSchool) {
      return 'first stag interview on school'
    }
  }
  // 第二次技术面
  if (isInterviewPhaseOnSecondTech) {
    if (isFeedbackFromSocial) {
      return 'second stag interview on social'
    }
    if (isFeedbackFromSchool) {
      return 'second stag interview on school'
    }
  }
  // 更多的if...else语句...
}

:上述代码还会继续优化,详情请移步到【合理封装】章节

使用 Object 给这些匹配值进行常量的声明,能带来两个好处:

(1)可以方便多个代码块、多个代码文件使用相同匹配值,方便统一管理,同步修改,也易于拓展

(2)降低开发者因为匹配值拼写错误而带来的调试成本

(3)可以在 Object 上对 key:value 进行注释,提高代码可读性

使用Object(Map)优化对应关系

ObjectMap 有天然的对应关系 key <=> value ,在一些场景下能发挥巨大作用

  • Bad
function Example(val) {
  switch (val) {
    case 'BEFORE_CREATE':
      return 'QuickGuide';
    case 'CREATED':
      return 'CompleteToDoGuide';
    case 'FINISHED':
      return 'Finished';
    default:
      return '';
  }
   // doSomething...
}
  • Good
/** 这里写注释 */
const SOURCE_MAP {
  BEFORE_CREATE: 'QuickGuide', // 注释
  CREATED: 'CompleteToDoGuide', // 注释
  FINISHED: 'Finished', // 注释
};
  
// 代替简单的条件判断
function resourceInfo (val) {
  return SOURCE_MAP[val] || '';
}

使用可选链和无效合并

可选链使我们能够处理树状结构,而无需显式检查中间节点是否存在,并且,空值合并与可选链结合使用非常有效,它可用于确保不存在的默认值

  • Bad
const street = user.address && user.address.street;
const street = user && user.address || 'default address';

// 常见于一些接口获取函数
$page.search.getJobs(this.params).then(res => {
  if (res && res.datas && res.datas.length) {
    // doSomething...
  } else {
    // doSomething...
  }
}).catch(err => {
  // doSomething...
})
  • Good
const street = user.address?street;
const street = user?.address ?? 'default address';

这个 ?? 的意思是,如果 ?? 左边的值是 null 或者 undefined,那么就返回右边的值。可选链让代码看起来更简洁。同时可选链还支持 DOM API,这意味着可以执行以下操作:

const value = document.querySelector('div#animal-dom')?.value;

合理封装

提炼函数

在开发过程中,我们大部分时间都在思考如何封装组件,如何封装函数,函数的功能是独立且纯粹的,如果一个函数过于复杂,不得不加上若干注释才能让这个函数显得易读一些,那这个函数就很有必要进行重构,一种常见的优化操作则是将可以独立出来的内容,放在另一个独立的函数中,然后将其引入

这样做的好处主要有以下几点。

  • 避免出现超大函数
  • 独立出来的函数有助于代码复用
  • 独立出来的函数更容易被覆写
  • 独立出来的函数如果拥有一个良好的命名,它本身就起到了注释的作用

很多人都知道函数封装的优点,但是每个人对“独立性”和“纯粹”的理解有所不同,例如下面这段代码,功能是通过接口去获取相应的数据,并将数据进行分发

// Vue
async fetchPhaseList () {
  try {
    this.loading = true
    const params = this.paramsFormatter()
    const { success, data, msg } = await $demandManage.queryDemandHcInfo(
      params
    )
    if (!success || !data) {
      throw new Error(msg)
    }
    const resData = Array.from(data || [])
    this.tableData = this.dataFormatter(resData)
  } catch (err) {
    this.$message.error(`列表数据获取失败(${err.toString()})`)
  } finally {
    this.loading = false
  }
}

如果说获取 PhaseList 并进行处理是一个整体,似乎也说得通,只是在我看来,这个函数不够“纯粹”,函数内部有几个环节导致代码看起来很缠绕

  • try..catch..finally 导致函数被拆分为3块,有了三个 {}
  • 接口返回数据后,还会对数据的有效性进行判断,导致又多了一个 {}

可见代码本身其实不复杂,做的功能也很简单,只是获取数据,判断有效性,然后赋值给 tableData ,最后在此基础上主动捕获异常。尽管如此,代码给人的第一感觉就是“乱”。如果让我去修改数据赋值的逻辑,我也会因为担心影响到其他内容,而将整个函数都浏览一边

“纯数据的处理”应该和“业务逻辑”进行区分,结合上面的代码来理解,“纯数据的处理”就是通过接口获取数据,并进行数据的有效性判断,且这个有效性判断是标准化的(也就是说大家都是判断 success 是否为 true ,以及 data 是否有值),“业务逻辑”就是拿到有效的数据后,根据功能来进行对应的数据处理。一般情况下,“纯数据的处理”被二次修改的概率偏低,“业务逻辑”则相反,而这个函数的“纯数据的处理”部分已经明显多余“业务逻辑”,显然这会对后面的业务修改带来阻力

因此,需要对函数进行“纯数据的处理”和“业务逻辑”的拆分,在拆分之前先创建一个公共的异常捕获函数(放在utils目录下)

注:这里更推荐在Ajax层面就进行异常的捕获

/**
     * 捕获代码块错误信息
     * @param {function} cb - 主要逻辑(ajax为主)
     * @param {string} fileName - 调用该捕获错误函数的代码文件名
     * @param {string} functionName - 调用该捕获错误函数的函数名
     */
async catchErrorMessage (cb, fileName = 'null', functionName = 'null') {
  if (cb) {
    try {
      await cb()
    } catch (err) {
      this.$message.error(err.toString())
      const path = location && location.pathname || 'null'
      // 主动上报sentry
      this.$Sentry({
        extra: {
          error_From_path: path, // 发生错误的页面path
          error_From_fileName: fileName || 'null',
          error_From_functionName: functionName || 'null'
        },
        level: 'error'
      })
    }
  }
}
// “纯数据的处理”
async queryPhaseList (params) {
  try {
    const { success, data, msg } = await $demandManage.queryDemandHcInfo(
      params
    )
    if (!success || !data) {
      throw new Error(msg)
    }
    const resData = Array.from(data || [])
    return this.dataFormatter(resData)
  } catch (err) {
    this.$message.error(`列表数据获取失败(${err.toString()})`)
  }
}

// “业务逻辑”
async handlePhaseList () {
  this.loading = true
  const params = this.paramsFormatter()
  await utils.catchErrorMessage(async () => {
    this.tableData = await queryPhaseList(params)
  })
  this.loading = false
}

一般页面会有多个接口,也会有多个类似的代码段,因此,“纯数据的处理”作为不牵扯业务逻辑的函数,可以将其单独放在某个文件中进行管理,如此一来,组件内部仅剩下“业务逻辑”函数,即我们需要真正关注的内容

合并重复的代码块

如果一个函数体内有一些条件式语句,而这些条件式语句内部包含一些重复的代码,那么就有必要进行优化,将重复的内容独立出来

  • Bad
// Vue
async fetchPhaseList () {
  try {
    this.loading = true
    // ...
    const { success, data, msg } = await $demandManage.queryDemandHcInfo(
      params
    )
    this.loading = false
    if (!success || !data) {
      throw new Error(msg)
    }
    // ...
  } catch (err) {
    this.$message.error(`请求失败(${err.toString()})`)
    this.loading = false
  }
}

上面代码是想在接口返回后、代码块报错后,将 loading 置为 false

  • Good
// Vue
async fetchPhaseList () {
  try {
    this.loading = true
    // ...
    const { success, data, msg } = await $demandManage.queryDemandHcInfo(
      params
    )
    // ...
  } catch (err) {
    this.$message.error(`请求失败(${err.toString()})`)
  } finally {
    this.loading = false // 统一置为 false
  }
}

把条件语句提炼为函数

这个原则不是说遇到条件语句就提炼为函数,是需要根据实际场景去判断的,如果函数内部存在几个条件的判断,且每个判断内容都比较复杂,这几个条件也各自独自,那就可以将其提炼为函数,直接看例子

  • Bad
/** 获取技术面的反馈信息 */
getTechInterviewFeedBack (info) {
  const { interviewPhase, feedbackId } = info
  const isInterviewPhaseOnFirstTech = interviewPhase === INTERVIEW_PHASE_TYPE_MAP.firstTech
  const isInterviewPhaseOnSecondTech = interviewPhase === INTERVIEW_PHASE_TYPE_MAP.secondTech
  const isFeedbackFromSocial = feedbackId === FEEDBACK_TYPE_MAP.social
  const isFeedbackFromSchool = feedbackId === FEEDBACK_TYPE_MAP.school

  if (!info) {
    console.log('info invalid');
    return ''
  }
  // 第一次技术面试
  if (isInterviewPhaseOnFirstTech) {
    if (isFeedbackFromSocial) {
      return 'first stag interview on social'
    }
    if (isFeedbackFromSchool) {
      return 'first stag interview on school'
    }
  }
  // 第二次技术面试
  if (isInterviewPhaseOnSecondTech) {
    if (isFeedbackFromSocial) {
      return 'second stag interview on social'
    }
    if (isFeedbackFromSchool) {
      return 'second stag interview on school'
    }
  }
  // 更多的if...else语句...
}

这个例子是前面【条件式语句的优化】章节的例子

可以看到函数已经比较清晰的表达了功能,但是这个函数的功能如果情况再多一些呢?或者后续还会增加其他情况的内容呢?那这个代码将会越来越复杂,成为一个巨型函数

以上面代码为例,函数包含两种情况(第一次技术面试、第二次技术面试),这两种情况是没有相互联系的,因此,可以将其独立出来

  • Good
/** 获取技术一面的反馈信息 */
function getFirstTechInterviewFeedBack(type) {
  const isFeedbackFromSocial = type === FEEDBACK_TYPE_MAP.social
  const isFeedbackFromSchool = type === FEEDBACK_TYPE_MAP.school
  if (isFeedbackFromSocial) {
    return 'first stag interview on social'
  }
  if (isFeedbackFromSchool) {
    return 'first stag interview on school'
  }
  return ''
}

/** 获取技术二面的反馈信息 */
function getSecondTechInterviewFeedBack(type) {
  const isFeedbackFromSocial = type === FEEDBACK_TYPE_MAP.social
  const isFeedbackFromSchool = type === FEEDBACK_TYPE_MAP.school
  if (isFeedbackFromSocial) {
    return 'second stag interview on social'
  }
  if (isFeedbackFromSchool) {
    return 'second stag interview on school'
  }
  return ''
}

/** 获取技术面的反馈信息 */
function getTechInterviewFeedBack (info) {
  if (!info) {
    console.log('info invalid');
    return ''
  }

  const { interviewPhase, feedbackId } = info
  const isInterviewPhaseOnFirstTech = interviewPhase === INTERVIEW_PHASE_TYPE_MAP.firstTech
  const isInterviewPhaseOnSecondTech = interviewPhase === INTERVIEW_PHASE_TYPE_MAP.secondTech

  if (isInterviewPhaseOnFirstTech) {
    return getFirstTechInterviewFeedBack(feedbackId)
  }
  if (isInterviewPhaseOnSecondTech) {
    return getSecondTechInterviewFeedBack(feedbackId)
  }
  // 更多的if...else语句...
}

尽可能避免全局方法

如果你要修改全局/公共的方法,请谨慎!举个例子,比如你想在Array的原型上新增一个 print 方法来顺序打印数组的所有元素,但是后面接手的人也想新增一个 print 方法,他想倒序打印所有元素,但是他在新增的时候不知道你有定义,那你们就会发生冲突。并且, Array 是一个 JS 定义的对象,开发者很常使用该方法,如果想要拓展,那也仅仅是为了你的业务所考虑,那就不该去影响到 Array 对象的定义,而应改用多态和继承实现拓展

  • Bad
Array.prototype.print = function print(ArrayList) {
  for(let i = 0; i < ArrayList.length; i++) {
    console.log(ArrayList[i])
  }
};
  • Good
class BetterArray extends Array {
  print(ArrayList) {
    for(let i = 0; i < ArrayList.length; i++) {
      console.log(ArrayList[i])
    }     
  }
};

如果后续还要继续拓展,那就直接拓展 BetterArray 的方法即可

删除弃用代码

简单一句话,删除!如果暂时不删除,可以添加详细 TODO: 进行描述

总结

感谢阅读! 如果本文有不正确或遗漏的地方欢迎提出