js的方法参数需要检查吗,检查原则是什么样的?

由于js为弱类型语言,所以当定义一个函数时,传入的参数有可能是任何类型的. 但是绝大部分时候,我们定义的函数都对各个参数有各种各样包括类型和其他方面的限制要求. 那么请问: 1.js中函数入参需要检查吗?还是靠调用者的自觉性? 2.如果需要检查,那么检查原则是什么? -比如函数内部只检查入参类型,其他方面靠调用者的自觉性? 3.当函数入参不符合方法要求时,应该如何处理? -直接return结束方法并且没有任何返回值? 例如写一个js函数g…
关注者
118
被浏览
6846

这是一个非常好的问题,值得谢邀。


回答之前,先澄清一个小点:

由于js为弱类型语言,所以当定义一个函数时,传入的参数有可能是任何类型的.

“弱类型“是一个相当模糊的概念。针对“传入参数可能是任何类型”,你想说的应该是“动态类型”,如 python 也是如此。参见《弱类型、强类型、动态类型、静态类型语言的区别是什么》。

大多数人提到“弱类型”时指的是允许隐式类型转换。参见《[ ] (空数组)== true》。有些隐式类型转换总体上是挺好用的,比如在 if 语句中任何类型都被转型为 boolean,像通常被认为是“强类型”的 python 也是如此。有些隐式转换就比较容易埋坑,比如《JavaScript 中应该用 "==" 还是 "==="》。我后面会谈到这一点对实施参数检查的影响。

1.js中函数入参需要检查吗?还是靠调用者的自觉性?

以语言创制者的角度,既然选择了动态类型,也就是放弃了静态类型检查。如果所有函数你都对入参进行运行时类型检查,就产生了大量琐碎的检查代码,不仅增加了编码负担,也干扰阅读代码。与其如此,还不如转而使用静态类型语言。这是为什么不少答案直接就写了 TypeScript。而在没有 TS 的时代,写 jsdoc 标注然后配合类型检查工具和IDE,也是类似的。

从调用者的角度来说,除了自觉之外,当然也还是可以利用 d.ts 类型定义文件或以前的 jsdoc 标注,配合工具和IDE来达到自动提示类型的。


2.如果需要检查,那么检查原则是什么? -比如函数内部只检查入参类型,其他方面靠调用者的自觉性?

不过,纵使 TypeScript 的类型系统已经超级强大,光类型检查也并不能涵盖所有参数检查需求。更不要说古老简陋的 jsdoc。举最简单的例子,TypeScript 目前仍然没有整数类型,而且我们经常有参数取值范围的要求,这类约束还是要通过代码实现。


此外,谈不上原则,但就实践来说,给外部使用的接口,应该对入参有比较完善的检查,而不对外暴露的内部实现部分,就不一定要那么严格。当然,内外是相对的。如果系统比较复杂,或者有多个分层,或者有多人合作,或者特别重要,或者涉及安全……都可能需要提高严格程度。


3.当函数入参不符合方法要求时,应该如何处理? -直接return结束方法并且没有任何返回值?

静默失败显然是糟糕的方式,比没检查也好不了多少。况且有些函数就是不需要返回值的,那如何区分是正常还是有问题?所以常规的方法就是扔异常。


既然谈到扔异常,我们就可以明白为什么在很多时候也不一定需要主动、显式的参数检查。因为如果参数不符合条件,通常情况下最终程序还是会跑不下去报异常的。

那么之所以有些情况我们还是倾向于要主动、显式的参数检查,主要是因为三种原因:

一、虽然最终会报异常,但报告得太晚,难以定位到错误的源头。

二、虽然有错,但不会报异常,只是得到了不符合预期的结果。

三、错误是偶发的,或仅在苛刻条件下出现。

强大的debugger有助于削弱第一条。

增加参数检查到底对第三条有多大的好处,其实是不确定的,从成本收益的角度,未必合算。

