身為開發者,我們花費許多時間來除錯,尤其是在發現問題來源方面。開發工具指導我們追蹤呼叫棧,但是追蹤過程仍然相當耗時,尤其在遇到級聯非同步呼叫的時候。這一問題在很早以前就被發現了。
假設我們有一個從不同文件結構中搜尋包含指定字串的元素的函數。我們使用以下看起來合法的呼叫:
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 pision function pide(a,b) { return a / b; } method(NewMath, "pide", pide); // The precondition for a pision pre(pide, function(a,b) { check(b !== 0, "b may not be 0"); }); })();
但是對我而言,它讀起來很複雜。我比較喜歡用簡潔乾淨的方式校驗前提條件/後置條件即可。 Contractual 提供的語法很符合我的要求:
function pide ( a, b ) { pre: typeof a === "number"; typeof b === "number"; b !== 0, "May not pide by zero"; main: return a / b; post: __result < a; } alert(pide(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( "p" ); // .. // 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代码中使用它从而得到对比编程设计模式的好处。
以上就是守护 Javascript 中的函数参数的内容,更多相关内容请关注PHP中文网(www.php.cn)!