closure-library 第二章 Closure javascript注解

来源:互联网 发布:js面向对象小例子 编辑:程序博客网 时间:2024/04/29 11:01

Closure Library原始代码都有注释,其中一些都有特殊的格式,并被Cloure Compiler处理。理解这些注解对阅读Closure代码有很大帮助,本书将有这些例子。本章介绍的JSDoc标记和类型表达式都可以在Clsure代码中找到。google在http://code.google.com/closure/compiler/docs/js-for-compiler.html.维护这两个主题。

JSDoc 标记

大多数开发者对Closure Library源代码的第一感觉是很冗长,特别是加入类型信息后。如下面的关于base.js中的goog.bind函数声明:

[javascript] view plaincopy
  1. /** 
  2.  * @param {Function} fn A function to partially apply. 
  3.  * @param {Object|undefined} selfObj Specifies the object which |this| should 
  4.  *     point to when the function is run. If the value is null or undefined, 
  5.  *     it will default to the global object. 
  6.  * @param {...*} var_args Additional arguments that are partially 
  7.  *     applied to the function. 
  8.  * @return {!Function} A partially-applied form of the function bind() was 
  9.  *     invoked as a method of. 
  10.  */  
  11. goog.bind = function(fn, selfObj, var_args) {  
  12.   // implementation of goog.bind()  
  13. };  
每一个参数和返回类型都类型和描述写明。java开发者可能会马上觉得这和javadoc语法很相似,实际上Closure代码是用JSDoc文档化的。它是基于javados的标准的专门针对javascript的语法。java和javascript都支持下面这两种注释格式:
[html] view plaincopy
  1. // The double slash is used to comment out everything to the end of the line.  
  2. var meaningOfLife = 42; // It is often used as a partial line comment like this.  
  3. /* The slash followed by an asterisk is used to comment out everything between  
  4.  * it and the asterisk followed by a slash at the end of this paragraph.  
  5.  * Although only the asterisks after the opening slash and before the closing  
  6.  * slash are required, it is customary to include an asterisk at the start of  
  7.  * each line within a multi-line comment, as illustrated by this example. */  

在Java中,包含javadoc的注释必须用/***/声明。这通常叫着文档注释。像javadoc一样,javascript也用/**  和/*包含JSDoc注释。许多编辑器支持对/*..*/和/**...*/用不同方式显示这两种注释。这会底帮助将普通注释块放在文档注释块的错误。

就像你妈妈常常告诉你的一样,它真正依赖其中的内容,在JSDoc中,文档注释中真正依赖的是JSDoc标记。JSDOC标记@character,紧接是可识别的JSDoc标记名,具体值可以是任何东西,直到下一个标记开始。

所有本意中的标记都是块标记,必须从一行开始,下面的块就不正确:

[html] view plaincopy
  1. /**  
  2.  * NEGATIVE EXAMPLE: This is NOT an appropriate use of block tags.  
  3.  * @constructor @extends {goog.Disposable} @private  
  4.  */  
正确写法如下:
[html] view plaincopy
  1. /**   
  2.  * This is an appropriate use of block tags.  
  3.  * @constructor  
  4.  * @extends {goog.Disposable}  
  5.  * @private  
  6.  */  

JSDoc也支持内联标记,如{@code}和{@link}.内联块可以简单地出现在花括号中。就像javadoc一样,内联标记可以出现在注释的任何地方。这两个标记意义和javadoc中的一样,因此本章将不会对其进行介绍。

回到上面的例子,goog.bind()的注释包含两个常见的JSDoc标记:@param和@return。和javadoc一样,他们用来标注参数和返回值。但是,在Closure编译器中,JSDoc不只是用来作注释用。

在Java中,文档注释仅仅是任文档用,Java编译器在编译时完全忽略他们。Java编译器根据方法签名取得类型信息。假如Java程序员想要对Java代码进行注解,就得使用Java annotations,它是Java1.5新加的一个特性。用来在源码中添加元数据,并且编译器和其它工具可以使用它们。

在Javascript中,这完全不同,它在语言级别没有支持类型信息和注解。实际,Javascript是解释性语言,因此它没有编译器来处理这些注解。但这些信息对静态检查程序的正确性比较有用。

这就是Closure编译器出现的地方,Jsdoc有双重着用,即对开发者提供文档,也对编译器提供代码注解。在goog.bind()的@param和@return标签中,在花括号中都包括类型信息。花括号专门用来包含类型信息,这将在下一节中详细介绍。Closure编译用这些类型表达式形成基本的类型系统。通过这些类型信息,编译器能在编译时确定所有方法是否传递了正确类型的参数。这能在大类中发现很多错误。将在408页中的“类型检查”中介绍。

