剑客
关注科技互联网

守护 Javascript 中的函数参数

作为开发者,我们花费许多时间来调试,尤其是在发现问题来源方面。开发工具指导我们追踪调用栈,但是追踪过程仍然相当耗时,尤其在遇到级联异步调用的时候。这一问题在很早以前就被发现了。

假设我们有一个从不同文档结构中搜索包含指定字符串的元素的函数。我们使用以下看起来合法的调用:

grep( "substring", tree );

但是我们并没有得到期望的结果。按照以往的经验,我们会花费一些时间来检查给定的树形文档结构,时间有可能会很长。然后我们很可能会做其他的检查,但是在最终,我们会从函数代码中发现传入的参数顺序反了。这样看来的话,我们只要关注函数的参数,就不会发生上面的错误。

function grep( tree, substring ){

  if ( !( tree instanceof Tree ) ) {

    throw TypeError( "Invalid tree parameter" );

  }

  if ( typeof substring !== "string" ) {

    throw TypeError( "Invalid substring parameter" );

  }

  //...

}

这种验证方式是 Design by Contract approach 
的一部分。它在软件组成部分中列出了需要验证的前置条件和后置条件。在以上示例中,我们必须测试函数输入参数符合指定的格式(比较第一个参数符合树文档的类型,第二个参数符合字符串类型)同时我们建议检查函数输出类型是否是一个字符串。

但是,Javascript目前为止还没有其他语言那样内置的功能作为函数入口和结束处的验证。对于一个示例,PHP语言有类型提示:

<?php

function grep( Tree $tree, string $substring ): string {}

TypeScript 有严格类型:

function grep( tree: Tree, substring: string ): string {}

此外,它还支持高级类型(联合类型,可选类型,交叉类型,泛型等等):

function normalize( numberLike: number | string, modifier?: boolean ): string {}

根据在ES规范中提出来得特性,今后会有一个叫做 Guards
的功能,它建议使用下面的语法:

function grep( tree:: Tree, substring:: String ):: String {}

目前为止在Javascript中,我们必须使用外部库或者可转换的编译器来解决这一问题。但是,可用的资源较少。最老的库是 Cerny.js 
。它类似于DbC(数据库计算机),强大且灵活:

var NewMath = {};

(function() {

    var check = CERNY.check;

    var pre = CERNY.pre;

    var method = CERNY.method;

    // The new division

    function divide(a,b) {

      return a / b;

    }

    method(NewMath, "divide", divide);

    // The precondition for a division

    pre(divide, function(a,b) {

       check(b !== 0, "b may not be 0");

    });

})();

但是对我而言,它读起来很复杂。我更喜欢使用简洁干净的方式校验前提条件/后置条件即可。 Contractual 
提供的语法很符合我的要求:

function divide ( a, b ) {

  pre:

    typeof a === "number";

    typeof b === "number";

    b !== 0, "May not divide by zero";

  main:

    return a / b;

  post:

    __result < a;

}

alert(divide(10, 0));

除了不是Javascript之外,看起来都很不错。如果你需要使用的话,必须用 Contractual或者
Babel Contracts
把源代码编译成Javascript。我不反对跨语言编译器,但是如果让我选择的话,我宁愿用 TypeScript。

但是回到Javascript,不知道你有没有发现,除了相关库和框架外,我们在注释函数和类的时候一直在用 JSDoc
描述函数入口和返回处的格式对比。如果文档注释可以用来验证格式的话就太好了。正如你所理解的,它离不开编译器。但是,我们可以使用依赖于Jascript文档表达式的库。幸运的是, byContract
就是这样的库。 byContract
的语法看起来像这样:

/**
 * @param {number|string} sum
 * @param {Object.<string, string>} dictionary
 * @param {function} transformer
 * @returns {HTMLElement}
 */
function makeTotalElement( sum, dictionary, transformer ) {
  // Test if the contract is respected at entry point
  byContract( arguments, [ "number|string", "Object.<string, string>", "function" ] );
  // ..
  var res = document.createElement( "div" );
  // ..
  // Test if the contract is respected at exit point
  return byContract( res, "HTMLElement" );
}
// Test it
var el1 = makeTotalElement( 100, { foo: "foo" }, function(){}); // ok
var el2 = makeTotalElement( 100, { foo: 100 }, function(){}); // exception

