故事
领导💬:“原来项目有个需求变动,需要你去改一下,没有改很多,这个应该很快吧。”
二盛💬:“好👌,我先看一下。”
内心忐忑的二盛打开了那个古老的项目,不看不知道,一看吓一跳,项目的代码大概是这样的:
一文千行洋洋洒洒每行代码密密麻麻
一气呵成想哪写哪
条理不清一团乱麻
重复代码整齐好看
宁写多次绝不封装
全局变量满天飞翔
想用就用从天而降
变量命名全按顺序
a1 , a2 , a3 , a4
只用for
+if
语句
实现一切无所畏惧
这里写死那里写死
复用全是直接复制
惜墨如金不写注释
看不懂是别人的事
面对着这些杂乱无章随心所欲的代码,二盛哇的一声哭了出来😭。
成年人的世界没有容易二字,二盛擦干泪,开始小心翼翼地改代码。
最后90%
的时间花在了阅读代码,10%
的时间花在修改上,如同完成一个多米诺骨牌的项目,成就感油然而生。
于是二盛在工作汇报中自豪地写下:今天改一个XXX(小功能),备注:原来的代码不好读懂。
当领导看到工作报告时的内心活动:“一个小小的功能,改了那么久?原来的代码是一个新手写的,居然还说不好读懂,看来这个员工能力不行啊。”
前言
- 文章目的:帮助大家写出可读性和可维护性高的代码😁
- 适合人员:初级人员,以及想让队友好好写代码的朋友们
- 阅读时长:因人而异,总共
4000+
字,看不完点个收藏👍⭐
3 个方面
准备数据
// 鼠笼🐀🐀🐀
const mouseList = [
{ id: 'm01', name: '小白鼠', type: '0' },
{ id: 'm02', name: '小黑鼠', type: '4' },
{ id: 'm03', name: '小红鼠', type: '5' },
{ id: 'm04', name: '小橙鼠', type: '3' },
{ id: 'm05', name: '小黄鼠', type: '1' },
{ id: 'm06', name: '小青鼠', type: '1' },
{ id: 'm07', name: '小蓝鼠', type: '2' },
{ id: 'm08', name: '小绿鼠', type: '2', cap:{} },
{ id: 'm09', name: '小紫鼠', type: '5' },
]
// 类型对照表
const typeMap = {
'0': '家鼠',
'1': '田鼠',
'2': '竹鼠',
'3': '松鼠',
'4': '米老鼠',
'5': '快乐番薯'
}
第1方面:表明意图
明确告诉读代码的人你在干什么
下面是例子
📄需求
你去👉委婉的告诉小绿鼠
,他老婆出轨了。
✍实现
var flag = false
for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].name == '小绿鼠') {
mouseList[i].cap.color = 'green'
flag = true
break
}
}
if (flag) {
console.log('我已经告诉他了')
} else {
console.warn('没有找到小绿鼠')
}
🔍分析
现在我们来分析以上代码做的事情:
- 定义一个变量
flag
, 默认值为false
- 对
mouseList
进行遍历 - 如果数组元素的
name
是小绿鼠
- 给该数组元素的
cap
属性的color
属性赋值 - 把变量
flag
重新赋值true
- 终止循环
- 通过
flag
判断是否找到,给出提示
可以看到,我们需要看完一整段代码之后,才能知道代码是在做什么,因为第一眼看到的是for
+if
,我们只能由此得知要进行遍历
+判断
,而无法得知更明确的意图。
🛠优化
下面这3种方法可以让代码的意图更加明确:
- 直接写注释
在两小段代码开通分别添加注释:
- 在所有老鼠中找到一只名字叫
小绿鼠
的老鼠,帮他把帽子染成绿色 - 事情做完了给出提示
写注释简单粗暴,可是十分有效。然而无论是作者写注释,还是读者读注释,都需要耗费时间,因此如果是简单的功能,那么注释是没有必要的,好钢用在刀刃上,注释也应该写在关键之处。
- 命名里面给讯息
注释可以省略不写,但是命名一般跑不掉,正所谓命名不规范,队友两行泪,瞎起名伤害的不仅仅是队友,还有将来看代码的自己。
原代码的写法是立一个flag
var flag = false
现在我们把它改成这样
let isFound = false
这样写有3个好处:
- 用
ES6
的let
而非const
,说明我将来要对这个变量重新赋值,而var
只是单纯声明 is
开头说明变量是Boolean
类型,如果在下文中更一群杂七杂八的变量混在一起,也能一眼认出个大概isFound
的意思是是否找到
,这个found
一出来,读者马上就知道作者找东西的意图
found 是 find 的过去分词
所以,单看let isFound = false
,不看下面的代码,我们就可以推测出作者是在寻找目标,isFound
是作为是否找到目标的标识,如果找到目标以后,一定会有isFound = true
的代码出现
- 使用函数
这里使用函数的意思是,把非必要内容封装进函数中,只留下主要信息,通过主要信息来凸显意图。
封装之前我们先把代码逻辑再拆分细一些, 把寻找目标和对目标进行操作分成两步,下面把寻找目标封装成函数,首先提取要素:
- 范围 (在哪里找)
- 目标描述 (找啥样的)
- 数量 (找几个)
- 结果 (找到没)
以此为来封装函数
/**
* 数组里面找元素
* @param {array} array 范围
* @param {function} callback 目标描述
* @param {number} count 数量
* @return {array} 结果
*/
function arrayFindItem(array, callback, count) {
const result = []
let _count = 0
for (let i = 0; i < array.length; i++) {
if (callback(array[i])) {
_count++
result.push(array[i])
if (_count === count) {
return result
}
}
}
return result
}
接着使用它
const result = arrayFindItem(mouseList, function(mouse) {
return mouse.name === '小绿鼠'
}, 1)
这样一来,代码里面剩下部分的信息就很明确了:
- 行为:
arrayFindItem
在数组中找元素 - 在哪里找:
mouseList
- 找啥样的:
function(mouse) { return mouse.name === '小绿鼠' }
- 找几个: 1
- 找到没:
result
虽然清晰了不少,但前提是需要把arrayFindItem
的参数和返回值了解清楚,而就本例而言,有更好的解决方法:
Array.prototype.find
const result = mouseList.find(function(mouse) {
return mouse.name === '小绿鼠'
})
由于是es6
规范里的数组方法,所以大家对它的行为已经非常了解,不需要额外的阅读成本。
第2方面:代码拆分
事情要一件一件地做,代码要一块一块地写。
📄需求
- 把笼子里面生的鼠,并按照下面的做法烹饪一下:
- 竹鼠 -> 油炸
- 家鼠和田鼠 -> 水煮
- 番薯 -> 碳烤🔥
- 做成晚餐,我晚上要吃🍽
✍实现
新手可能写出来的代码
var dinnerList = []
for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].isRaw != true) {
if (mouseList[i].type === '2') {
// 宽油竹鼠
mouseList[i].recipe = '油炸配方'
mouseList[i].newName = '油炸' + mouseList[i].name
// ...被省略的油炸的其他操作
mouseList[i].isRaw = true
dinnerList.push(mouseList[i])
} else if (mouseList[i].type === '0' || mouseList[i].type === '1') {
// 水煮家鼠 + 田鼠
mouseList[i].recipe = '水煮配方'
mouseList[i].newName = '水煮' + mouseList[i].name
// ...被省略的水煮的其他操作
mouseList[i].isRaw = true
dinnerList.push(mouseList[i])
} else if (mouseList[i].type === '5') {
// 烤番薯
mouseList[i].recipe = '碳烤配方'
mouseList[i].newName = '碳烤' + mouseList[i].name
// ...被省略的碳烤的其他操作
mouseList[i].isRaw = true
dinnerList.push(mouseList[i])
}
}
}
console.log(dinnerList)
🔍分析
- 定义数组
dinnerList
- 遍历
mouseList
- 找出所有属性
isRaw
是true
的数组元素 - 在3的基础上,根据属性
type
的不同,进行不同的操作type
是2
==> 油炸操作type
是0
或1
==> 水煮操作type
是5
==> 碳烤操作
很明显,把烹饪过程直接写在for
循环里面会造成循环过长,不利于阅读,所以应该将其拆分出来。
🛠优化
那么我们先进行第一步,将烹饪方法拆分出来
/* ****** 这里是烹饪的方法们 ****** */
function fry(mouse) {
mouse.recipe = '油炸配方'
mouse.newName = '油炸' + mouse.name
// ...被省略的油炸的其他操作
mouse.isRaw = true
}
function boil(mouse) {
mouse.recipe = '水煮配方'
mouse.newName = '水煮' + mouse.name
// ...被省略的水煮的其他操作
mouse.isRaw = true
}
function roast(mouse) {
mouse.recipe = '碳烤配方'
mouse.newName = '碳烤' + mouse.name
// ...被省略的碳烤的其他操作
mouse.isRaw = true
}
这样我们就有3个烹饪方法了,把它们放一起给上注释,既清晰又方便维护。
假设现在的需求是修改某一种烹饪方法,我们只需要找到方法,并修改方法内部的实现就搞定了,甚至不用去管该方法在哪里被调用。
还没完,接着把代码补充完整
var dinnerList = []
for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].isRaw != true) {
if (mouseList[i].type === '2') {
fry(mouseList[i]) // 宽油竹鼠
dinnerList.push(mouseList[i])
} else if (mouseList[i].type === '0' || mouseList[i].type === '1') {
boil(mouseList[i]) // 水煮家鼠 + 田鼠
dinnerList.push(mouseList[i])
} else if (mouseList[i].type === '5') {
roast(mouseList[i]) // 烤番薯
dinnerList.push(mouseList[i])
}
}
}
console.log(dinnerList)
这段循环中出现了if... else if... else if...
,并且判断的对象都是type
,这就证明了里面有可以拆分出来的逻辑。
回顾一下我们最开始的需求
- 竹鼠 -> 宽油竹鼠
- 家鼠和田鼠 -> 水煮
- 番薯 -> 碳烤
让一段代码的逻辑贴近需求,那么无论是代码可读性还是应对需求变更的能力,都会上升一个层次。
接下来就是把需求转换成代码: 左边用类型代替,右边用函数代替,初步版:
条件 | 操作 |
---|---|
type 是 2 |
油炸操作 |
type 是 0 或1 |
水煮操作 |
type 是 5 |
碳烤操作 |
接着用代码符号代替:
type | cookFn |
---|---|
2 |
fry() |
0 || 1 |
boil() |
5 |
roast() |
到这一步会发现这种对映关系就是key
=> value
,key
是老鼠的type
, value
是烹饪的方法cookFn
, 所以我们理所应当用对象来存储对应关系
// 老鼠烹饪方法映射表
const mouseCookFnMap = {
// type: cookFn
'0': boil,
'1': boil,
'2': fry,
'5': roast
}
完美!你如果有强迫症的话,也可以把所有的类型都补充完整,像这样
// 老鼠烹饪方法映射表(强迫症版)
const mouseCookFnMapIllVer = {
// type: cookFn
'0': boil,
'1': boil,
'2': fry,
'3': undefined, // 未指定烹饪方法
'4': undefined,
'5': roast
}
写好了就马上用一下
var dinnerList = []
for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].isRaw != true) {
var cookFn = mouseCookFnMap[mouseList[i].type]
if (cookFn) { // cookFn !== undefined
cookFn(mouseList[i])
dinnerList.push(mouseList[i])
}
}
}
console.log(dinnerList)
这时候,你收到一个需求变动:“宽油竹鼠太费油了,给我改成碳烤。”
只需要把'2': fry,
改成'2': roast,
就🆗了。
并且,使用映射关系表可以轻松应对某些需求更为复杂的场景。
比如说宽油竹鼠
,实际上并非油炸就能完成的,竹鼠的皮肉比较厚实,还需要长时间的焖煮,所以对于竹鼠
,需要先油炸
再焖煮
。
此时,映射表的value
就不单单是一个方法了,应该是多个方法并且是有序的,显然可以用数组来存储:
// 加一个`焖煮方法`
function braise(mouse) {
// ... 省略的焖煮方法具体实现
}
// 老鼠烹饪方法映射表加强版
const mouseCookFnMapPlus = {
// type: cookFnArray
'0': [boil],
'1': [boil],
'2': [fry, braise],
'5': [roast]
}
使用的时候将直接调用方法
cookFn(mouseList[i])
改成遍历数组依次调用
cookFnArray.forEach(cookFn => cookFn(mouseList(i)))
这里实在不想写for
循环了,用了forEach
,下文会劝你们不要尽量写for
循环
第3方面:去除冗余
这一方面主要是从语法层面上,来探讨如何去掉代码中的冗余,具体做法是找到代码中与主题无关或重复的部分(主要是变量),尝试去除它们。
这里就不加新的需求了,直接把上面的例子拿过来用
箭头函数➡
ES6
箭头函数的优点有两个:
- 改变函数内
this
指向
过去为了将函数内部的this
指向到外层作用域,主要方法是
var that = this
// or
var self = this
讲真的,看到that
我头都大了😰,每个函数开始前都定义一个that
不累吗?
而箭头函数中的this
就是指向到外层的,彻底去除了上面这种冗余的代码!💯
能写=>
的时候,就不要写function
。与其说用箭头函数是为了将this
指向到外层,不如说function
关键字是为了将this
指向到本层才会去用。
- 简化写法
先感受一下
mouseList.find(function(mouse) {
return mouse.name === '小绿鼠'
})
// 箭头函数写法
mouseList.find(mouse => mouse.name === '小绿鼠')
少写很多字有没有,附上写法对比
写法 | function | (参数) | => | { | 自动return | 函数体 | } |
---|---|---|---|---|---|---|---|
原写法 | function | (参数) | { | 函数体 | } | ||
箭头1 | (参数) | => | { | 函数体 | } | ||
箭头2 | (参数) | => | // 有 | 单行代码 |
箭头函数太棒了!写者能少写,看者能少看。具体使用看文档,我们接着往下看
循环
首先看这个for
循环,它又长又宽
for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].name == '小绿鼠') {
mouseList[i].cap.color = 'green'
}
}
很明显此处的变量i
毫无意义,那么如何去除i
呢?🤔
第一种是使用ES6
的 for..of
for (const mouse of mouseList) {
if (mouse.name == '小绿鼠') {
mouse.cap.color = 'green'
}
}
不过使用for...of
,即使想要下标 i
它也给不了,推荐一般情况下,能用 forEach
的时候都用
Array.prototype.forEach()
mouseList.forEach((mouse) => {
if (mouse.name == '小绿鼠') {
mouse.cap.color = 'green'
}
})
说明:for...of
可以遍历所有部署了iterator
(迭代器)的数据,而forEach
仅仅是数组原型上的方法。但是你如果铁了心要用forEach
,可以利用展开运算符(...)来把可迭代对象转换成数组:[...iterableValue].forEach()
forEach
虽然好用,但是千万别只用forEach
用到死,数组还有那么多好用的方法,它们封装得更完整也更具语义化,对数组方法不熟悉的话可以多看几遍文档
知道有写着方法,一直想不起来去用怎么办?
在写循环之前,先想想自己最后想要什么,有了明确的目标之后再下手
目标 | 手段 | 返回值 |
---|---|---|
找一项 | find |
数组元素 (没找到是undefined ) |
找一项的下标 | findIndex |
number (没找到是-1 ) |
找多个(过滤) | filter |
array |
复制全部并改造 | map |
array |
有部分是? | some |
boolean |
全都是? | every |
boolean |
... | ... | ... |
解构(析构)
变量的解构有很多种,都差不多,这里只介绍最常用的一种,对象解构
例子
假设我们要取出小白鼠的几个属性,不用解构赋值是这样的
const whiteMouse = { id: 'm01', name: '小白鼠', type: '0' }
const id = whiteMouse.id
const name = whiteMouse.name
const type = whiteMouse.type
用了解构赋值是这样的
const whiteMouse = { id: 'm01', name: '小白鼠', type: '0' }
const { id, name, type } = whiteMouse
优势很明显了,去掉了很多冗余的代码。
解构可以用在很多地方,只要是取对象的某个属性赋值给一个变量,就可以用解构,下面是小绿鼠的例子的加强版
const greenMouse = mouseList.find(mouse => mouse.name === '小绿鼠')
if (greenMouse) {
greenMouse.cap.color = 'green'
greenMouse.cap.size = 'big'
greenMouse.cap.brightness = 'high'
}
由于担心太过委婉以致于小绿鼠没有发觉,我们增大了帽子🎩尺寸并且让帽子变得更加耀眼。
在遇到这种一个对象属性在后文中被多次使用的情况,最好用一个变量来存一下,避免多个 对象.属性.属性...
的写法让代码臃肿不堪,影响阅读。
const greenMouse = mouseList.find(({ name }) => name === '小绿鼠')
if (greenMouse) {
const { cap } = greenMouse
cap.color = 'green'
cap.size = 'big'
cap.brightness = 'high'
}
好理解也好用,不过细心的话会发现这里还有另一个地方也用了解构,就是.find()
的回调函数的参数部分。
这样写可以减少一个自定义的变量mouse
,它也是与主题无关的,而且定义出来只用一次,也算一种冗余。
解释一下这个解构
(mouse) => mouse.name === '小绿鼠'
.find()
传入的回调函数(mouse) => mouse.name === '小绿鼠'
,
它的第一个参数mouse
,是mouseList
中的元素,也就是
{ id: 'm01', name: '小白鼠', type: '0' } // 第一次回调运行时`mouse`的值
{ id: 'm02', name: '小黑鼠', type: '4' } // 第二次回调运行时`mouse`的值
// ...
既然mouse
是一个对象,我们只需要它的name
属性,那就({ name })
只不过是把
const mouse = { id: 'm01', name: '小白鼠', type: '0' }
改成了
const { name } = { id: 'm01', name: '小白鼠', type: '0' }
解构真的很常用,请求接口回来的时候,就经常会这么写
async function getData() {
const { code, msg, data } = await requestFn()
// ...
}
所以这里说一句,请求接口后把回调函数
和.then()
收起来吧😂,你看这async + await
,它不清晰吗,可读性不高吗?
最后
周末两天的时间全花在这篇文章上面了,希望对各位朋友有所帮助。如果文中有什么错误,或者有什么建议,欢迎指出,谢谢大家😘。