类型信息也可以通过@type注解标注:

[html] view plaincopy
  1. /** @type {number} */  
  2. example.highestRecordedTemperatureForTodayInFahrenheit = 33;  
编译器也支持枚举类型。枚举是键值对。和Java一样,枚举的名字是单一的值,但值可以是任何东西。枚举类型用@enum注解标注,并且每一个值都必需是正确的枚举类型:
[javascript] view plaincopy
  1. /** @enum {number} */  
  2. example.CardinalDirection = {  
  3.   NORTH: Math.PI / 2,  
  4.   SOUTH: 3 * Math.PI / 2,  
  5.   EAST: 0,  
  6.   WEST: Math.PI  
  7. };  
[javascript] view plaincopy
  1. /** @enum {string} */  
  2. example.CardinalDirectionName = {  
  3.   NORTH: 'N',  
  4.   SOUTH: 'S',  
  5.   EAST: 'E',  
  6.   WEST: 'W'  
  7. };  
当一个函数声明要使用this关键字的时候,@this注解用来声明这种类型。这在Jquery对一系列Dom元素频繁使用回调方法时中很常用 。下面的例子是Jquery中用来删除所有<img>元素的:
[javascript] view plaincopy
  1. /** @this {Element} */  
  2. example.removeElement = function() { this.parentNode.removeChild(this); };  
  3. // $('IMG') in jQuery returns an object with an each() method that takes a  
  4. // function and calls it for each element where 'this' is bound to the element.  
  5. $('IMG').each(example.removeElement);  
Closure类型系统不创建新类就能引入新的类型,像Java这样的强类型语言很难区别类开类型。@typedef注解用来声明一个复杂类型的别名:
[javascript] view plaincopy
  1. /** @typedef {{x: number, y: number}} */  
  2. example.Point;  
上面例子中,exapmle.Point是{x:number,y:number}的别名,用来记录一个包含数字属性x,y的一个对象。现在可以用example.Point来替换{x:number,y:number},这样函数将更简洁:
[javascript] view plaincopy
  1. /** 
  2.  * Returns a new Point which is the original point translated by distance in 
  3.  * both the x and y dimensions. 
  4.  * @param {example.Point} point 
  5.  * @param {number} distance 
  6.  * @return {example.Point} 
  7.  */  
  8. example.translate = function(point, distance) {  
  9.   return {  
  10.     x: point.x + distance,  
  11.     y: point.y + distance  
  12.   };  
  13. };  

在Java中要创建一个抽象类,必须创建一个抽象类文件,但Javascrit中添加一个新类型更简单。

类型表达式

类型表达式是JSDoc标记中用来描述数据类型。@param用来描述参数类型,@return用来描述返回值类型,类型静态式通常用花括号声明,本章也会讨论一些复合类型。

编译器能在编译时验证参数返回值类型,但是默认情况下是支持的。就像408页所讲的“类型检查”一样,在编译器中必须开启类型检查选项。除编译器外,类型表达式仅仅是普通的文档,用不增强对Closure Library的理解和阅读。

简单类型和复合类型

在上一节中,除了@typedef外,所有例子都是最基本的类型表达式:每一个基本类型的名字都用花括号包围起来。注意的是,number,string和boolean都是小写,因此又称为原始类型,与之相对的是包装类型:Number,String和Boolean。在Closure Library中,包装类型是禁止的。因为在一些函数中如果用包装类型代替原始类型可能会有问题。可以参考“goog.isString(obj), goog.isBoolena(obj), goog.isNumber(obj)”,在64页中有更详细的介绍。

除了上面提到的原始类型外,像Date,Array和Object也可以被使用。对于一个包含Object值的数组可以用.加上尖括号表示。有点像Java泛型。

[javascript] view plaincopy
  1. /** 
  2.  * @param {Array} arrayOfUnknowns Specifies an Array type. This makes no 
  3.  *     guarantees on the types of the elements in this Array. 
  4.  * @param {Array.<string>} arrayOfStrings Specifies an Array whose elements 
  5.  *     are strings. 
  6.  * @param {Array.<Array.<string>>} arrayOfStringArrays Specifies an Array of 
  7.  *     Arrays whose elements are strings. As shown here, parameterized types 
  8.  *     can be nested. 
  9.  * @param {Object} someObject Specifies the Object type. This includes subtypes, 
  10.  *     such as Date and Array, but excludes primitive types, such as number, 
  11.  *     string, and boolean. 
  12.  * @param {Object.<number>} mapWithNumericValues Because the key of an object 
  13.  *     is a string by definition, only the type of the value needs to be specified. 
  14.  * @param {Object.<example.CardinalDirectionName,string>} mapWithEnumKey The 
  15.  *     keys in an object may also be restricted to values from a specific enum. 
  16.  */  
