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