如你所见,我们可以从文档注释处复制/粘贴指定的类型到
byContract 然后进行对比,就这么简单。下面我们更仔细地检查以下

byContract 可以被当做UMD模块(AMD或者CommonJS)或者全局变量来访问。我们可以把值
/Javascript 文档表达式作为一对参数传给
byContract

byContract( value, "JSDOC-EXPRESSION" );

或者值列表对应文档表达式列表作为一对参数也可以:

byContract( [ value, value ], [ "JSDOC-EXPRESSION", "JSDOC-EXPRESSION" ] );


byContract 会检测传入的值

,如果和对应的
JSDoc 表达式格式不一致,就会抛出

带有像
` 传入的值违反类型NaN`信息的
byContract.Exception 异常

在最简单的案例中,byContract用来验证如 `array`, `string`, `undefined`, `boolean`, `function`, `nan`, `null`, `number`, `object`, `regexp`之类的
原型类型:

byContract( true, "boolean" );

当我们需要允许输入值在一个指定类型列表中的时候,可以使用 type union

byContract( 100, "string|number|boolean" );

一个函数可以有必填的参数,也可以有可选参数。默认情况下,参数在和原型类型做对比的时候是必填的。但是用’=’修饰符我们就可以设置成可选类型。所以
byContract 处理如

`number=` 这样的表达式时候,会转为
`number|undefined`

function foo( bar, baz ) {
  byContract( arguments, [ "number=", "string=" ] );
}

下面是Js文档中 nullable/non-nullable types
(可空/不可空类型):

byContract( 42, "?number" ); // a number or null.
byContract( 42, "!number" ); // a number, but never null.

当然,我们可以用接口来做比较。这样我们就可以引用作用域范围内任何可用的对象,包括Javascript内置接口:

var instance = new Date();
byContract( instance, "Date" );
byContract( view, "Backbone.NativeView" );
byContract( e, "Event" );

对于数组和对象,我们可以有选择性地验证其内容。比如可以验证所有数组的值必须是数字或者所有的对象的键和值是字符串类型:

byContract( [ 1, 1 ], "Array.<number>" );
byContract( { foo: "foo", bar: "bar" }, "Object.<string, string>" );

以上的验证对线性数据结构有用,其他情况下就不起作用了。所以同样的,我们可以创建一个 type definition
(类型定义)来描述对象的内容(参考byContract类型定义)然后在后面作为一个类型引用它即可。

byContract.typedef( "Hero", {
  hasSuperhumanStrength: "boolean",
  hasWaterbreathing: "boolean"
});
var superman = {
  hasSuperhumanStrength: true,
  hasWaterbreathing: false
};
byContract( superman, "Hero" );

这个示例定义了一个’Hero’类型来表示一个对象/命名空间,必须有boolean类型的 `hasSuperhumanStrength`和`hasWaterbreathing`
属性。

所有的方法都通过类型验证传入的值,但是不变的量(常量)呢?我们可以用一个自定义类型来包装类型约束。比如说检测字符串是不是一个邮件地址类型,我们可以增加这样的验证:

byContract.is.email = function( val ){
  var re = /^(([^<>()[/]//.,;:/s@"]+(/.[^<>()[/]//.,;:/s@"]+)*)|(".+"))@((/[[0-9]{1,3}/.[0-9]{1,3}/.[0-9]{1,3}/.[0-9]{1,3}])|(([a-zA-Z/-0-9]+/.)+[a-zA-Z]{2,}))$/;
  return re.test( val );
}
byContract( "john.snow@got.com", "email" ); // ok
byContract( "bla-bla", "email" ); // Exception!

事实上,你很可能不要用事件来写验证函数,而是用外部库(类似 validator
)代替:

byContract.is.email = validator.isEmail;

验证逻辑取决于开发环境。使用
byContract, 我们可以用全局触发器来禁用验证逻辑

if ( env !== "dev" ) {
  byContract.isEnabled = false;
}


byContract 是一个很小的验证插件(压缩文件大约1KB大小)

,你可以在你的Javascript代码中使用它从而得到对比编程设计模式的好处。

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址