目前只有Array和Object在参数语法中支持。

许多浏览器中的对象,比如document,类型名字和W3C DOM规范中定义的名字一样,document的类型对象是HTMLDocument,DOM元素和节点的类型对象是Element和Node.编译器知道引用的外部文件中所引用的类型。

一般情况下,一个外部声明类和接口的Javascript文件不包含实现部分,它的实现会在运行时提供。比如你不用实现document对象,它会由浏览器提供,但是编译器需要知道HtmlDocument对象的声明才能检查document中方法调用时参数的正确性。

开发者也可以声明自己的类型,最简单的方式是用@typedef标示,但是,最一般的方法是声明一个新类,接口或枚举来创建新类型。前面讲了用枚举来创建新类型,第5章将要介绍创建类和接口来创建新类型。类和接口声明可以用@constructor和@interface注解标识。Closure Library 中许多文件都声明了类和接口,因此可以用作类型表达式。

[javascript] view plaincopy
  1. /** 
  2.  * @param {goog.net.XhrIo} xhr must be an instance of the goog.net.XhrIo 
  3.  *     class (or null). 
  4.  * @param {goog.events.EventWrapper} eventWrapper must be an object that 
  5.  *     implements the goog.events.EventWrapper interface (or null). 
  6.  */  
另两种类型是null和undefined。管道字符|用来连接联合类型,联合类型用来表明参数可以有多个类型:
[javascript] view plaincopy
  1. /** 
  2.  * Tries to use parseInt() to convert str to an integer. If parseInt() returns 
  3.  * NaN, then example.convertStringToInteger returns 0 instead. 
  4.  * 
  5.  * @param {string|null|undefined} str 
  6.  * @return {number} 
  7.  */  
  8.   
  9. example.convertStringToInteger = function(str) {  
  10.   var value = parseInt(str, 10);  
  11.   return isNaN(value) ? 0 : value;  
  12. };   

骑过用联合类型, example.convertStringToInteger将可以接受string, null或undefined三种类型,但它返回一个非null的数字。因为参数是否非空很常见,因此有一个快捷方式用来标明联合类型是否包含null:

[javascript] view plaincopy
  1. /** 
  2.  * @param {?string} str1 is a string or null 
  3.  * @param {?string|undefined} str2 is a string, null, or undefined. The ? 
  4.  *     prefix does not include undefined, so it must be included explicitly. 
  5.  */  
除了基本类型的所有类型,如Object,Array和HTMLDocument默认都可以为空,这些类型统称为对象类型,因此?前缀对对象类型是多余的:
[javascript] view plaincopy
  1. /** 
  2.  * @param {Document} doc1 is a Document or null because object types are 
  3.  *     nullable by default. 
  4.  * @param {?Document} doc2 is also a Document or null. 
  5.  */  
如果要声明一个非空对象对开,可以用!前缀:
[javascript] view plaincopy
  1. /** 
  2.  * @param {!Array} array must be a non-null Array 
  3.  * @param {!Array|undefined} maybeArray is an Array or undefined, but 
  4.  *     cannot be null. 
  5.  */  
同样的,因为基本类型默认是非空的,因此!前缀是多余的,因此,{!number}和{number}都是非空数字。

函数类型

在javascript中,函数可以作为参数传递给别一函数,因此Closure类型系统中有丰富的语法对其提供支持:

[javascript] view plaincopy
  1. /** @param {Function} callback is some function */  
这个例子只是简单声明了一个函数参数,并没有提供它的参数和返回值声明。使用Function注解,类型不能提供更详细的功能,近来,由于对类型表达式的加强,function注解用来描述函数表达式,它能提供更详细的参数说明,函数类型的参数可以用括号包围:
[javascript] view plaincopy
  1. /** 
  2.  * @param {function()} f Specifies a function that takes no parameters and has 
  3.  *     an unknown return value. 
  4.  * @param {function(string, ?number)} g Specifies a function that takes a 
  5.  *     non-null string and a nullable number. Its return value is also unknown. 
  6.  */  