因此靠近业务的,经常变化的代码,出于成本考虑,选择“跑起来再说”的策略,也是可以理解的。而相对稳定和底层的代码,或者像各种框架,本身目的就包含了规范和约束,可以选择更多检查并勤抛异常的方式。

我要特别讲下第二条。前面讲的隐式类型转换往往就会造成这种情形。像 python 虽然是动态语言,但在常用运算上并不会做隐式类型转换,所以如果发生错误(比如数字加法,但被传入一个字符串和一个数字),是会扔异常的。

但是这并不意味着 JS 里就只有类型检查扔异常这一种方式。实际上在 JS 里更常见的方式是通过转型来确保参数类型符合(后续程序的)预期。比如接受字符串参数,并不因你传入非字符串而报错,而可以通过 String() 函数把非字符串转型成字符串,再进行下面的运算。再如接受数字,可以通过 +x 来转型;接受整数,可以通过 x | 0 来把非整数转型为 int32;如果超出值域范围,则取最接近的那个值……JS 的内建函数绝大多数都是按照这个方式运作的。因此如果是接近通用库的,宜多考虑是否可采纳此方式。

【从这个角度说,TypeScript 既没有提供生成运行时类型检查的能力(可能有一些第三方工具提供了),也没有在编译后的代码中加入自动转型,我认为可算一个不大不小的缺陷。】

【另外,配合代码规范和 eslint 工具,有助于减少隐式类型转换的坑,也可略为降低没有类型检查带来的风险。】


例如写一个js函数getFieldValue,传入一个对象object和想要获取的属性名称fieldName,格式为xxxx.xxxx.xxxx,然后方法返回该对象的该属性的值. 显然,该函数要求object参数必须是一个js对象,然后fieldName必须是一个符合js属性名称要求的字符串或者能转换为字符串的类型.
这种函数内部,是否需要检查这两个参数是否符合要求? 如果参数不符合要求,如object不是一个对象,无法进行取值操作,那么该返回什么?

以这个例子来说,常见的检查决策分支如下:

1. 传入的第一个参数不是对象(而是 primitive 值)

A. 扔 TypeError

B. 转型为对象

C. 放任不管,考虑后续程序逻辑,典型结果是传入 null/undefined 时会报异常,否则结果同 B。

2. 缺少第二个参数

A. 扔 TypeError

B. 视为空字符串

C. 放任不管,典型结果是被视为 "undefined"

3. 第二个参数的格式不符合属性标识符要求

A. 扔异常

B. 放任不管,典型结果是也没什么关系,因为用 [] 访问属性值并没有限制。


那么到底采用哪种?

其实很难用一个很简单的统一原则来确定。尤其像 getFieldValue 这样一个本来就展现了动态语言特性的工具方法,需要好好斟酌。

对于1,A(扔 TypeError)看上去是可行的,因为对于非对象,取属性没有多大意义。但是反过来说,对于非对象取属性反正是取不到,也不会出现什么大错误。而且本来在第二个参数的某个段就可能返回非对象,所以宜采用一致的策略,即 getFieldValue({x:0}, 'x.y') 与 getFieldValue(0, 'y') 应返回相同的值(如 undefined);getFieldValue({}, 'x.y') 与 getFieldValue(undefined, 'y') 也应返回相同的值(如 null)。因此如果前者都不会报异常,那么就不宜采用A或C。

对于2,C明显是坑(DOM API里就有这样的例子)。B虽然略好,但本来以空字符串为key就是罕见情况,最好不要再增加额外的值去映射到罕见情况。故我会选择A(抛 TypeError)。

对于3,选B。因为选A并没有什么特别的好处,反而还要考虑如何满足不符合标识符的属性怎么访问,徒增烦恼。


注意,以上分析和决策,其实出发点并不是参数检查,而是edge case的行为和预期。结果1大体上会用转型,2用检查并抛异常,3则不管。

以上。


最后顺便圈个票:荣誉会员评选 - 编程