此篇我将会从函数的命名、注释和鲁棒性方面,阐述如何编写高质量的函数。前端
PS:这一篇相对上一篇文章要简单好懂多了。node
写第二篇以前,先说个事情。针对前面我写的 如何编写高质量的函数 -- 敲山震虎篇 文章的评论区,小伙伴提出的一些问题,我也所有都看了,特此写了答疑篇。react
答疑篇放在了 issues
上,点击下面连接便可 TP
:git
如何编写高质量的函数 -- 敲山震虎的答疑篇github
对个人回答有什么疑问的话,能够 issues
讨论,这篇文章就不放在掘金了。web
PS: 提一下一些公众号(掘金,奇舞周刊等)转载的状况,因为文章一开始有错误,而后公众号又没有同步,而后我又不能帮大家改,因而点开公众号文章,看着那些还在的错误,却无能为力的心情,内心默默
ob
: 感谢地主们的转载,那就这样吧😂。面试
好,胡诌很少说,直接开始吧。express
从文章开头的图片能够知道,命名和缓存是计算机科学中的两大难题。npm
而今天要说的是函数命名,虽然函数命名涉及到的范围较窄,但思想都是同样的,彻底 能够借鉴到其余的形式中。编程
我阅读过代码大全的变量一章,也针对性的阅读过一些源码,好比 lodash
, ramda
这些函数工具库。如今根据我我的的一些感悟,总结了一些我我的认为能帮助你完全解决命名这个事情的 best practice
。
PS: 虽然变量命名这个没有所谓
best practice
,可是对于前端的函数命名来讲,我我的认为是能够有一套完善的best pratice
的, 听我娓娓道来。
存在什么问题呢?要我说啊,那些业界标准,好比驼峰,首字母大写的类和构造函数,下划线,$
等都不是瓶颈。真正的瓶颈是一些你察觉不到的,或者察觉到可是无能无力的细节。好比:
下面进行简明扼要的分析。
PS: 关于驼峰等耳熟能详的业界标准我就再也不提了。
为何一开始要说这个呢,由于我认为这是目前命名中,存在的最大的问题。英语水平好的哥们没多少,不少人不能掌握英语命名的那一套语法规则,说白了就是你英语水平达不到能像外国人那样能写出符合英语 style
的名字。
理由就三个:
最大的困难就是 不会 。
我举个例子,都知道 react
的生命周期吧,以下:
componentDidMount
componentWillReceiveProps
shouldComponentUpdate
不少人都会有疑问,为何用 did
,为何用 will
。行吧,记住就完事了,而后过了一段时间,面试的被问到了,而后内心 ob:
是 componentMounted
仍是啥来...
多么鲜活的例子,嗯,继续往下读吧,后面有惊(极)为(为)天(惊)人(悚)的答案。
黑人脸,怎么让啊?
老哥,多翻翻高中或者初中的英语语法知识吧。好比我举个最简单的例子,你就知道了。
componentDidMount
是react
等生命周期的钩子,可是为何要这样命名?
componentWillReceiveProps
为何要这样命名?
答案就在下图:
注意上图中的 did
表明通常过去时,will
表明通常未来时。
而后咱们百科通常过去式和通常未来时,而后如图所示:
通常过去时:
通常未来时
看上图的红箭头,did
表示通常过去时,时是指动做发生的时间,用在这里,突出了钩子的含义,一旦 mount
成功就执行此函数。是否是瞬间明白了,好了,will
同理。
啥也别说了,赶忙去好好看看初高中英语语法吧。
这是个小特性,好比 shouldComponentUpdate
, 为何 should
放在最前面。
由于这个函数返回的值是布尔值。那么咱们能够理解为这是个问句,经过问句的形式来告诉咱们,这里具备不肯定性,须要根据返回值来判断是否更新。
关于问句的英语语法,老铁们还要多翻翻语法书啊(泪)。
谷歌翻译这个就不说了,你们都会
这是一个神器,用来搜索各类开源项目中的变量命名,来给你提供参考。
对应名字的 VSCODE
插件也有,具体怎么用,小伙伴自行去了解吧。
可能你给一个函数命名了,其余人看到这个函数时,一脸懵逼,彻底不知道这命名的啥子东西哟,只能靠猜。
好比我从 ramda
源码中找了一个函数,代码以下:
var forEachObjIndexed = _curry2(function forEachObjIndexed(fn, obj) {
var keyList = keys(obj);
var idx = 0;
while (idx < keyList.length) {
var key = keyList[idx];
fn(obj[key], key, obj);
idx += 1;
}
return obj;
});
export default forEachObjIndexed;
复制代码
这个函数叫 forEachObjIndexed
,看到这个命名,是否是一脸懵逼,反正我第一次看到是懵逼了,这啥子嘛,什么鬼东西,而后我就去趴了下源码,从源码里面的函数注释中才知道是干啥的,函数注释以下图:
看到没,多详细,固然,这是为了输出文档用的,可是给了咱们一块儿很是好的解决方法。那就是:
若是你实在想不到如何去命名,或者你本身已经知道这个命名很烂了,好比太长,好比很难理解,那这个时候你就别挣扎了。写一个你以为还 ok
的命名,而后把剩下的时间留给你写注释吧。好比 forEachObjIndexed
的第一部分的注释就是对整个函数的总体介绍和说明。
若是你的函数命名很烂,那这个时候,函数的总体介绍和说明就显得很是重要了。这部分你必定要作好,英语水平很差的话,那就老老实实写中文。这部分作好了,这个函数你哪怕用两行文字命名的,或者用了火星文命名的,也不要紧,问题不大。
最后说一说命名的分类,这是我我的的一些见解。
为何我会说函数命名的分类呢,是由于咱们常常会看到函数会这样命名(源码中很广泛)。好比:
- $xxx()
- _xxx()
复制代码
这种带各类前缀的函数名,看起来并很差看。这样命名,在我我的看起来是很是别扭的,可是为何要有这种命名呢,其实这是前端的无奈之举。
核心缘由就是
JS
语言不支持私有变量,致使只能使用_
或者$
来保证相应的对外不可见,经过治标不治本的方法来解决这个问题。
因此我把前端的函数命名分为两大类,以下:
第一类:不想暴露给外部访问的函数(好比只给内部使用)
第二类:暴露给外部访问的函数(各类功能方法)
我我的目前的观点,大体也就这两大类了。
PS:这里我没把 Symbol 初始化的函数命名考虑在内,好比以下代码:
const ADD = Symbol('add')
[ADD](a, b) {
console.log('a + b')
}
复制代码
关于 Symbol
的用法,你们能够自行了解,这种特例我就不考虑在内了。
PS:关于这个无奈之举,在了解的更多的时候,会发如今前端,并无什么方法(设计模式也好,
hack
方法也好)能绝对的解决上面的问题,因此有时候你不得不使用_
等,由于当都不能解决这个问题的时候,那越简单的方式越受欢迎,这就是现实。
总结一下最佳实践:
多学习初中高中英语语法,开源项目中的函数命名没有那么难理解,经过语法的学习和借助工具,函数命名基本能够解决,若是遇到没法清晰描述所写函数的目的的命名时,请务必给函数写上良好注释,无论函数名字有多长多难懂,只要有良好的注释,那就是能够接受的一件事情。毕竟你也不想命名的这么难懂啊,可是能力有限,那就用汉语作好注释吧,这样的效果也是杠杠的。
如何经过良好的函数命名来提供函数的质量,我也说的差很少了,答案都在文字中,如何去借助工具,如何去理解英语中的命名语法,如何去经过多维度来增长命名含义的准确性和可读性。简单聊了下目前前端界函数命名的分类,你们自行体会和运用吧。
PS:一些都知道的点我就不说了,好比动词+名词,名词+动词,驼峰等,清晰描述函数的目的,这都不是痛点,痛点我都说了,最佳实践也说了。
咱们来谈函数的注释,注释一方面提升了可读性,另外一方面也能够经过注释去作一些其它的事情,好比生成在线文档。一个高质量的函数,注释少不了的,可是这并不表明全部的函数都须要注释。富有富的活法,穷有穷的潇洒,重要或者说复杂的函数,那就给个好注释,简单或者不重要的函数,能够不给注释或者给一个简单的注释。说空话没有意义,咱们来看看目前函数的注释都有哪几种方式。
PS:这里要注意我上面的用词,若是你以为这个函数命名很烂,那你就应该给一个好的注释。
就像大学里面写论文以前,都要阅读不少文献资料,咱们也同样,咱们来看看几个有名的 npm
包是怎么玩注释的。
从图中,咱们看到 egg.js
的入口文件的注释状况,暂且不去判断这是否是一种 doc
工具的注释规则(不要在乎细节)。咱们就看一下其注释特色,是否是发现和你脑海中的注释风格又有区别了呢。这种入口文件的注释特色,简单整洁,这种思想是否是要吸取一波,之后你作开源项目的时候,这些思想均可以带给你灵感。
继续看下图:
这是一个被抽象出来的基类,展现了做者 [Yiyu He
] 当时写这个类的时候,其注释的风格。从这张图中,咱们能学到什么呢?有如下几点:
第一点:构造函数的注释规则,表达式语句的注释规则。
第二点:注释的取舍,有一些变量能够不用注释,有些要注释,不要有那种要注释就要所有注释的思想。
再看两张有趣的图片:
看上面两张图的箭头,指向的都是同一个做者 [fengmk2
] , 咱们看他的函数注释规则。体会一下不一样,想一想为何第一张图没有空格,第二种有空格,还有对返回的 this
的注释,好比不少人习惯将 this
直接注释成 Object
类型。
说到函数注释,就不能不说到 lodash.js
。可是写到这,我发现这块要是加上去的话,第二篇的文字就又超了,那这里就再也不说了,你们本身看看源码分析一下吧(这操做真香)。
有人说注释要很规范,方便给别人,好比用 jsdoc
等 。这里我我的的见解是这样的,对一些不须要开源的 web
项目,没有必要用 jsdoc
, 理由以下:
jsdoc
规则来jsdoc
有入侵性,文档规则须要写在代码中。这里我认为若是要写注释说明手册,对于大型项目,我推荐使用 apidoc
, 由于 apidoc
入侵性不强,不要求把规则写在代码中,你能够把全部规则写到一个文件中。具体使用方法,我就不说了,自行搜索相关资料。
可是通常小项目,没有必要单独写一份 api
文档。若是是开源的大型项目,那你要考虑的事情就更多了,首先须要有开源的官方网站,你会看到网上的一些开源项目官网好像很酷,其实这个世界上不缺的就是轮子,你也能够很快的作出这样的网站,下面咱们来看看是如何作到的。
首先咱们看一下 taro
源码,会发现以下图:
这里就是生成一个静态网站的秘密,执行这个 npm run docs
就能够了。用到的是 docusaurus
包,不知道的能够自行搜索。
而后这里你看下图:
从图中能够知道,文档的内容,来源于 docs
目录,里面都是 md
文件,开源项目的文档说明都在这里。
固然也有把对应的文档直接放到对应的代码目录下的,好比 ant-design
以下图:
就是直接把文档放在组件目录下了。
从这里,咱们能够知道,目前流行的开源项目的官方网站是怎么实现的,以及文档该怎么写。你能够说这和函数注释没有什么关系,可是想一想好像又有点关系,这里就很少言了,本身体会吧。
下面说说我本人对函数注释(只针对函数注释)的一些我的风格或者意见。
VSCode
关于注释的几个工具Better Comments
给注释上色Document This
自动生成注释TODO Highlight
高亮 TODO
,并能够搜寻全部 TODO
具体用法就不说了,下面是一张演示图,自行去研究吧:
我我的的观点是这样的:
不影响可读性,复杂度低,对外界没有过分干涉的函数能够不写注释。
函数内,表达式语句的注释能够简单点,我通常以下图所示,//
后面加简要说明。
function add(a, b) {
// sum ....
let sum = a + b
}
复制代码
function say() {
// TODO: 编写 say 具体内容
console.log('say')
}
复制代码
function fix() {
// FIXME: 删除 console.log方法
console.log('fix')
}
复制代码
通常我分为普通函数和构造函数。
普通函数注释:
/** * add * @param {Number} a - 数字 * @param {Number} b - 数字 * @returns {Number} result - 两个整数之和 */
function add(a, b) {
// FIXME: 这里要对 a, b 参数进行类型判断
let result = a + b
return (result)
}
复制代码
构造函数注释:
class Kun {
/** * @constructor * @param {Object} opt - 配置对象 */
constructor(opt = {}) {
// 语句注释
this.config = opt
}
}
复制代码
从开源项目的代码中能够发现,注释的风格多种多样,有时候我本身不一样项目的注释风格也有点差异,可是我会尽量的去平衡注释和不注释,上面注释的基本原则仍是要遵照的。
可是怎么说呢,注释这块不存在银弹。
你们都听过防护性编程对吧,let it crash
。 咱们看一个段子,下图:
看最后一句,测试测了那么多场景,最后酒吧仍是炸了(哈哈哈哈哈哈怎么回事?)。
因此,咱们能够看出,防护性编程的核心就是:
把全部可能会出现的异常都考虑到,而且作相应处理。
可是我我的认为,防护性的程度要看其重要的程度。通常来讲,不可能去处理全部状况的,可是提升代码鲁棒性的有效途径就是进行防护性的编程。
我接手过一个需求,重写(彻底重构)苏宁易购微信小程序的登陆注册绑定的功能,并将代码同步到苏宁其余小程序(和其余小程序的开发进行代码交接并协助 coder
平稳完成版本过渡)。
这个项目重要性不言而喻,因为用户的基数很大,风险程度很高,须要考虑不少场景,好比:
支不支持线上版本回退,也就是须要有前端的 AB
版本方案(线上有任何问题,能够快速切到旧登陆方案)
须要有各类验证:图形验证码、短信验证码、ip
、人机、设备指纹、风控、各类异常处理、异常埋点上报等。
代码层面的考虑:经过代码优化,缩短总的响应时间,提升用户体验。
如何确保单个节点出问题,不会影响整个登陆流程。
你会发现,须要考虑的点不少,如何去合理的完成这个需求仍是比较有难度的。
PS: 关于第四点的如何确保单个节点出问题,不会影响整个登陆流程,文末有答案。
下面我就关于函数鲁棒性,说一说我我的的一些见解。
在 ES6
的到来后,函数的入参写法已经获得了质的提升和优化。看下面代码
function print(obj = {}) {
console.log('name', obj.name)
console.log('age', obj.age)
}
复制代码
print
函数,入参是 obj
经过 obj = {}
来给入参设置默认的参数值,从而提升入参的鲁棒性。
可是你会发现,若是入参的默认值是 {}
,那函数里面的 obj.name
就会是 undefined
,这也不够鲁棒,因此下面就要说说函数内表达式语句的鲁棒性了。
继续上个例子:
function print(obj = {}) {
console.log('name:', obj.name || '未知姓名')
console.log('age:', obj.age || '未知年龄')
}
复制代码
若是这样的话,那你会发现表达式语句就变得比较鲁棒性了,可是还不够好,这样写不够抽象,咱们换种方式稍微把表达式语句给解耦一下,代码以下:
function print(obj = {}) {
const { name = '未知姓名', age = '未知年龄' } = obj
console.log('name:', name)
console.log('age:', age)
}
复制代码
这样的话,看起来就感受好多了,其实还能够再抽象,好比吧 console.log
封装成 log
函数,经过调用 log(name)
,就能完成 console.log('name:', name)
的功能, 这里就再也不说了,自行研究吧。
上面的那几个点,我我的认为能够归类到一个方案层面去,那就是:
防患于未然,从一开始就不要让异常发生。
可是不要忘了,总会有万一,还有一个方案层面要去考虑,那就是:
异常仍是出现了,该怎么去处理出现的异常。
下面两个层面已经肯定了,那如何去更好的处理各类异常,提升函数的鲁棒性呢,我我的有如下几点见解。
try/catch
的原理有不少人不清楚怎么去用 try/catch
。这里我来按照我我的的看法,来推一下其原理吧,首先 js
是运行在 node.js
提供的运行时环境中的,而 node.js
是用 C++
写的。C++
是有本身的异常处理机制的,也是有 try/catch
的 。那就说明 js
的 try/catch
的底层实现是直接经过桥,调用 C++
的 try/catch
。
而 C++
的 try/catch
具备的一些特性,你们能够自行去了解一下,好比其中就有一个特性是这样的:
try/catch
只能捕捉当前线程的异常。
因此这也就很好的解释了,为何 JS
的 try/catch
只能捕捉到同步的异常,而对于异步的异常就无能为力了(由于异步是放在另外一个线程中执行的)。
这里是个人推导,不表明确切答案。
这里我推荐一篇博客:
有兴趣的能够看看。
这里有几个方法:
第一个方法:若是是同步的操做,能够用
throw
来传递异常
看下面代码:
try {
throw new Error('hello godkun, i am an Error ')
console.log('throw 以后的处代码不执行')
} catch (e) {
console.log(e.message)
}
复制代码
首先咱们要知道 throw
是以同步的方式传递异常的,也就是 throw
要和使用 throw
传递错误的函数拥有相同的上下文环境。
若是上下文环境中,都没有使用 try/catch
的话,可是又 throw
了异常,那么程序大几率会崩溃。
若是是 nodejs
,这个时候就应该再加一个进程级的 uncaughtException
来捕捉这种没有被捕捉的异常。一般还会加上 unhandledRejection
的异常处理。
第二个方法:若是是异步的操做
有三种方式:
使用callback
,好比 nodejs
的 error first
风格
对于复杂的状况可使用基于 Event
的方式来作,调用者来监听对象的 error
事件
使用 promise
和 async/await
来捕捉异常
如今的问题是,怎么去选择哪一个方式呢?有这几个原则:
简单的场景,直接使用 promise
和 async/await
来捕捉异常
复杂的场景,好比可能会产生多个错误,这个时候最好用 Event
的方式
第三个方法:若是既有异步操做又有同步操做
怎么办呢?这个时候,我我的认为,最好的方式,就是使用最新的语法:async/await
来结合 promise
和 try/catch
来完成对既有同步操做又有异步操做的异常捕捉。
第四个方法:处理异常的一些抽象和封装
对处理异常的函数进行抽象和封装也是提升函数质量的一个途径。如何对处理异常进行抽象和封装呢?有几个方式能够搞定它:
第一个方式:对 nodejs
来讲,一般将异常处理封装成中间件,好比基于 express/koa
的异常中间件,一般状况下,处理异常的中间件要做为最后一个中间件加载,目的是为了捕获以前的全部中间件可能出现的错误
第二个方式:对前端或者 nodejs
来讲,能够将异常处理封装成模块,相似 Event
的那种。
第三种方式:使用装饰器模式,对函数装饰异常处理模块,好比经过装饰器对当前函数包裹一层 try/catch
。
第四种方式:使用函数式编程中的函子( Monad
)等来对异常处理进行统一包裹,这里 的 Monad
和 try/catch
在表现上都至关于一个容器,这是一个至关强大的方法。从 Monad
能够扩展出不少异常处理的黑科技,可是我建议慎用,由于不是全部人都能看懂的,要考虑团队的总体技术能力,固然一我的的话,那就随便嗨了。
合理的处理异常,须要能肯定使用哪种方式来处理异常,我大体也说了具体的选择状况,这里我推荐一篇博客:
目前我见到的讲的最全的处理异常的博客,但我这里说的都是我认为比较重要的主要的点,二者仍是有明显区别的,你们融合一下吸取吸取吧。
好比登陆流程须要4个安全验证,按照一般的写法,其中一个挂了,那就所有挂了,可是这不够鲁棒性,如何去解决这个问题呢。这里我提一下,可能不少人都不会注意到。
主要方案就使用将 promise
的链式写法换一种方式写,之前的写法是这样的:
伪代码以下:
auth().then(getIP).then(getToken).then(autoLogin).then(xxx).catch(function(){})
复制代码
通过鲁棒调整后,能够改为以下写法:
伪代码以下:
auth().catch(goAuthErrorHandle).then(getIP).catch(goIPErrorHandle).then(function(r){})
复制代码
通过微调后的代码,直接让登陆流程的鲁棒性提高了不少,就算出错也能够经过错误处理后,继续传递到下一个方法中。
我我的认为对异常的处理,仍是要根据实际状况来分析的。大概有如下几点见解:
要考虑项目可维护性,团队技术水平
我曾在一个需求中,使用了诸如函子等较为抽象的处理异常的方法,虽然秀了一把(做死),结果致使,后续这块的需求改动,还得我本身来。嗯,就是这么刺激,由于同事不熟悉函数式编程。
要提早预估好项目的复杂性和重要性。
好比在作一个比较重要的业务时,一开始没有想到异常处理须要这么细节,并且通常初版的时候,需求并无涉及到不少异常状况处理,可是后续需求迭代优化的时候,发现异常状况处理是如此的多,直接致使须要重写异常处理相关的代码。
因此之后在项目评估的时候,要学会尝试根据项目的重要性,来提早预留好坑位。
这也算是一种面对将来的编程模式。
关于函数的鲁棒性(防护性编程),我介绍了不少东西,基本上是前端或者是 nodejs
处理异常的常规方法吧。处理异常不是一个简单的活,工做中还得结合业务去肯定合适的异常处理方式,总之,多多实践出真知吧。
Jest
,按照官网文档来,本着函数式编程的思想,问题不大。能够这么说,读了这篇文章,你的 git
和 gerrit
就没有什么问题。
关于如何阅读 npm
包源码的故事。
最近刚写的一篇文章,发现没什么阅读量,可是我以为我写的很好啊,这篇文章绝对会对绝大多数前端工程师有所启发和帮助。
宣传一波,想启发更多还在路上的前端小伙伴:
加上本篇,这个系列已经写了两篇了,我计划三篇搞定,可是看这状况,后面都是硬货,很难 7000
字之内搞定呀,后续几篇搞定,我再 ob
一下吧。
小伙伴们能够关注个人掘金博客或者 github
来获取后续的系列文章更新通知。
掘金系列技术文章在 github
上汇总以下,以为不错的话,点个 star 鼓励一下,我将 使命必达 ,持续输出精品文章。
我是源码终结者,欢迎技术交流。
也能够进 前端狂想录群 你们一块儿头脑风暴。有想加的,由于人满了,能够先加我好友,我来邀请你进群。
最后:尊重原创,转载请注明出处哈😋