函数表达式的返回值可以用冒号加上类型表示:
[javascript] view plaincopy
  1. /** 
  2.  * @param {function(): boolean} f Specifies a function that takes no parameters 
  3.  *     and returns a boolean. 
  4.  * @param {function(string, ?number): boolean} g Specifies a function that takes 
  5.  *     a non-null string and a nullable number and returns a non-null boolean. 
  6.  */  
最后this类型也可以用this:再加上类型:
[javascript] view plaincopy
  1. /** 
  2.  * @param {function(this: Element, string, string)} f Specifies a function that 
  3.  *     takes two string parameters where "this" will be bound to an Element when 
  4.  *     the function is called. 
  5.  */  
尽管上面看上去包括this:共有3个参数,但实际上只有两个,因为this:是被类型系统所用的特殊参数。

记录类型

像example.Point例子中所示,类型表达式可能是一个包含属性值的对象,这样的类型叫记录类型。记录类型声明和javascript中的对象声明一样,只是它的值可以不声明或者声明为类型表达式:

[javascript] view plaincopy
  1. ** @typedef {{row: number, column: string, piece}} */  
  2. example.chessSquare;  
上面例子用@typedef声明example.chessSquare为一个对象,有一个number类型的属性row和一个string类型的属性column,piece没有类型声明,因此可以为任何内容。

就像以前提到的一样,记录类型的值可以是任何类型,包括另一个记录类型,函数类型,也可以是自身的引用:

[javascript] view plaincopy
  1. /** @typedef {{from: example.chessSquare, to: example.chessSquare}} */  
  2. example.chessMove;  
  3. /** @typedef {{state: Array.<example.chessSquare>, 
  4.                equals: function(example.chessBoard, example.chessBoard): boolean}} */  
  5. example.chessBoard;  
值得注意的是,记录类型中的属性并不是对象拥有的所有属性,他只列出了最少必须的属性。如下面的例子,threeDimensionalPoint能传递给  example.translate(),因为它满足example.Point类型声明:
[javascript] view plaincopy
  1. /** 
  2.  * The additional property named 'z' does not disqualify threeDimensionalPoint 
  3.  * from satisfying the example.Point type expression. 
  4.  * 
  5.  * @type {example.Point} 
  6.  */  
  7. var threeDimensionalPoint = { x: 3, y: 4, z: 5};  
  8. // twoDimensionalPoint is { x: 1, y: 2 }, which satisfies the definition for  
  9. // example.Point.  
  10. var twoDimensionalPoint = example.translate(threeDimensionalPoint, -2);  
特殊的@param类型
指定可选参数

在javascript中,function通常提供可选参数。通过指定一参数是可选的,在没有传入所有参数时编译器不会发出警告。通过在一个类型表达式的后面加一下=后缀就表明参数是可选的:

[javascript] view plaincopy
  1. /** 
  2.  * Creates a new spreadsheet. 
  3.  * @param {string} author 
  4.  * @param {string=} title Defaults to 'New Spreadsheet'. 
  5.  */  
  6. example.createNewSpreadsheet = function(author, title) {  
  7.   title = title || 'New Spreadsheet';  
  8.   // Create a new spreadsheet using author and title...  
  9. };  
因为title参数是可选的,因此example.createNewSpreadsheet('bolinfest@gmail.com').在编译不会有警告。如果两个参数都是必须的,将会有如下错误:
[javascript] view plaincopy
  1. spreadsheet-missing-optional-annotation.js:13: ERROR - Function  
  2. example.createNewSpreadsheet: called with 1 argument(s). Function requires  
  3. at least 2 argument(s) and no more than 2 argument(s).  
  4. example.createNewSpreadsheet('bolinfest@gmail.com');  
  5.                             ^  
  6. 1 error(s), 0 warning(s), 86.6% typed  
尽管一个方法中可以有任意多个可选参数,但是可选参数不应改出现在必须参数之前,如果出现在之前,代码必须写成如下形式:
[javascript] view plaincopy
  1. // DO NOT DO THIS: optional parameters must be listed last.  
  2. /** 
  3.  * @param {string=} title Defaults to 'New Spreadsheet'. 
  4.  * @param {string} author 
  5.  */  
  6. example.createNewSpreadsheet = function(title, author) {  
  7.   if (arguments.length == 1) {  
  8.     author = title;  
  9.     title = undefined;  
  10.   }  
  11.   // Create a new spreadsheet using author and title...  
  12. };  
