1

综述

javascript 属于弱类型语言,参数的类型错误只能在运行期发现。当你需要 expose “非常健壮”的接口给外部,或者在调试较大项目的时候,你可能会怀念强类型语言的类型约束,或者 assert 一类东西。

正因为 js 没有类型约束,也没有 assert 这样的“契约型”断言工具,所以同一个人写出的 js 代码,健壮性常常是不稳定的,有时约束多,有时约束少,有时候返回 null,有时候抛异常,并且约束代码也常常不统一放在函数入口处。

本文尝试编写一种参数检查工具,期待能缓解类似问题。

参数检查

假设,我们需要给所有接口统一添加稳定的约束,以及约束破坏后统一的反馈行为(比如崩溃),除了语言原生支持(听说 Eiffel 有这个能力,有兴趣的可以 google 下),最直接的方法就是设计一个类似 assert 的参数检查函数 check,在每个函数入口处调用 check 检查参数,如果检查失败则执行既定的失败反馈。

如果所有的函数都这样编写,就可以保证所有函数严格执行约束,约束破坏后立刻停止运行,并打印相应的信息。

接口

我们很容易大致设想一个 check 接口的模样——

check.setCheckFailedCallback(function (e) {});

function test(a) {
    check(a).检查1(条件1).检查2(条件2)……
}

有几个细节需要讨论一下:

  • 上面的代码使用了链式调用,链式调用的必要性是很显然的——我们需要一种组合检查步骤的方式。为了实现链式调用,check 返回的是一个特殊的包装对象 Checker

  • 当参数 a 通过所有检查后,代码向下执行。如果有一个检查没有通过,此时需要执行一个反馈。由于外层代码可能存在 try 块,所以这里抛异常是不可靠的,或者说我们要想一个办法抛出一个“不可 catch”的异常。这里采用的最简单的办法,上层设置回调函数 checkFailedCallback,检查失败后自行处理结果,同时抛出一个异常。

  • check(a) 这种写法,实际上是做不到的。js 里没有宏,所以没有办法接受一个变量同时拿到变量的名称。如果要打印出检查失败的参数名,需要写成 check(a, 'a')。这种写法有点累赘,可能有更好的方案,我还在思考。

逻辑组合

刚才说到链式调用可以用来组合检查步骤,但是只有一种组合方式显然是不行的。因为检查步骤之间的关系可能有三种:与、或、非。我们要想办法使用同一的规则把三种关系表达清楚。

具体就不解释了,分享一下我的规则:

链式调用实现“与”

// a 是 number 型,并且大于 1 小于 3
check(a, 'a').is('number').gt(1).lt(3);

参数表实现“或”

// a 是 number 型,并且位于 [0, 1) || (1, 2] 区间上
check(a, 'a').is('number').within('[0, 1)', '(1, 2]');

注:由于参数表实现“或”,所以这里“或”的优先级永远比“与”高,如果需要“与”比“或”高,则需要一点技巧,具体见我这篇文章

not 属性实现“非”

// a 是字符串并且不符合正则表达式 /^[\w][\w\d]+$/
check(a, 'a').is('string').not.match(/^[\w][\w\d]+$/);

// a 是字符串并且不符合正则表达式 /^[\w][\w\d]+$/, 并且长度等于 10
check(a, 'a').is('string').not.match(/^[\w][\w\d]+$/).length().eq(10);

注:

  1. not 是一个特殊属性,会返回一个特殊对象 NotChecker,这个对象使用 try 执行原对象的检查方法,catch 到异常则认为检查通过。并且 NotChecker 的检查方法返回的是原对象而不是自己,所以 not.match 之后连接 length 时,已经不再 not 的作用范围。

  2. 由于德摩根定律的存在,not 后的参数表实际上在表达"与"的关系,比如:

    check(a, 'a').not.is('string', 'number').

    表示的是参数 a 既不为 string 也不为 number。

其他

另外,为了方便使用,还需要实现一些另外的接口,比如:

// a 包含属性 foo,大于 1 小于 3; 同时包含属性 bar, 大于 2 小于 4
check(a, 'a').has('foo').gt(1).lt(3).owner.has('bar').gt(2).lt(4);

注:

  1. 上面的代码中,has 是一个特殊方法,它检验参数中是否包含指定的属性(own property),如果包含,就返回一个包装该属性的 Checker,否则抛检查失败的异常。

  2. owner 是一个特殊属性,它返回包装上一层对象的 Checker 对象。所以我们可以在调用 has 检查属性之后,调用 owner“跳回去”继续检查上层对象。

代码

为了检验上面的想法,我实现了一个 js 库 param-check,代码位于:
https://github.com/yusangeng/param-check

因为只是一个语言切换是产生的 idea,所以目前这个库还不完善,实际能有多大意义还不好说,对性能和编程范式的影响还需要评估。


Y3G
458 声望14 粉丝

桌面软件、 web前端、 移动app混合开发、 安防行业技术