未加星标

守护 Javascript 中的函数参数

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二03 | 时间 2016 | 作者 红领巾 ] 0人收藏点击收藏

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

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

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( "[email protected]", "email" ); // ok byContract( "bla-bla", "email" ); // Exception!

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

byContract.is.email = validator.isEmail;

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

if ( env !== "dev" ) { byContract.isEnabled = false; } byContract 是一个很小的验证插件(压缩文件大约1KB大小) ,你可以在你的Javascript代码中使用它从

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

分页:12
转载请注明
本文标题:守护 Javascript 中的函数参数
本站链接:http://www.codesec.net/view/482452.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(34)