代码首先是给写给人看的,就算不给别人看,自己也要能看的明白。
在我的编程生涯初期,我看过的编程书籍里,更偏向于《代码大全》、《代码整洁之道》这样的类型,反而具体的技术细节相关的书籍看的比较少。
这一章内容,我从编写好代码的底层逻辑来讲述。
区别于那些具体教你怎么样优化代码的文章,理解了这篇内容,你将会找到每个优化措施所对应的逻辑,自然而然的也会写出更好的代码。
童子军军规
我初次接触到这个理念是在《代码整洁之道》这本书中,整个理念只有一句话:
让营地比你来时更干净。
每次当你接手一段代码,开发完比你接手前更好,那么项目充斥的坏代码将会更少。
清理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复代码,清理一个嵌套
if
语句。
推崇这个理念,并不是让你时时刻刻都做到,牢记这么一个理念只会让你做事情越来越好,无论是否是编程领域。
六个简单的理念
对于如何编写代码,我遵循这么几个简单的理念。
1 清楚的命名
好的代码能够清楚的表述它所执行的逻辑,这也是所谓的“自解释代码”。
而自解释代码首先就需要你将代码中的变量、方法等声明清楚。
我不推崇一定要用英文进行命名,只要编程语言支持,你可以用任何你喜欢的“字符”来命名你的变量或方法。
但有一点请注意,请完整表述变量或方法所代表的含义。
不要为了省事,而使用看不懂的缩写。
对于长命名,编辑器会有提示补全,你也可以通过鼠标双击选中变量并进行复制。
// bad
function change(bool) {
value = bool;
}
function baDXFangBX() {
kaiBX();
fangDX();
guanBX();
}
// good
function putElephantIntoFridge() {
openFridge();
putElephantIn();
closeFridge();
}
function baDaXiangFangJinBingXiang() {
daKaiBingXiang();
fangJinDaXiang();
guanBiBingXiang();
}
function 把大象放进冰箱() {
打开冰箱();
放进大象();
关闭冰箱();
}
复制代码
清晰的语义让你看到任何一行代码,任何一个变量都能直观的知道这个东西是做什么的,而不需要你去找到变量的声明、或使用的地方,才能知道具体作用。
2 直接了当的语义化代码
虽然下面两段代码的含义相同,但是看到 forEach
就能知道是对数据里的每一个元素进行处理。
for (const item of array) {
// do sth
}
array.forEach((item) => {
// do sth
})
复制代码
你可以试着看看下面哪个代码更好理解。
// first
const target = array.find(item => item === 0)
if (target) {
// do sth
}
// second
const hasTarget = array.map(item => item === 0 ? item : undefined).filter(item => item).length > 0
if (hasTarget) {
// do sth
}
复制代码
第一段的意思是找到一个等于 0 的元素,如果有,做一些什么。
而第二段逻辑是找到数组中等于 0 的元素,过滤并判断数组长度,如果过滤后的数组长度大于 0(有等于 0 的元素),做一些什么。
虽然两段代码的功能是一样的,但是第二段的意思更加“绕口”一些。
3 模块化代码
模块化代码的含义是:将你的代码拆分成单独的“模块”,每个模块的代码拥有自己的含义,大到整个项目,小到一行代码,都可以作为单独的一个“模块”。
拆分方法
将多行语句拆分成方法,是常见的拆分方式。这使得多行代码的逻辑,能够用一句话来表达。
这是我从项目中截取的部分代码,最后四行分别做了设置标记、设置状态、触发默认选中事件,以及计算单元格列宽。使几百行代码在这一段中,浓缩为了“四句话”。
function dealRoomData(data) {
// ... other codes
this.setCombineFlag(selectedRooms, fields);
this.setCombineStatus(fields);
this.emitSelectionChange();
this.calculateCellWidth([]);
}
复制代码
对齐代码
除了拆分成方法,合理划分代码也是一种将代码模块化的方法,使得在文件的某一块具备它自己的意思。
对于方法无论你使用以下哪种方式,都是可以的,主要还是看整体项目风格。
但是无论哪一种,都隐含着“模块”的理念在里面。
对于第一种,fuction
的首字母 f
是和 }
对齐的,在这整个一块区域里,就是 foo
的逻辑。
第二种也是,两个大括号之间的则是。(PS:js
会遇到自动补全分号导致的 bug,所以优先推荐第一种)
// first
function foo() {
// other codes
}
// second
function foo()
{
// other codes
}
复制代码
而对于标签,也是同样的道理,在一个闭合的区域里,包含的是同一功能的内容。
<component-name
:disabled="disabled"
class="some-classes"
:value="value"
@change="handleChange"
>
<other-component></other-component>
<other-component></other-component>
</component-name>
<component-name
:disabled="disabled"
class="some-classes"
:value="value"
@change="handleChange"
/>
<other-component></other-component>
<other-component></other-component>
复制代码
如果是下面这样的形式,至少在我看来,想要一下子找到 component-name
这个标签的属性部分,是不那么直观的。
在脑子里,我需要对比一下这块区域,然后明白,“哦,这块代码是一起的”。
// first
<component-name
:disabled="disabled"
class="some-classes"
:value="value"
@change="handleChange"/>
<other-component></other-component>
<other-component></other-component>
// second
<component-name
:disabled="disabled"
class="some-classes"
:value="value"
@change="handleChange">
<other-component></other-component>
<other-component></other-component>
</component-name>
复制代码
针对函数调用、JSX 也是同理,我会让 (
和 )
两个括号对齐,来形成一个区域,来“封闭”一块内容。
this.selectedRooms.forEach((block) => {
block.children.forEach((room) => {
this.resetRowPrice(room);
});
});
function isMoreThanCurrentDate() {
return (
time.getTime() > new Date().getTime()
);
}
复制代码
让每行代码都有它的含义
除了上面两种情况,你甚至应该让每一行代码都有它自己的意思。
比如下面的代码。
对于组件属性,每一行代表着设置某一个属性,只需要按照纵向扫过去,有没有设置哪些属性将会一目了然。
而方法的调用,第几个参数传的是什么内容,也能直观的看到。
<component-name
:disabled="disabled" // 设置禁用属性
class=" // 设置class
class1
class2
"
:value="value" // 绑定 value
@change="handleChange" // 绑定 change 事件
/>
renderSomeComponent(
id,
getName(someCondition, anotherCondition),
age,
icon,
getAddress(id, name, age)
)
复制代码
下面这样的代码都是我项目中实际遇到过的,你不能一下子看到组件有哪些属性,方法第几个参数是什么。
你必须得一个个看过去,甚至还需要区分哪一部分是调用方法获得的。
<component-name :disabled="disabled" class="class1 class2" :value="value" @change="handleChange">
renderSomeComponent(id, getName(someCondition, anotherCondition), age,
icon, getAddress(id, name, age))
renderSomeComponent(anotherId, getName(someCondition, anotherCondition), age,
icon, getAddress(anotherId, name, age))
复制代码
4 适当注释
当代码能够清楚表达它所做的事情的时候,是不需要额外的注释来为它做解释的,但不代表着永远不需要写注释了。
代码只能向阅读者传达出这段代码的作用,但是为什么要这么做,却只能通过注释来向读者说明。
这是一个项目中表格列宽自适应部分的代码,这里的注释说明了为什么要这么做的内容——防止单元格内容被遮挡。
// 计算表格列宽。
// 防止单元格内容因宽度不够,被遮挡的问题。
calculateCellWidth(currView) {
// 计算单元格列宽代码
this.checkTableWidthGap();
},
// 在 body 宽度与表头宽度不足一个单元格时,将剩余宽度补足。
checkTableWidthGap() {
// 列宽补足代码
},
复制代码
还有一些程序员之间用来交流的注释?。
/***
* 这个公司没有年终奖的,兄弟别指望了,也别来了,我准备辞职了
* 另外这个项目有很多*Bug* 你坚持不了多久的,拜拜!
*/
复制代码
5 单一职责
一个文件维护一类代码、一个方法维护一个功能、一块区域里的代码做一件事、一行代码只代表一个意思。
当遵循这么一个简单的理念,从一行代码到整个项目,都会有着自己的含义。
前面说的将代码模块化也是为了保持每个模块的职责单一。
6 减少重复
当代码中出现重复的部分的时候,就是考虑拆分的时候了。
相关属性及方法拆分到一起,就是类;
模板+逻辑+样式拆分到一起,就是组件;
功能相关的代码拆分到一个文件,就是模块文件。
类、组件、模块、框架,或者说封装操作,正是为了聚合关联性强的代码,使之形成一个又一个的“模块”。
而这些封装操作将大块代码聚合在一起,正是为了保持职责单一,并且后续如果需要进行维护或功能扩展时也会变得方便。
比如遍历数组元素并设值。对于下面这样的代码,完全可以在循环体顶部声明一个变量然后取值。
这里只有两行,我遇到过七八行代码都是这样的,当取值逻辑变化的时候,比如变成 i + 1
,那所有取值的部分都需要修改一次。
for (let i = 0; i < people.length; i++) {
somebody.id = people[i].id;
somebody.name = people[i].name;
}
复制代码
拆分方法也是同理,如果某一段相同的代码逻辑需要变动,那么所有地方都得修改一次。
如果我们要对内部进行改造,只要保持整个模块内部的功能是一致的,也不用担心原有功能受到影响。
代码质量指标
在最后,我想给你列出这 7 个衡量代码质量的指标。
比较公认的标准有7个:
- 可维护性(maintainability)
- 可读性(readability)
- 可扩展性(extensibility)
- 灵活性(flexibility)
- 简洁性(simplicity)
- 可复用性(reusability)
- 可测试性(testability)
其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。
结语
今天分享的这六个理念足够让你打下一个写好代码的基础,至于代码要不要写分号、括号前后要不要空格、一行代码不能超过多少列等这样的要求交给 lint
工具吧。