如何写出好代码之可读性
可读性是好代码的一个重要指标,只有代码能让人读懂,才有可维护性,所以可读性是可维护性的基础。
如今大部分程序员都知道可读性很重要,可是如何提高可读性,有哪些具体可操作性的方法呢,本文从命名、短小精悍、清晰的结构、不玩魔术四个大的方面进行总结,由于笔者本人是前端开发,所以示例大部分用js,不过道理基本都是相通的,才疏学浅,欢迎指正。
一、 命名
提升代码可读性的最快速最有效的方法就是命名,别的开发技能需要大量的学习才行,而命名是我们很快可以掌握的,
关键是要有重视命名的意识。
想要快速提高编程水平,从每一个命名开始!
见名知意
名称要能反应代码的意图,看到名称就能知道作者想要表达什么。
比如CR中经常可以看到的:
let arr = [] //arr是什么? appList?
let obj = {} //obj代表什么? appDetail?
let str = ''
//e是什么,结构是怎么样的,必须上下搜索handle才能明白
//handle({appName, appStatus})?
function handle(e){
if(e==0){
}
}
//data是什么,状态status?
function getColor(data){
}
//求求你饶了我吧,flag是啥? isAdmin、canOperate?、shouldCreate
function create(flag){
if(flag){
}else{
}
}
不好的命名带来理解上的困难,有时候必须要看完整个函数,才能猜测出大致的含义,有的要去结合上下文,
看看别处传来的参数是什么含义,而如果传入参数的地方,命名也不良好,就要继续往上查找了,经过这么一串查找,
有时候都快忘记为什么要去看最初那个函数了...
常见的命名方式
平时使用使用最多的命名大致分为这样几种:资源、状态、 事件、动作、条件
-
资源命名一般为"资源名称 + 资源类型":如 appList、appDetail,单纯的List或者Detail,通用性太强,
不知道想要表达什么,特别是一个页面有多种资源时。 -
状态命名最好也加上资源名称,如runtimeStatus、clusterStatus
-
事件处理函数一般为动宾短语,如deleteApp、destroyCluster
- 尽量不要叫 deleteData这样的通用性的词,存在多种资源时不知道Data代指何物
-
动作一般为动宾短语,也可以是个句子,关键要把做的事情表达清楚,如getAppDetail、syncK8sStatusToCluster
-
条件一般为布尔值,布尔值一般分为这几种:是什么(is)、可以吗(can)、应该吗(should)、有吗(has)
- 是什么: isAppRunning、 isDelivery
- 可以吗:canDelete、canSave
- 应该吗:shouldUseCache
- 有吗:hasDeletePermission
- 进行时/过去式:enabled/disabled、destroying/destroyed
去除无意义的词
像数据类型这样的词没有太多的意义,通常可以去除或者换成更友好的词
//bad
let appArr = []
//good
let appList = []
//bad,肯定是函数,无需多言
function deleteFun(){
}
//good
function deleteResource(){
}
//或者有时想要表达事件处理函数,又容易和其他重名
//可以添加Handle代替Fun,Handle一般代指事件的处理函数,调用方一般为事件
function deleteResourceHandle(){
}
好名字可以念出来
对比下下面的代码
function delProdFun(){
}
function deleteProductHandle(){
}
当和同事沟通时,"哎,小李,你的那个d-e-l-p-r-o-d-f-u-n(逐个单词念出来)函数好像有点问题哎~"
代码是要和别人沟通的,必须能够通过人的语言讲出来,否则写的时候自己没感觉,一到多人沟通的时候可能要闹笑话了。
如无必要,尽量不用简写。
好名字不怕长
有时候会担心名字太长会不会不太好,是不是太长了写的就慢,其实不是,目前编辑器都有提示功能,
写出几个字符就会有推荐名称可以选择,不会增加编码负担。
长的名字通常具有较好的表现力,能把要做的事通过命名表现出来,所以好的命名就是注释、文档。
//同步状态到集群中
function syncStatus(){
}
function syncStatusToCluster(){
}
较短的名称一般很容易和别人重复,而较长的名字一般都有其特定场景,不容易和其他业务重复,对于全局搜索也比较友好。
拒绝误导
更有甚者,有时候命名和要表达的意思可能是反的,这种误导性的命名,可以当做bug处理了。
特别是在一些Boolean值应用时容易出现。
export function dealPathWithParams(url, obj,isEnvToProject) {
if (!isEnvToProject) url = isEnv(url);
}
不过幸好,一般都不会犯这样的错误,但是下面这个错误非常场景,比如在get命名的函数中会做很多set的工作。
function getDetail(){
requrst(url,params).then(_=>{
setData()
})
}
比如在获取cache或者localStorage时做一些set操作,命名和要做的事情不一致,或者名称只说了一件事,
函数内部干了多件事,这也同时违反了单一职责原则。
function getCache(key){
if(!cacheData[key]){
setCache(key, '')
}else{
clearCache(key)
return cacheData[key]
}
}
统一表达
一个团队沟通要想高效,必须使用相同的语言,一致的表达方式,减少不必要的学习及认知成本,写代码也是一样。
特别是在涉及一些业务问题时,同一个业务,如果使用不同的词语来表达,难免增加不必要的沟通理解成本。
制定一个领域词汇表
可以将业务领域内的相关词汇抽取出来形成一个词汇表,一般为名词,代指业务中涉及的资源和概念。
一来后续新人进入可以通过这些名词来了解业务的大概面貌, 二来统一了大家在业务方面的认知,减少沟通成本,
如什么是Commodity和product,接入环境到底用Environment还是Project,
机器是用Host还是Machine? 如果没有明确下来,读写代码时可能就很迷惑。
变量名 | 含义 | 备注 |
---|---|---|
P3M | 百度P3M商品 | |
Commodity | 商品组 | |
Environment | 环境 | 接入环境、版本 |
App | 容器应用 | |
AppVersion | 容器应用版本 | |
NativeApp | 裸机应用 | |
ResourcePool | 资源池 | |
Cluster | 集群 | |
Host | 机器 | |
Delivery | 交付中心 |
除了业务领域统一用词外,还有一些通用的字段可以规范下来,比如创建时间是用create_time还是create_at,
创建人是create_by还是creator,虽然表达含义并无多大区别,但是经常变化也会让人感觉不够规范专业。
统一命名格式
一个团队必须统一命名格式,不同格式的命名表达不同的含义,通过命名就可以指定变量是做什么用的,
而且也让外人看来,更加的专业,增强信任感。试想一下,假如我们对外提供了一个接口,有的返回的变量格式是下划线,
有的是大驼峰,别人又怎么能相信功能是靠谱的呢?
一般常见的命名规范如下:
- 变量:小驼峰,如appDetail
- 函数:小驼峰,如getAppDetail
- 类名:大驼峰,如EventBus
- 常量:大写+下划线分割,如APP_STATUS
- 目录:小写+中划线分割,如native-app
- 文件:小驼峰,如eventBus.js,也有采用中划线分割的,如 event-bus.js,团队内部统一即可
- 组件:大驼峰,如TnTable.vue,如果在目录下的index文件,则用小写,如index.vue
- class:类名一般应为小写+中划线,参考html元素的规范,html中一般小写+中划线
有了规范,在CR时就可以针对问题进行评论,否则有时候会感觉,这样也行那样也行,最后造成代码的混乱。
最后提醒一下,规范并不是为了让人记忆的,最好是搭配ESLint这种校验工具,我们的目的是产出规范的代码,
而不是让每个人都背会规范,增加新人融入成本。
二、短小精悍
除了命名之外,第二个最容易掌握的技巧就是把代码弄短。
虽然短小不是目的,但却是提高代码可读性的一个关键手段,短小的代码有以下好处:
- 短小的东西天然的更容易理解,相比动辄几百上千行的函数或者文件,几行或者10-20行的代码需要更小的认知负担
- 短小的代码更有可能做到单一职责,也更有可能被复用
- 要实现短小,逼迫你去思考代码的结构,而不是杂糅一堆代码
写文章还是写诗
比如我要做一道中原名吃蒸面条,我们来看看两种表达方式有什么不同
文章的写法:
《蒸面条》
我去菜市场买了4块钱的面条,然后又买了一袋豆芽,还买了新鲜的五花肉,
回到家里,把豆芽洗干净,五花肉切片,拍了几颗大蒜,又找了两根葱切段,还切了几篇姜,然后起锅烧油,
加入五花肉炒出油,然后加入葱姜蒜爆香,翻炒一会再加入豆芽,继续大火翻炒,最后加入水,水开放上蒸笼,
把面条放上去,蒸个20来分钟,关火焖一会。
用代码写的文章
function zhengmiantiao(personCount){
goToMarketByBike()
if(personCount < 3){
buyNoodles('500g')
}else{
buyNoodles('1000g')
}
bugDouya('200g')
let meatPrice = getPrice('meat')
if(meatPrice > 18){
buyMeat('500g')
}else{
buyMeat('1000g')
}
washDouya()
cutMeat()
paiSuan()
qieCong()
qieJiang()
fire()
drainOil()
if(temperature > 300){
putMeat()
putDouya()
putWater()
}
sleep('5min')
putNoodles()
sleep('20min')
}
接下来采用诗的写法:
《蒸面条》
买菜,买面条、买豆芽,买五花
配菜,洗豆芽,切五花,拍蒜、葱切段、姜切片
炒菜,起锅烧油,炒五花,加调料,加豆芽,加水烧开
焖面,放蒸笼,铺面条,蒸20分钟,焖5分钟
function zhengmiantiao(person){
buyFoods(person)
prepare()
friedVegetable()
braisedNoodles()
}
function buyFoods(person){
goToMarketByBike()
if(personCount < 3){
buyNoodles('500g')
}else{
buyNoodles('1000g')
}
bugDouya('200g')
let meatPrice = getPrice('meat')
if(meatPrice > 18){
buyMeat('500g')
}else{
buyMeat('1000g')
}
}
function prepare(){
washDouya()
cutMeat()
paiSuan()
qieCong()
qieJiang()
}
function friedVegetable(){
fire()
drainOil()
if(temperature > 300){
putMeat()
putDouya()
putWater()
}
sleep('5min')
}
function braisedNoodles(){
putNoodles()
sleep('20min')
}
可以看到,采用诗一样的写法,更容易让人看懂整个流程,如果需要去了解某个细节,再进入某个细节函数进行查看即可,
整体可读性得到大大的提升。
拆分并不是简单的把一个大函数实现硬性拆解到几个小函数,而是真正的去分解一个业务流程,各个子函数之间互相解耦,
并不互相干涉。 以上面蒸面条为例,假如我们发现菜糊了,那我们就去炒菜函数friedVegetable检查,是否温度不合适,
如果发现肉块太大了,那就去优化配菜函数prepare,在试吃人员提出bug之后,我们无需浏览整个蒸面条的细节,
而是可以直接定位到bug可能存在的地方,只阅读那块代码即可,通过这种方式,降低了认知负担,提高了可读性。
用结构化的思维去思考,用结构化的形式去编程。
仅在最底层的函数中实现细节,上层函数更加关注流程和结构。
臃肿的文件
当接手一个几千行的代码时,想必都会从由衷的说一句'oh shit',不管你有多少理由写出这样的代码,
一定在某种程度上缺乏深度思考和代码设计,很少遇到一个优秀的开源库,里面出现上千行的代码,一般都100-300行之间,
他们的复杂度肯定要比我们平时的业务复杂的多,他们都没有超过千行,我们有什么理由呢?
超大的类
一般我们前端在写业务时很少遇到写类的情况,但是如果是要写一些底层库,通常还是避免不了的,针对超大的类,也有很多方法,
减少类文件的大小。比如我们常用的Vue,本身就是一个类,如果把一个Vue类的所有属性和方法集中在一个文件,可读性肯定是非常差的。
以Vue类的实现为例
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
//Vue类的构造函数
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
//在不同的文件中,给Vue类增加相应的方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
主文件中,仅仅定义一个构造函数 function Vue(), 并没有为其添加任何属性和方法。通过将Vue类的功能进行结构化的拆分,
分成几个不同的文件去为Vue类增加方法。
以state.js为例,在stateMixin方法中,为Vue增加原型方法,这样就通过多个文件,共同创建一个超大型的类。
export function stateMixin(Vue: Class<Component>) {
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function () {
//...
}
}
除了这种方式来实现一个超大的类外,还可以将一个大类中的不同模块拆分成不同的类,然后再找个大类中进行组合。
比如一个组件类Component,可能包含数据管理、渲染管理、事件管理,那么我们就可以把数据管理Store和事件管理Event单独抽出去实现,
在组件类中组合Store和Event两个类,共同完成一个复杂的任务,这样组件类Component的文件长度就大大降低了。
import Store from './store.js'
import Event from './Event.js'
class Component {
constructor({el, template, data, computed, mounted, updated, methods}) {
this.store = new Store({data, computed});
this.event = new Event();
}
}
超大的页面文件
现在的前端组件化开发已经非常成熟了,Vue和React等框架为我们实现组件化提供了很好的支持,我们可以很方便的进行组件开发。
页面就是由一个一个的组件构成,通过将一个大的页面拆分成一个一个小的组件,一方面符合分治的思想,将大任务拆分为小任务,
然后逐个去实现,一个大的功能也许很复杂,但是具体到一个个的小组件上,都是一个可以实现的小任务,自上而下拆分,自下而上实现。
比如在实现一个复杂功能的时候,我一般习惯上来就按照功能拆分成不同的组件,然后再一个一个去实现。
以一个简单的列表页,上来可能就先写几个空的组件文件,并不去真正实现,也许只是简单的返回一个div,
先把整体的页面架子搭好,然后再去依次实现,其他更复杂的功能也是同样的道理。
<template>
<div>
<TableHeader>
<OperateButtons />
<SearchForm />
</TableHeader>
<CommonTable />
</div>
</template>
将大页面拆分成小组件去实现,还有个好处就是可以多人并行开发,如果不这么拆分,就算有10个人,也只能一个人写,其他人看。
毕竟我们没法10个人在一个文件里面去写代码,那样造成的冲突不敢想象。
组件化拆分带来的另一个好处就是复用,通过抽取小的公共的组件,能够大大提升后续功能的开发速度,提升研发效率及质量。
一般一个页面文件,不建议超过300行,dom结构不建议超过100行,否则就带来很大的理解负担。
小文件一定比大文件更易读,如果不是,那一定是拆分人的错。
总结
- 要写出短小的代码,必须结构化的思考,理清业务结构和流程,然后进行拆分实现
- 代码应该像诗一样具有表现力,而不是写一个长篇大论的文章
- 底层函数实现业务细节,顶层函数控制业务流程和结构
- 超大的类,可以通过拆分成不同的模块文件去实现相关方法,也可以将一些模块实现成类,在大类中进行调用
- 组件化开发页面,每个文件不超过300行
- 小文件一定比大文件更易读,如果不是,那一定是拆分人的错,拆分时注意解耦
三、清晰的结构
使用卫语句
卫语句也就是提前return,防止if嵌套层次太深。
function install() {
if (hasCluster()) {
if (hasHost) {
installApp()
} else {
showToast("没有机器")
return
}
} else {
showToast("没有集群")
return;
}
}
使用卫语句改写后如下,改写的关键就是翻转if条件,让其尽快return
function install() {
if (!hasCluster()) {
showToast("没有集群")
return;
}
if (!hasHost) {
showToast("没有机器")
return
}
installApp()
}
switch代替多个if else
function commandHandle(command){
if(command === 'install'){
install()
}else if(command === 'start'){
start()
}else if(command === 'restart'){
restart()
}else{
showToast('未知命令')
}
}
switch比较适合这种判断条件比较单一的情况,如判断是否等于某个字符串或者数字
function commandHandle(command){
switch (command){
case 'install':
install();
break;
case 'start':
start();
break;
case 'restart':
restart();
break
default:{
showToast('未知命令')
}
}
}
当然如果事情如此简单,我们还可以进行更进一步的优化,通过一个map来指定每个命令对应的动作。
function commandHandle(command) {
let commandHandleMap = {
install,
start,
restart,
default: () => showToast('未知命令')
}
let handle = commandHandleMap[command] || commandHandleMap['default']
handle()
}
跳出链式调用
这里的链式调用是指在一个函数A中调用函数B,函数B中调用函数C,如果要弄明白整个流程,
则需要沿着链条一步一步往下去查看,有些情况可以将链式调用改为顺序结构。
以平时最常见的一个组件初始化为例,再下面这个例子中,首先在组件挂载时调用init,init中调用获取数据getData,
获取数据后调用格式化数据formatData,格式化数据之后再调用setFormData给表单赋初始值。
<script>
export default {
mounted(){
this.init()
},
methods:{
init(){
this.getData()
},
getData(){
request(url).then(res =>{
this.formatData(res)
})
},
formatData(res){
let initData = res.map(item => item)
this.setFormData(initData)
},
setFormData(initData){
this.formData = initData
}
}
}
</script>
如果想要弄明白init中到底发生了什么,需要沿着链条一个一个去读明白,增加了要了解的知识面范围。
我们只需在函数中返回数据,就可以将链式调用改为顺序结构
<script>
export default {
mounted() {
this.init()
},
methods: {
async init() {
let data = await this.getData()
let formData = this.formatData(data)
this.setFormData(formData)
},
getData() {
return request(url).then(res => {
return res
})
},
formatData(res) {
return res.map(item => item)
},
setFormData(initData) {
this.formData = initData
}
}
}
</script>
改写之后可以清楚看到init中发生了3件事,获取数据、格式化数据、给表单赋值,对整体流程有了明确的认识,
后面有需要可以再分别进入具体函数进行查看。
使用管道代替循环
使用管道操作可以大大简化代码的写法,去除一些无用的代码干扰,只关注需要关注的数据。
比如从一个应用列表找出运行中的应用id,使用for循环写法如下:
let appList = [
{
id: 1,
name: '应用1',
status: 'running'
},
{
id: 2,
name: '应用2',
status: 'destroyed'
}
]
function getRunningAppId(){
let ids = [];
for(let i = 0; i < appList.length; i++){
if(appList[i].status === 'running'){
ids.push(appList[i].id)
}
}
return ids
}
使用管道改写后
let appList = [
{
id: 1,
name: '应用1',
status: 'running'
},
{
id: 2,
name: '应用2',
status: 'destroyed'
}
]
function getRunningAppId(){
return appList.filter(app => items.status =='running').map(app => app.id)
}
管道操作将关注点聚焦到过滤条件是什么,想要的map数据结构是什么,也就是将焦点聚焦到业务需求,
而不是关注点被噪音代码分散。
同一个层次对话
一个函数内部的实现,应该在做同一个层次的事情。
比如在一个战略会议上,别人发言都是谈方向谈策略,你不能发言说自己今天的工作安排是什么,这样的细节和抽象混合在一起,
显然是很不合适的。
比如我们上面提到的蒸面条函数,整个函数想要表达蒸面条的一个具体流程,就不要把怎么炒菜的过程写在整个函数中,
炒菜的细节和买菜、配菜、焖面,不在一个抽象层次,显得很混乱。
function zhengmiantiao(person) {
buyFoods(person) //买菜
prepare() //配菜
//大谈炒菜的细节
fire()
drainOil()
if (temperature > 300) {
putMeat()
putDouya()
putWater()
}
sleep('5min')
//炒菜结束
braisedNoodles() //焖面
}
明确的数据结构
使用js编程遇到的一个很大问题就是,不知道参数的数据结构,导致进行函数调用时,需要阅读完整的上下文,
以了解到底参数是个什么结构,特别是在参数是对象时,这个问题更加明显。
在不使用ts的情况下,我们可以通过注释和对象解构来表明对象的具体结构。
function addNews(news){
//不知道news的结构
}
可以通过js的对象结构来表明参数结构,这样别人调用这个函数,就知道传入什么结构数据了。
function addNews({title, content, keywords}){
//不知道news的结构
}
四、不要玩魔术
所谓玩魔术,就是超出预期,比如莫名其妙有个值可以使用,或者某个值突然变化了,抑或突然之间产生了某种副作用。
总之不在显式的预期范围内的,都有点像玩魔术,写代码应该尽量减少这种魔术,这不是惊喜,通常是惊吓。
数据来源在预期之内
以react为例,目前引入了hooks来进行逻辑的复用,相较于之前的高阶组件,hooks的调用更加显式。
通常高阶组件会向被包裹的组件传递属性, 比如下面这个,会向组件传递一个data属性,如果一个组件被多个高阶组件包裹,
则会传递更多属性。
function hoc(WrappedComponent) {
// ...并返回另一个组件...
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
data:[]
};
}
//...省略业务代码
render() {
//高阶组件会向组件传递属性
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
在组件内部可以通过this.props来进行获取,那么当你只看这个组件时,这个props下的data属性从何而来?
要想搞明白这个魔术数据,可能你要沿着包裹一层一层往外查看,增加了阅读障碍。
class ChildComponent extends React.Component {
constructor(props) {
super(props);
console.log(this.props.data)
}
}
而采用hooks这没有这方面的疑问,所有的数据来源都是清晰明确的,可以直接看出data是从useData来的。
function ChildComponent(){
let data = useData()
}
vue中的mixin也是有点像玩魔术,引入mixin后,组件内部多了很多可以访问的属性和方法,
但是这个属性和方法并没有在当前组件定义,如果没有仔细看mixin,甚至觉得这是写代码人的bug,
而且如果引入多个mixin,就更不清楚这些变量和方法从何而来,这简直是在玩魔术,
所以目前vue改用了组合式Api来解决这些问题。
这种魔术的数据来源,节省了一次引入的工作,可是这个节省的工作缺导致可读性大大降低,得不偿失。
程序员最怕的是不确定性,让一切尽在掌握之中才是最佳做法。
依赖全局数据
注意这里说的是全局数据,包括全局变量以及其他一些全局共享的数据,如vuex、redux,全局数据也是一种魔术魔术,
他很神秘,不知从何而来,也不知要往那里去,神出鬼没。
使用全局数据进行首次开发的人很爽,通过全局共享,降低了各模块之间传参的复杂度,但是对于后续维护的人来说,
很不友好,不明白全局数据首次在什么时机设置,对使用的时序有限制,比如我在首次设置之前调用就为空,可能会出错,
其次不知道有什么人会对其进行修改,也不知道我对全局数据的修改对其他人有什么影响,多个模块通过全局数据紧紧耦合在一起。
如无必要,尽量减少全局数据
前几年曾经看到过这样一些react代码,将各个组件的内部数据全部放到redux,包括不需要共享的内部state,
现在看来这是一种糟糕的设计。
全局数据只存放多模块组件共享的最小量数据,最大限度减低全局变量带来的耦合和阅读故障。
将全局数据改为参数
比如在一些utils函数中可能也用到了全局数据,按理说utils是不应该耦合全局数据的,希望utils是一些简单的纯函数,
可以在业务代码中调用utils中的函数,然后将全局数据传递过来。
比如utils提供了一个diff方法,将传递过来的数据和全局数据中的project进行比对
//utils中的一个函数
function diff(source) {
//依赖全局数据store
let target = store.state.project
//对比source和target
}
我们可以将全局数据变为一个参数,在调用方传递过来
function diff(source, target){
//对比source和target
}
//调用方
diff(source, store.state.project)
通过这样的改写,将业务逻辑集中在业务代码中,这样utils不和业务耦合,假如后续要和其他数据进行diff,
只需要传参即可,不像之前,只能和全局数据中的某个属性进行对比,通过简单的改写增加了可复用性和可测试性。
纯函数
纯函数是最能预测的函数,它不玩魔术,就像数学中的函数一样,只要给定同样的输入,必然给出同样的输出,纯函数是可信赖的。
不管什么时候调用,只要输入不变,输出也永远不会变。
纯函数是所有函数式编程语言中使用的概念,这是一个非常重要的概念,我们应该尽可能的使用纯函数,
纯函数是可读性最好的函数。
满足以下条件的函数是纯函数。
1. 输入不变输出不变
比如一个加法函数,无论什么时候调用,无论调用多少次,只要输入确定了,你总能轻易的预料到输出。
function add(a,b){
return a + b
}
类似于数学中的函数 f(x) = 2*x + 1,x给定了f(x)也是确定的。
2. 不依赖外部,只依赖参数
纯函数的输入只来自参数,而不会使用全局变量或者外部变量
下面的函数就不是纯函数,依赖了外部变量
let sum = 0
function add(a,b){
return sum +a + b
}
3. 纯函数没有副作用
副作用包含很多,比如
- 修改参数
- 修改全局量的值或者外部变量
- 进行网络请求
- 进行dom的操作
- 进行定时器的调用
比如以下这个就不是纯函数,因为它修改了参数
function addItem(arr, item){
arr.push(item)
}
这样就是纯函数,或者深度克隆arr再push也可以
function addItem(arr, item){
return [...arr, item]
}
在实际编程中,可以将一些函数中的纯函数部分提取出来,尽量增加可预测的部分,减少不可预测的部分
总结
- 数据的从哪里来到哪里去,都要非常明确,符合预期,可以预测
- 尽量减少全局变量的使用,特别是在一些底层工具函数中
- 推荐使用纯函数编程,可以将非纯函数的纯函数部分提取出来
- 不要玩魔术,可预测,可控才是最重要的
前端同学欢迎添加vx好友交流:_hit757_
后续会继续总结如何提高复用性及如何解耦,欢迎专注~~