这样的代码很难跟踪和维护。如果有大量的可靠参数,最好的方法是将其它移到一个必须的对象参数中:
[javascript] view plaincopy
  1. **  
  2.  * @param {{author: (string|undefined), title: (string|undefined),  
  3.  *     numRows: (number|undefined)  
  4.  */  
  5. example.createNewSpreadsheetWithRows = function(properties) {  
  6.   var author = properties.author || 'bolinfest@gmail.com';  
  7.   var title = properties.title || 'New Spreadsheet';  
  8.   var numRows = properties.numRows || 1024;  
  9.   // Create a new spreadsheet using author, title, and numRows...  
  10. };  
  11. // THIS WILL YIELD A TYPE CHECKING ERROR FROM THE COMPILER  
  12. example.createNewSpreadsheetWithRows({title: '2010 Taxes'});  
不幸的是,上面的代码却不能工作,因为编译不能将未声明值当成已声明值undefined,编译器将发出如下类型检查错误:
[javascript] view plaincopy
  1. badspreadsheet.js:17: ERROR - actual parameter 1 of  
  2. example.createNewSpreadsheetWithRows does not match formal parameter  
  3. found   : {title: string}  
  4. required: { author : (string|undefined), title : (null|string),  
  5.     numRows : (number|undefined) }  
  6. example.createNewSpreadsheetWithRows({title: '2010 Taxes'});  
  7.                                      ^  
  8. 1 error(s), 0 warning(s), 86.7% typed  
因为目前在Closure类型系统中我们还不能声明一个可选对象,我们变通的方法是声明一个泛型对象并在文档中描述其属性:
[javascript] view plaincopy
  1. /** 
  2.  * Creates a new spreadsheet. 
  3.  * @param {Object} properties supports the following options: 
  4.  *   author (string): email address of the spreadsheet creator 
  5.  *   title (string): title of the spreadsheet 
  6.  *   numRows (number): number of rows the spreadsheet should have 
  7.  * @notypecheck 
  8.  */  
  9. example.createNewSpreadsheetWithRows = function(properties) {  
  10.   var author = properties.author || 'bolinfest@gmail.com';  
  11.   var title = properties.title || 'New Spreadsheet';  
  12.   var numRows = properties.numRows || 1024;  
  13.   // Create a new spreadsheet using author, title, and numRows...  
  14. };  
  15. // This no longer results in a type checking error from the Compiler.   
  16. example.createNewSpreadsheetWithRows({title: '2010 Taxes'});  
@notypecheck注解告诉编译器对example.createNewSpreadsheetWithRows()忽略类型检查,没有它,编译器将发出如下错误:
[javascript] view plaincopy
  1. spreadsheet-without-notypecheck.js:13: ERROR - Property  
  2. author never defined on Object  
  3.   var author = properties.author || 'bolinfest@gmail.com';  
  4.                ^  
  5. spreadsheet-without-notypecheck.js:15: ERROR - Property   
  6. numRows never defined on Object  
  7.   var numRows = properties.numRows || 1024;  
  8.                 ^  
  9. 2 error(s), 0 warning(s), 86.3% typed  
尽管用函数包含很多可选参数比对象包含可选参数玩高效,但它不能通过编译器检查,将来可能有新的注解来完成此功能 。

可选参数

在javascript函数中,如果一个方法有一个参数,当调用时没有传参数给方法,这个参数的值为undefined,因此,对一boolean型参数默认值一般为false.下面的方法将会使用默认值为true:

[javascript] view plaincopy
  1. // DO NOT DO THIS: optional boolean parameters should default to false  
  2. /** 
  3.  * @param {boolean=} isRefundable Defaults to true. 
  4.  * @param {number=} maxPrice Defaults to 1000.  
  5.  */  
  6. example.buyTicket = function(isRefundable, maxPrice) {  
  7.   // goog.isNumber() is introduced in the next chapter.  
  8.   maxPrice = goog.isNumber(maxPrice) ? maxPrice : 1000;  
  9.   isRefundable = (isRefundable === undefined) ? true : isRefundable;  
  10.   if (isRefundable) {  
  11.     example.buyRefundableTicket(maxPrice);  
  12.   } else {  
  13.     example.buyNonRefundableTicket(maxPrice);  
  14.   }  
  15. };  
尽管不是必须要求,null常常被用来表示可选参数的值,当调用example.buyTicket(null,250)时,isRefundable当为false,而不是默认的true.为避免这种问题,最好将可选的boolean默认值保持为false,如果默认值被使用,可用undefined代替。
[javascript] view plaincopy
  1. /** 
  2.  * @param {boolean=} isNonRefundable Defaults to false. 
  3.  * @param {number=} maxPrice Defaults to 1000.  
  4.  */  
  5. example.buyTicket = function(isNonRefundable, maxPrice) {  
  6.   maxPrice = goog.isNumber(maxPrice) ? maxPrice : 1000;  
  7.   if (isNonRefundable) {  
  8.     example.buyNonRefundableTicket(maxPrice);  
  9.   } else {  
  10.     example.buyRefundableTicket(maxPrice);  
  11.   }  
  12. };  

不定数量参数

Closure的方法也支持不定数量参数。即使这些参数没有名字,他也也能够通过方法中的arguments对象访问,编译器要求使用@param标签注解不定参数中的第一个,其它参数的注解是...,其类型表达式是可选的。

[javascript] view plaincopy
  1. /** 
  2.  * @param {string} category 
  3.  * @param {...} purchases 
  4.  * @return {number} 
  5.  */  
  6. example.calculateExpenses = function(category, purchases) {  
  7.   // Note that purchases is never referenced within this function body.  
  8.   // It primarily exists so it can be annotated with {...} for the Compiler,  
  9.   // though it could be used in this function body to refer to the first  
  10.   // variable parameter, if it exists. It is identical to arguments[1].  
  11.   var sum = 0;  
  12.   // Initialize i to 1 instead of 0 because the first element in arguments is  
  13.   // the "category" parameter, which is not part of the sum.  
  14.   for (var i = 1; i < arguments.length; ++i) {  
  15.     sum += arguments[i];  
  16.   }  
  17.   alert('The total spent on ' + category + ' is ' + sum + ' dollars.');  
  18.   return sum;  
  19. };  
  20. // The Compiler will issue an error because "category" is a required parameter.  
  21. example.calculateExpenses();  
  22. // The Compiler will not issue an error for either of the following because 0  
  23. // or more parameters can be specified after the required "category" parameter.  
  24. example.calculateExpenses('breakfast');  
  25. example.calculateExpenses('nachos', 25, 32, 11, 40, 12.50);  
因为所有可变参数都是数字,所以purchases可以用如下注解:
[javascript] view plaincopy
  1. * @param {...number} purchases  
这可以更明确的注解 example.calculateExpenses()方法。

除了用@param标注注解注解参数外,也可以注解函数的可选和可变参数:

[javascript] view plaincopy
  1. /** 
  2.  * @param {function(string, string=)} f Function that takes a string and 
  3.  *     optionally one other string. example.createNewSpreadsheet() satisfies 
  4.  *     this type expression. 
  5.  * @param {function(string, ...[number]}: number} g Function that takes a string 
  6.  *     and a variable number of number parameters and returns a number. 
  7.  *     example.calculateExpenses() satisfies this type expression. Square 
  8.  *     brackets for the variable parameter type are required when defining 
  9.  *     the type of a variable parameter as an argument to a function type. 
  10.  */  
在Closure Library中,可选参数通常用opt_前缀命名,可变参数长度通常用var_args表示。这是Google在类型系统中的代码习惯。在新代码中使用opt_前缀是应该鼓励的,这会使代码更可读:
[javascript] view plaincopy
  1. /** 
  2.  * @param {string} author 
  3.  * @param {string=} opt_title Defaults to 'New Spreadsheet'. 
  4.  */  
  5. example.createNewSpreadsheet = function(author, opt_title) {  
  6.   // Now this function has both 'title' and 'opt_title' variables in scope.  
  7.   var title = opt_title || 'New Spreadsheet';  
  8.   // Create a new spreadsheet using author and title (but not opt_title)...  
  9. };  
可变参数使用var_arg将减少很多麻烦,也可也选择一个能更好描述功能的名字,如purchases in example.calculateExpenses()中的那样。

字类型和类型转换

在Closure中用类型表达式指定类型。如果T在的所有属性都属于S的话,可以说S是T的字类型。在这种情况下,类型T中的Javascript对象中的属性不是用键-值对表示的。但是类型T以应该被声明。例如,下面的两种类型:

[javascript] view plaincopy
  1. /** @typedef {{year: number, month: number, date: number}} */  
  2. example.Date;  
  3. /** @typedef {{year: number, month: number, date: number, 
  4.                hour: number, minute: number, second: number}} */  
  5. example.DateTime;  
example.Date和example.DateTime中都各自用键-值对描述了所有属性,如key为year,value为number.但是example.DateTime中有hour属性,可example.Date中没有。

在Closure类型系统中,example.DateTime应该为example.Date的子类型,因为example.DateTime包含example.Date中的所有属性。由于有父子关系,编译器允许所有参数为example.Date的地主用example.DateTime替换。如果反过来却不可以。因为 example.DateTime有3个 example.Date 没有的属性,因此 example.Date 不是example.DateTime的子类型。
当一个函数类型中的参数中一个类是另一个类的子类时,他们的关系和上面的相反:

[javascript] view plaincopy
  1. /** @typedef {function(example.Date)} */  
  2. example.DateFunction;  
  3. /** @typedef {function(example.DateTime)} */  
  4. example.DateTimeFunction;  
上面情况下,example.DateFunction是example.DateTimeFunction的子类,因为example.DateTimeFunction包含example.DateFunction中的所有属性,下面是一个更详细的例子:
[javascript] view plaincopy
  1. /** @type {example.DateFunction} */  
  2. example.writeDate = function(date) {  
  3.   document.write(date.year + '-' + date.month + '-' + date.date);  
  4. };  
  5. /** @type {example.DateTimeFunction} */  
  6. example.writeDateTime = function(dateTime) {  
  7.   document.write(dateTime.year + '-' + dateTime.month + '-' + dateTime.date +  
  8.       ' ' + dateTime.hour + ':' + dateTime.minute + ':' + dateTime.second);  
  9. };  
  10. /** 
  11.  * @param {example.DateFunction} f 
  12.  * @param {example.Date} date 
  13.  */  
  14. example.applyDateFunction = function(f, date) { f(date); };  
  15. /** 
  16.  * @param {example.DateTimeFunction} f 
  17.  * @param {example.DateTime} dateTime 
  18.  */  
  19. example.applyDateTimeFunction = function(f, dateTime) { f(dateTime); };  
  20. /** @type {example.Date} */  
  21. var date = {year: 2010, month: 12, date: 25};  
  22. /** @type {example.DateTime} */  
  23. var dateTime = {year: 2010, month: 12, date: 25, hour: 12, minute: 13, second: 14};  
下面两种写法能正确运行:
[javascript] view plaincopy
  1. / Writes out: 2010-12-25  
  2. example.applyDateFunction(example.writeDate, date  
  3. // Writes out: 2010-12-25 12:13:14  
  4. example.applyDateTimeFunction(example.writeDateTime, dateTime);  
现在看看 example.DateFunction为 example.DateTimeFunction子类下发生的情况:
[javascript] view plaincopy
  1. // Writes out: 2010-12-25  
  2. example.applyDateTimeFunction(example.writeDate, dateTime);  
尽管没有像上例子有日期输出,但也没有错误出现。如果  example.DateTimeFunction 是 example.DateFunction的子类:
[javascript] view plaincopy
  1. // Writes out an ill-formed DateTime: 2010-12-25 undefined:undefined:undefined  
  2. example.applyDateFunction(example.writeDateTime, date);  
这个程序会产生不正确的结果,因为 example.DateTimeFunction 不是 example.DateFunction的子类型。

Colsure编译器用这样的基本原则来确定一个类型能否成为另一个的子类型。不幸的是,一些替换可能是安全的,但 Closure编译不能保证其安全性。例如,如果程序中声明一个非空属性,但编译器却不这么认为:

[javascript] view plaincopy
  1. /** 
  2.  * @param {!Element} element 
  3.  * @param {string} html 
  4.  */  
  5. example.setInnerHtml = function(element, html) {  
  6.   element.innerHTML = html;  
  7. };  
  8. var greetingEl = document.getElementById('greeting');  
  9. // If type checking is enabled, the Compiler will issue an error that greetingEl  
  10. // could be null whereas example.setInnerHTML() requires a non-null Element.  
  11. example.setInnerHTML(greetingEl, 'Hello world!');  
在externs/gecko_dom.js中,getElementById()方法传入下个string参数并返回HTMLElement。因为返回类型为!HTMLElement,这个方法也可能返回null。开发人员能保证些方法返回非空,但编译器却不能。

编译器提供了一种@type中的特殊用法来解决此问题:

[javascript] view plaincopy
  1. var greetingEl = /** @type {!Element} */ (document.getElementById('greeting'));  
  2. // Because of the use of @type on the previous line, the Compiler will consider  
  3. // greetingEl as an object of type {!Element} going forward. This eliminates the  
  4. // error previously issued by the Compiler.  
  5. example.setInnerHTML(greetingEl, 'Hello world!');  
这是一个类型转换的例子,编译器将一种类型转换为另一种类型。在编译过程中,对象的数据没有任何改变:仅仅是对象中与编译类型相关的才会变化。通过用/** @type
TYPE_EXPRESSION */封装一个表达式,编译器将结果和 TYPE_EXPRESSION声明的进行比较。实际上,子类型将在第5章介绍。父类和子类的关系也会改变父类弄和子类型。

这有点像C语言中的类型转换,但和Java不一样,因为Java会在运行时抛出ClassCastException错误,在javascript和c中转换错误不会有警告。程序也会继续执行。为了避免这些错误,应尽量不要使用类型转换。

All类型

有一种特殊的类型叫ALL类型,它请允许任何类型:

[javascript] view plaincopy
  1. /** 
  2.  * @param {*} obj The object to serialize. 
  3.  * @throw Error if obj cannot be serialized 
  4.  * @return {string} A JSON string representation of the input 
  5.  */  
  6. goog.json.serialize = function(obj) { /* ... */ };  
因为All类型比{Object}更通用,因为obj也要要用{Object}声明,通过All类型,可以 goog.json.serialize()传入基本类型和undefined。
JSDoc标记不是全处理类型

JSDoc不光是用来处理类型,也可以用来注解常量,过期变量,和licensing信息。其它注解标记将在第5张介绍,如:@extends,  @implements,  @inheritDoc,  @interface,  @override,  @private, 和@protected.

常量

@const用来声明一个常量:

[javascript] view plaincopy
  1. /** 
  2.  * @type {number} 
  3.  * @const 
  4.  */  
  5. var MAX_AMPLIFIER_VOLUME = 11;  

因为MAX_AMPLIFIER_VOLUME是一个常量,如果有地方重新声明它的话,编译器会抛出错误。编译器为了使代码更小,会将其内连。

如果用@define注解,编译器能在编译时声明变量,只能在boolean,string或number变量使用这个标记:

[javascript] view plaincopy
  1. /** @define {boolean} */  
  2. example.USE_HTTPS = true;  
  3. /** @define {string} */  
  4. example.HOSTNAME = 'example.com';  
  5. /** @define {number} */  
  6. example.PORT = 80;  
  7. /** 
  8.  * @type {string} 
  9.  * @const 
  10.  */  
  11. example.URL = 'http' + (example.USE_HTTPS ? 's' : '') + '://' +  
  12.     example.HOSTNAME + ':' + example.PORT + '/';  
在上面例子中,example.URL可能为  https://example.com:80/,但开发时可能不支持HTTPS,因此用--define如下编译:

[javascript] view plaincopy
  1. java -jar compiler.jar --js example.js --define example.USE_HTTPS=false \  
  2.     --define example.HOSTNAME='localhost' --define example.PORT=8080  
编译后,example.URL值为:http://localhost:8080/.

过期变量

像Java一样,@deprecated用来标识不再使用的方法和属性:

[javascript] view plaincopy
  1. /** @deprecated Use example.setEnabled(true) instead. */  
  2. example.enable = function() { /*...*/ };  
  3. /** @deprecated Use example.setEnabled(false) instead. */  
  4. example.disable = function() { /*...*/ };  
当过期变量被使用时编译器会发出警告。

版权信息

版权信息用来出现在编译后的Javascript文件开始,用@license或@preserve标注:

[javascript] view plaincopy
  1. /** 
  2.  * @preserve Copyright 2010 SomeCompany. 
  3.  * The license information (such as Apache 2.0 or MIT) will also be included 
  4.  * here so that it is guaranteed to appear in the compiled output. 
  5.  */  

这些是真正必要的吗?

由于在注解中大量用到javadoc工具和java注解关键字,很多人提出:"这是在将javascript转换成java!"

确实在Closure有将Java和其它一些语言的概念转换为javascript,比如类型检查,信息隐藏和继承,对这些功能的支持也使得增加javascript代码和需要更多内存。

不过放心,这重量级的JSDoc在Closure中是可选的。编译器也能编译没有注解的代码,只有注解后的代码在编译时能发现更多错误和编译成更高效代码。

注解也能最后生成一个文档,像gmail和google map这样复杂的应用,有很多开发者超过一年时间来开发,注解可以让开发的代码能更好的维护。

0 0
原创粉丝点击