历经多年,亦或参与过多个多个项目,亦踩坑之数,不可细数。今日在这里记下一笔,当作日后回看之用,如果有什么不对的,请各位看官帮忙指出,以便日后修正。多谢!多谢!
声明:下文中种种案例,收集的均为真实案例(代码均做过脱敏处理,如有雷同,纯属巧合!)
作为程序员,代码规范,是一个必谈的话题,本文是对Code Review主要Review什么?的延续,欢迎各位同学点评讨论!
1. 异常报错
Cannot read properties of undefined
不知此画面是否熟悉,明明上周代码运行还好好的,怎么这周就出问题了呢? 难道是后端同学的数据返回有问题?或者是哪位前端同学调用函数时传递过来的参数不对。
function getUserIconUrl(userInfo){
const icon = userInfo.icon
if(icon.startsWith('https://'){
return icon
}
return `https://example.icon.com/images/${icon}`
}
代码分析
简单分析下此函数,可能存在以下问题:
- 入参合法性未校验,入参需是一个Object对象,但实际只用到
icon属性的值。在入参传入正确时,没有什么问题,但若非Object对象,如:Array, undefined, null, false, string等类型的异常值时,则均可能会出现问题- 建议: 入参,即外部输入的数据,均不可信,在编码时,我们需要对其进行核验,如若出现异常时,做出相应的处理,避免程序执行出错。
代码整改
在不修改入参的情况下,我们修改函数如下:
function getUserIconUrl(userInfo, defaultIcon = 'https://example.icon.com/images/user/default.png'))
const icon = _.get(userInfo, 'icon', '')
if(typeof icon === 'string'){
if(icon.startsWith('https://')) return icon
else return `https://example.icon.com/images/${icon}`
}
return defaultIcon
}
如果可以修改入参,建议修改为:
function getUserIconUrl(iconUrl, defaultIcon = 'https://example.icon.com/images/user/default.png'))
if (typeof iconUrl === 'string'){
if (iconUrl.startsWith('https://')) return iconUrl
else return `https://example.icon.com/images/${iconUrl}`
}
return defaultIcon
}
小结
- 函数入参尽量选择使用基本数据类型, 如:string, number, boolean等,这样会使函数更易受控。
- 若使用复杂的数据类型,如:Object, Array,或都有甚者使用 二维数组等更复杂的数据作为函数的入参(若因历史原因等特殊情况,必须要传递这样的入参,建议将函数对入参的数据进行分层处理)
Uncaught TypeError
不知这样的报错,是否熟识?
和大多数“祖传”代码类似,维护或新增代码时,需格外小心谨慎,但还是防不住。
报错的原因,很快可定位出来:
const entities = _.get(db, 'source.entities')
取得的变量entities值为undefined。因此,后面执行forEach报错,就好解释了。
function getEntities(srcDBs, type) {
const entity = _.get(getCurrentDB(), 'source.entity')
const arr = []
const result = []
Object.values(srcDBs).forEach((db) => {
const entities = _.get(db, 'source.entities')
entities.forEach((e) => {
arr.push(e.name)
const entNum = _.filter(arr, (v) => v === e.name).length
if (entNum === viewLen) {
result.push(type === 'options' ? {text: `${e.name}-${e.label}`, value: e.name, default: e.name === entity} : e.name )
}
})
})
return result
}
代码分析
初步分析,代码存在以下问题:
- 建议以不可信任的态度处理外部数据,以确保程序能够正常执行。
- 三目运算符,建议在一些简单场景使用,如:
a ? true : false, 复杂的场景,建议使用if/else,例如下面这段,个人建议从可读性考虑,使用if/else代替。
result.push(type === 'options' ? {text: `${e.name}-${e.label}`, value: e.name, default: e.name === entity} : e.name )
代码整改
function getEntities(srcDBs, type) {
if (!_.isObject(srcDBs)) return []
const entity = _.get(getCurrentDB(), 'source.entity', '')
const arr = []
const result = []
Object.values(srcDBs).forEach((db) => {
const entities = _.get(db, 'source.entities', [])
if(_.isArray(entities)) {
entities.forEach((e) => {
arr.push(e.name)
const entNum = _.filter(arr, (v) => v === e.name).length
if (entNum === viewLen) {
if (type === 'options') {
result.push({
text: `${e.name}-${e.label}`,
value: e.name,
default: e.name === entity
})
} else {
result.push(e.name)
}
}
})
}
})
return result
}
小结
对于接收到的外部输入的数据,在实现函数时,建议采用不可信任的态度,对其进行严格校验或判断,避免异常(极端)场景未处理到,导致执行时报错。
2. 代码可读性
“魔鬼变量”
即不知道从哪冒出来的,无任何意义的变量或值,例如:
function isShowBtnGroups(){
return getCurrentMode() === 0 || getCurrentMode === 1
}
函数isShowBtnGroups在执行一些业务判断,以决定BtnGroup是否显示。咋一看,函数的实现好像没有什么问题,但仔细读起来会发现 getCurrentMode() === 0或 getCurrentMode() === 1 这里的 0和1代码什么意思呢?
代码分析
可以看出,函数体内部实现比较简单,但突然冒出的变量,着实让代码的可读性差不少,建议整改!
代码整改
function isShowBtnGroups(){
const MODE = {
read: 0,
edit: 1
}
return getCurrentMode() === MODE.read || getCurrentMode === MODE.edit
}
小结
整改后代码,虽然新增了一个看起来“多余”的变量,但可读性增强不少,不用写注释。 同时,可维护性也增强不少,例于后续因业务变化而进行代码重构。
简单问题复杂化
项目中,有使用到一些成熟稳定的公共工具函数或库(如:lodash),在实际开发中,却编码随意,例如:
function initToolBarItems(toolBars){
console.log('toolBars', toolBars)
}
function getToolsInfo(config) {
let toolBarItems = []
if (config && config.toolbar) {
let toolbar = config.toolbar || {}
if (toolbar && toolbar.groups && toolbar.groups.length) {
toolBarItems = toolbar.groups[0].items || []
}
}
return initToolBarItems(toolBarItems)
}
代码分析
可以看出,函数在做各种判断,以便对入参进行校验。但仍存在以下问题:
- 可读性差
- 建议:可以考虑使用lodash来简化处理
- 函数复杂度高
- 建议:可以考虑使用lodash来简化处理
代码整改
function initToolBarItems(toolBars){
console.log('toolBars', toolBars)
}
function getToolsInfo(config) {
const toolBarItems = _.get(config, 'toolbar.groups[0].items', [])
return initToolBarItems(toolBarItems)
}
小结
合理的使用工具库,能够帮助我们提高工作效率,增强代码可读性。
问题综合体
下文所示的示例代码,为实际项目中所发现的,作者是一位有近2年前端开发经验的同学,项目的基本功能都能较好的完成,但每次在阅读他的代码时,难以言表,比如:
export function setDefaultImg(res) {
let imgUrl = getImagePrefixUrl()
let previewImgSrc = require('@/asserts/images/icon.png')
if(res.imgId) {
previewImgSrc = `${imgUrl}${res.imgId}.${res.type}?t=${+new Date()}`
}
return previewImgSrc
}
代码分析
上例函数, 其实是想对传入的参数进行判断, 如果值无效, 取默认图片, 否则返回拼接地址好的img src相对地址. 从函数的实现来看, 有如下几个问题:
- 函数名setDefaultImg并不合适, 无任何对数据的更新/修改, 最终目的是想拿到img的拼接地址(或者默认图片)
- 形参
res是一个对象, 实际使用到的属性只有两个基本类型, 遵循最少知识原则, 应修改形参. let imgUrl, 其实是拼接url的前缀, 取名如imgPrefixUrl会比较合适.- let 使用不当, 如
let imgUrl, 因涉及不到变量再赋值, 使用const是比较合适的 res.imgId, 即异常情况的处理, 缺少了对res.type的处理- 变量命名, 如
previewImgSrc, 取此名也不太合适, 该src使用的目的并不确定, 建议使用有比较通用涵义的名字
代码整改
export function getImgSrc(imgId, imgType) {
if(typeof imgId !== 'string' || typeof imgType !== 'string') {
return require('@/asserts/images/icon.png')
}
return `${getImagePrefixUrl()}${imgId}.${imgType}?t=${+new Date()}`
}
小结
函数功能尽量单一,入参尽量简单化,函数更易控制。
项目开发中,程序都是对数据进行读取或写入,因此,最常使用的是 getter / setter, 关于getter / setter,有兴趣的同学,可以阅读下这篇文章JavaScript getter-setter金字塔
3. 大复杂度函数
import _ from 'lodash'
function codeScript(data, codes){
if (data.item.type === 'customType') {
cells = data.item.layout.cells
cells && cells.forEach((cell) => {
if (cell.cont && cell.cont.type === 'demoType') {
if (cell.cont.toolbar && cell.cont.toolbar.groups) {
(cell.control.toolbar.groups || []).forEach((group) => {
(group.items || []).forEach((item) => {
(item.events || []).forEach((event) => {
let code = _.find(codes, (v) => {
return v.contId === item.itemId
}) || {}
event.script = code.script || ''
})
})
})
}
}
})
}
return data
}
代码分析
初步来看,代码可能存在以下问题:
- 可读性差, 相信看到这样的函数,真是头大!
- 建议:复杂的业务函数,建议使用分层函数进行处理
- 入参合法性未检验,程序执行时可能会出错
- 建议:对入参进行合法性校验
- 函数复杂度高
- 建议函数复杂度不要超过5-10,同时建议开启eslint函数复杂度强制检查 eslint-complexity
代码整改
import _ from 'lodash'
function codeScript(data, codes){
const type = _.get(data, 'item.type', '')
if(type === 'customType'){
forEachFn(data, 'item.layout.cells', [], cell => {
const contType = _.get(cell, 'cont.type', '')
if(contType === 'demoType'){
forEachFn(cell, 'cont.toolbar.groups', [], group => {
forEachFn(group, 'items', [], (item) => {
forEachFn(item, 'events', [], event => {
const itemId = _.get(item, 'itemId', '')
const code = _.find(codes, v => v && v.contId === itemId) || {}
event.script = code.script
})
})
})
}
})
}
return data
}
function forEachFn(obj, path, callback){
const value = _.get(obj, path, defaultValue)
_.isArray(value) && value.forEach(item => callback(item))
}
小结
整改后,看起来会稍微好些,但奈何数据结构和业务如此,还未想到更好的代码表达方式。 不知道各位看官,有没有更好的整改办法。
相关链接
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。