一个称职的开发人员不仅为了实现功能,更是在为将来的自己和下一个接手的 “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'));
相应的方法还有 some
、 find
使用Object(Map)枚举
开发者在修改老代码的时候应该经常遇到下面这种匹配的语句,如果不写注释,根本不知道 P02
、 replaceWith
代表的意思
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)优化对应关系
Object
和 Map
有天然的对应关系 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:
进行描述
总结
感谢阅读! 如果本文有不正确或遗漏的地方欢迎提出