转载地址:http://blog.csdn.net/zj831007/article/details/6801110
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函数声明:
- /**
- * @param {Function} fn A function to partially apply.
- * @param {Object|undefined} selfObj Specifies the object which |this| should
- * point to when the function is run. If the value is null or undefined,
- * it will default to the global object.
- * @param {...*} var_args Additional arguments that are partially
- * applied to the function.
- * @return {!Function} A partially-applied form of the function bind() was
- * invoked as a method of.
- */
- goog.bind = function(fn, selfObj, var_args) {
- // implementation of goog.bind()
- };
- // The double slash is used to comment out everything to the end of the line.
- var meaningOfLife = 42; // It is often used as a partial line comment like this.
- /* The slash followed by an asterisk is used to comment out everything between
- * it and the asterisk followed by a slash at the end of this paragraph.
- * Although only the asterisks after the opening slash and before the closing
- * slash are required, it is customary to include an asterisk at the start of
- * each line within a multi-line comment, as illustrated by this example. */
在Java中,包含javadoc的注释必须用/***/声明。这通常叫着文档注释。像javadoc一样,javascript也用/** 和/*包含JSDoc注释。许多编辑器支持对/*..*/和/**...*/用不同方式显示这两种注释。这会底帮助将普通注释块放在文档注释块的错误。
就像你妈妈常常告诉你的一样,它真正依赖其中的内容,在JSDoc中,文档注释中真正依赖的是JSDoc标记。JSDOC标记@character,紧接是可识别的JSDoc标记名,具体值可以是任何东西,直到下一个标记开始。
所有本意中的标记都是块标记,必须从一行开始,下面的块就不正确:
- /**
- * NEGATIVE EXAMPLE: This is NOT an appropriate use of block tags.
- * @constructor @extends {goog.Disposable} @private
- */
- /**
- * This is an appropriate use of block tags.
- * @constructor
- * @extends {goog.Disposable}
- * @private
- */
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注解标注:
- /** @type {number} */
- example.highestRecordedTemperatureForTodayInFahrenheit = 33;
- /** @enum {number} */
- example.CardinalDirection = {
- NORTH: Math.PI / 2,
- SOUTH: 3 * Math.PI / 2,
- EAST: 0,
- WEST: Math.PI
- };
- /** @enum {string} */
- example.CardinalDirectionName = {
- NORTH: 'N',
- SOUTH: 'S',
- EAST: 'E',
- WEST: 'W'
- };
- /** @this {Element} */
- example.removeElement = function() { this.parentNode.removeChild(this); };
- // $('IMG') in jQuery returns an object with an each() method that takes a
- // function and calls it for each element where 'this' is bound to the element.
- $('IMG').each(example.removeElement);
- /** @typedef {{x: number, y: number}} */
- example.Point;
- /**
- * Returns a new Point which is the original point translated by distance in
- * both the x and y dimensions.
- * @param {example.Point} point
- * @param {number} distance
- * @return {example.Point}
- */
- example.translate = function(point, distance) {
- return {
- x: point.x + distance,
- y: point.y + distance
- };
- };
在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泛型。
- /**
- * @param {Array} arrayOfUnknowns Specifies an Array type. This makes no
- * guarantees on the types of the elements in this Array.
- * @param {Array.<string>} arrayOfStrings Specifies an Array whose elements
- * are strings.
- * @param {Array.<Array.<string>>} arrayOfStringArrays Specifies an Array of
- * Arrays whose elements are strings. As shown here, parameterized types
- * can be nested.
- * @param {Object} someObject Specifies the Object type. This includes subtypes,
- * such as Date and Array, but excludes primitive types, such as number,
- * string, and boolean.
- * @param {Object.<number>} mapWithNumericValues Because the key of an object
- * is a string by definition, only the type of the value needs to be specified.
- * @param {Object.<example.CardinalDirectionName,string>} mapWithEnumKey The
- * keys in an object may also be restricted to values from a specific enum.
- */
许多浏览器中的对象,比如document,类型名字和W3C DOM规范中定义的名字一样,document的类型对象是HTMLDocument,DOM元素和节点的类型对象是Element和Node.编译器知道引用的外部文件中所引用的类型。
一般情况下,一个外部声明类和接口的Javascript文件不包含实现部分,它的实现会在运行时提供。比如你不用实现document对象,它会由浏览器提供,但是编译器需要知道HtmlDocument对象的声明才能检查document中方法调用时参数的正确性。
开发者也可以声明自己的类型,最简单的方式是用@typedef标示,但是,最一般的方法是声明一个新类,接口或枚举来创建新类型。前面讲了用枚举来创建新类型,第5章将要介绍创建类和接口来创建新类型。类和接口声明可以用@constructor和@interface注解标识。Closure Library 中许多文件都声明了类和接口,因此可以用作类型表达式。
- /**
- * @param {goog.net.XhrIo} xhr must be an instance of the goog.net.XhrIo
- * class (or null).
- * @param {goog.events.EventWrapper} eventWrapper must be an object that
- * implements the goog.events.EventWrapper interface (or null).
- */
- /**
- * Tries to use parseInt() to convert str to an integer. If parseInt() returns
- * NaN, then example.convertStringToInteger returns 0 instead.
- *
- * @param {string|null|undefined} str
- * @return {number}
- */
- example.convertStringToInteger = function(str) {
- var value = parseInt(str, 10);
- return isNaN(value) ? 0 : value;
- };
骑过用联合类型, example.convertStringToInteger将可以接受string, null或undefined三种类型,但它返回一个非null的数字。因为参数是否非空很常见,因此有一个快捷方式用来标明联合类型是否包含null:
- /**
- * @param {?string} str1 is a string or null
- * @param {?string|undefined} str2 is a string, null, or undefined. The ?
- * prefix does not include undefined, so it must be included explicitly.
- */
- /**
- * @param {Document} doc1 is a Document or null because object types are
- * nullable by default.
- * @param {?Document} doc2 is also a Document or null.
- */
- /**
- * @param {!Array} array must be a non-null Array
- * @param {!Array|undefined} maybeArray is an Array or undefined, but
- * cannot be null.
- */
函数类型
在javascript中,函数可以作为参数传递给别一函数,因此Closure类型系统中有丰富的语法对其提供支持:
- /** @param {Function} callback is some function */
- /**
- * @param {function()} f Specifies a function that takes no parameters and has
- * an unknown return value.
- * @param {function(string, ?number)} g Specifies a function that takes a
- * non-null string and a nullable number. Its return value is also unknown.
- */
- /**
- * @param {function(): boolean} f Specifies a function that takes no parameters
- * and returns a boolean.
- * @param {function(string, ?number): boolean} g Specifies a function that takes
- * a non-null string and a nullable number and returns a non-null boolean.
- */
- /**
- * @param {function(this: Element, string, string)} f Specifies a function that
- * takes two string parameters where "this" will be bound to an Element when
- * the function is called.
- */
记录类型
像example.Point例子中所示,类型表达式可能是一个包含属性值的对象,这样的类型叫记录类型。记录类型声明和javascript中的对象声明一样,只是它的值可以不声明或者声明为类型表达式:
- ** @typedef {{row: number, column: string, piece}} */
- example.chessSquare;
就像以前提到的一样,记录类型的值可以是任何类型,包括另一个记录类型,函数类型,也可以是自身的引用:
- /** @typedef {{from: example.chessSquare, to: example.chessSquare}} */
- example.chessMove;
- /** @typedef {{state: Array.<example.chessSquare>,
- equals: function(example.chessBoard, example.chessBoard): boolean}} */
- example.chessBoard;
- /**
- * The additional property named 'z' does not disqualify threeDimensionalPoint
- * from satisfying the example.Point type expression.
- *
- * @type {example.Point}
- */
- var threeDimensionalPoint = { x: 3, y: 4, z: 5};
- // twoDimensionalPoint is { x: 1, y: 2 }, which satisfies the definition for
- // example.Point.
- var twoDimensionalPoint = example.translate(threeDimensionalPoint, -2);
指定可选参数
在javascript中,function通常提供可选参数。通过指定一参数是可选的,在没有传入所有参数时编译器不会发出警告。通过在一个类型表达式的后面加一下=后缀就表明参数是可选的:
- /**
- * Creates a new spreadsheet.
- * @param {string} author
- * @param {string=} title Defaults to 'New Spreadsheet'.
- */
- example.createNewSpreadsheet = function(author, title) {
- title = title || 'New Spreadsheet';
- // Create a new spreadsheet using author and title...
- };
- spreadsheet-missing-optional-annotation.js:13: ERROR - Function
- example.createNewSpreadsheet: called with 1 argument(s). Function requires
- at least 2 argument(s) and no more than 2 argument(s).
- example.createNewSpreadsheet('bolinfest@gmail.com');
- ^
- 1 error(s), 0 warning(s), 86.6% typed
- // DO NOT DO THIS: optional parameters must be listed last.
- /**
- * @param {string=} title Defaults to 'New Spreadsheet'.
- * @param {string} author
- */
- example.createNewSpreadsheet = function(title, author) {
- if (arguments.length == 1) {
- author = title;
- title = undefined;
- }
- // Create a new spreadsheet using author and title...
- };
- **
- * @param {{author: (string|undefined), title: (string|undefined),
- * numRows: (number|undefined)
- */
- example.createNewSpreadsheetWithRows = function(properties) {
- var author = properties.author || 'bolinfest@gmail.com';
- var title = properties.title || 'New Spreadsheet';
- var numRows = properties.numRows || 1024;
- // Create a new spreadsheet using author, title, and numRows...
- };
- // THIS WILL YIELD A TYPE CHECKING ERROR FROM THE COMPILER
- example.createNewSpreadsheetWithRows({title: '2010 Taxes'});
- badspreadsheet.js:17: ERROR - actual parameter 1 of
- example.createNewSpreadsheetWithRows does not match formal parameter
- found : {title: string}
- required: { author : (string|undefined), title : (null|string),
- numRows : (number|undefined) }
- example.createNewSpreadsheetWithRows({title: '2010 Taxes'});
- ^
- 1 error(s), 0 warning(s), 86.7% typed
- /**
- * Creates a new spreadsheet.
- * @param {Object} properties supports the following options:
- * author (string): email address of the spreadsheet creator
- * title (string): title of the spreadsheet
- * numRows (number): number of rows the spreadsheet should have
- * @notypecheck
- */
- example.createNewSpreadsheetWithRows = function(properties) {
- var author = properties.author || 'bolinfest@gmail.com';
- var title = properties.title || 'New Spreadsheet';
- var numRows = properties.numRows || 1024;
- // Create a new spreadsheet using author, title, and numRows...
- };
- // This no longer results in a type checking error from the Compiler.
- example.createNewSpreadsheetWithRows({title: '2010 Taxes'});
- spreadsheet-without-notypecheck.js:13: ERROR - Property
- author never defined on Object
- var author = properties.author || 'bolinfest@gmail.com';
- ^
- spreadsheet-without-notypecheck.js:15: ERROR - Property
- numRows never defined on Object
- var numRows = properties.numRows || 1024;
- ^
- 2 error(s), 0 warning(s), 86.3% typed
可选参数
在javascript函数中,如果一个方法有一个参数,当调用时没有传参数给方法,这个参数的值为undefined,因此,对一boolean型参数默认值一般为false.下面的方法将会使用默认值为true:
- // DO NOT DO THIS: optional boolean parameters should default to false
- /**
- * @param {boolean=} isRefundable Defaults to true.
- * @param {number=} maxPrice Defaults to 1000.
- */
- example.buyTicket = function(isRefundable, maxPrice) {
- // goog.isNumber() is introduced in the next chapter.
- maxPrice = goog.isNumber(maxPrice) ? maxPrice : 1000;
- isRefundable = (isRefundable === undefined) ? true : isRefundable;
- if (isRefundable) {
- example.buyRefundableTicket(maxPrice);
- } else {
- example.buyNonRefundableTicket(maxPrice);
- }
- };
- /**
- * @param {boolean=} isNonRefundable Defaults to false.
- * @param {number=} maxPrice Defaults to 1000.
- */
- example.buyTicket = function(isNonRefundable, maxPrice) {
- maxPrice = goog.isNumber(maxPrice) ? maxPrice : 1000;
- if (isNonRefundable) {
- example.buyNonRefundableTicket(maxPrice);
- } else {
- example.buyRefundableTicket(maxPrice);
- }
- };
不定数量参数
Closure的方法也支持不定数量参数。即使这些参数没有名字,他也也能够通过方法中的arguments对象访问,编译器要求使用@param标签注解不定参数中的第一个,其它参数的注解是...,其类型表达式是可选的。
- /**
- * @param {string} category
- * @param {...} purchases
- * @return {number}
- */
- example.calculateExpenses = function(category, purchases) {
- // Note that purchases is never referenced within this function body.
- // It primarily exists so it can be annotated with {...} for the Compiler,
- // though it could be used in this function body to refer to the first
- // variable parameter, if it exists. It is identical to arguments[1].
- var sum = 0;
- // Initialize i to 1 instead of 0 because the first element in arguments is
- // the "category" parameter, which is not part of the sum.
- for (var i = 1; i < arguments.length; ++i) {
- sum += arguments[i];
- }
- alert('The total spent on ' + category + ' is ' + sum + ' dollars.');
- return sum;
- };
- // The Compiler will issue an error because "category" is a required parameter.
- example.calculateExpenses();
- // The Compiler will not issue an error for either of the following because 0
- // or more parameters can be specified after the required "category" parameter.
- example.calculateExpenses('breakfast');
- example.calculateExpenses('nachos', 25, 32, 11, 40, 12.50);
- * @param {...number} purchases
除了用@param标注注解注解参数外,也可以注解函数的可选和可变参数:
- /**
- * @param {function(string, string=)} f Function that takes a string and
- * optionally one other string. example.createNewSpreadsheet() satisfies
- * this type expression.
- * @param {function(string, ...[number]}: number} g Function that takes a string
- * and a variable number of number parameters and returns a number.
- * example.calculateExpenses() satisfies this type expression. Square
- * brackets for the variable parameter type are required when defining
- * the type of a variable parameter as an argument to a function type.
- */
- /**
- * @param {string} author
- * @param {string=} opt_title Defaults to 'New Spreadsheet'.
- */
- example.createNewSpreadsheet = function(author, opt_title) {
- // Now this function has both 'title' and 'opt_title' variables in scope.
- var title = opt_title || 'New Spreadsheet';
- // Create a new spreadsheet using author and title (but not opt_title)...
- };
字类型和类型转换
在Closure中用类型表达式指定类型。如果T在的所有属性都属于S的话,可以说S是T的字类型。在这种情况下,类型T中的Javascript对象中的属性不是用键-值对表示的。但是类型T以应该被声明。例如,下面的两种类型:
- /** @typedef {{year: number, month: number, date: number}} */
- example.Date;
- /** @typedef {{year: number, month: number, date: number,
- hour: number, minute: number, second: number}} */
- example.DateTime;
在Closure类型系统中,example.DateTime应该为example.Date的子类型,因为example.DateTime包含example.Date中的所有属性。由于有父子关系,编译器允许所有参数为example.Date的地主用example.DateTime替换。如果反过来却不可以。因为 example.DateTime有3个 example.Date 没有的属性,因此 example.Date 不是example.DateTime的子类型。
当一个函数类型中的参数中一个类是另一个类的子类时,他们的关系和上面的相反:
- /** @typedef {function(example.Date)} */
- example.DateFunction;
- /** @typedef {function(example.DateTime)} */
- example.DateTimeFunction;
- /** @type {example.DateFunction} */
- example.writeDate = function(date) {
- document.write(date.year + '-' + date.month + '-' + date.date);
- };
- /** @type {example.DateTimeFunction} */
- example.writeDateTime = function(dateTime) {
- document.write(dateTime.year + '-' + dateTime.month + '-' + dateTime.date +
- ' ' + dateTime.hour + ':' + dateTime.minute + ':' + dateTime.second);
- };
- /**
- * @param {example.DateFunction} f
- * @param {example.Date} date
- */
- example.applyDateFunction = function(f, date) { f(date); };
- /**
- * @param {example.DateTimeFunction} f
- * @param {example.DateTime} dateTime
- */
- example.applyDateTimeFunction = function(f, dateTime) { f(dateTime); };
- /** @type {example.Date} */
- var date = {year: 2010, month: 12, date: 25};
- /** @type {example.DateTime} */
- var dateTime = {year: 2010, month: 12, date: 25, hour: 12, minute: 13, second: 14};
- / Writes out: 2010-12-25
- example.applyDateFunction(example.writeDate, date
- // Writes out: 2010-12-25 12:13:14
- example.applyDateTimeFunction(example.writeDateTime, dateTime);
- // Writes out: 2010-12-25
- example.applyDateTimeFunction(example.writeDate, dateTime);
- // Writes out an ill-formed DateTime: 2010-12-25 undefined:undefined:undefined
- example.applyDateFunction(example.writeDateTime, date);
Colsure编译器用这样的基本原则来确定一个类型能否成为另一个的子类型。不幸的是,一些替换可能是安全的,但 Closure编译不能保证其安全性。例如,如果程序中声明一个非空属性,但编译器却不这么认为:
- /**
- * @param {!Element} element
- * @param {string} html
- */
- example.setInnerHtml = function(element, html) {
- element.innerHTML = html;
- };
- var greetingEl = document.getElementById('greeting');
- // If type checking is enabled, the Compiler will issue an error that greetingEl
- // could be null whereas example.setInnerHTML() requires a non-null Element.
- example.setInnerHTML(greetingEl, 'Hello world!');
编译器提供了一种@type中的特殊用法来解决此问题:
- var greetingEl = /** @type {!Element} */ (document.getElementById('greeting'));
- // Because of the use of @type on the previous line, the Compiler will consider
- // greetingEl as an object of type {!Element} going forward. This eliminates the
- // error previously issued by the Compiler.
- example.setInnerHTML(greetingEl, 'Hello world!');
TYPE_EXPRESSION */封装一个表达式,编译器将结果和 TYPE_EXPRESSION声明的进行比较。实际上,子类型将在第5章介绍。父类和子类的关系也会改变父类弄和子类型。
这有点像C语言中的类型转换,但和Java不一样,因为Java会在运行时抛出ClassCastException错误,在javascript和c中转换错误不会有警告。程序也会继续执行。为了避免这些错误,应尽量不要使用类型转换。
All类型
有一种特殊的类型叫ALL类型,它请允许任何类型:
- /**
- * @param {*} obj The object to serialize.
- * @throw Error if obj cannot be serialized
- * @return {string} A JSON string representation of the input
- */
- goog.json.serialize = function(obj) { /* ... */ };
JSDoc标记不是全处理类型
JSDoc不光是用来处理类型,也可以用来注解常量,过期变量,和licensing信息。其它注解标记将在第5张介绍,如:@extends, @implements, @inheritDoc, @interface, @override, @private, 和@protected.
常量
@const用来声明一个常量:
- /**
- * @type {number}
- * @const
- */
- var MAX_AMPLIFIER_VOLUME = 11;
因为MAX_AMPLIFIER_VOLUME是一个常量,如果有地方重新声明它的话,编译器会抛出错误。编译器为了使代码更小,会将其内连。
如果用@define注解,编译器能在编译时声明变量,只能在boolean,string或number变量使用这个标记:
- /** @define {boolean} */
- example.USE_HTTPS = true;
- /** @define {string} */
- example.HOSTNAME = 'example.com';
- /** @define {number} */
- example.PORT = 80;
- /**
- * @type {string}
- * @const
- */
- example.URL = 'http' + (example.USE_HTTPS ? 's' : '') + '://' +
- example.HOSTNAME + ':' + example.PORT + '/';
- java -jar compiler.jar --js example.js --define example.USE_HTTPS=false \
- --define example.HOSTNAME='localhost' --define example.PORT=8080
过期变量
像Java一样,@deprecated用来标识不再使用的方法和属性:
- /** @deprecated Use example.setEnabled(true) instead. */
- example.enable = function() { /*...*/ };
- /** @deprecated Use example.setEnabled(false) instead. */
- example.disable = function() { /*...*/ };
版权信息
版权信息用来出现在编译后的Javascript文件开始,用@license或@preserve标注:
- /**
- * @preserve Copyright 2010 SomeCompany.
- * The license information (such as Apache 2.0 or MIT) will also be included
- * here so that it is guaranteed to appear in the compiled output.
- */
这些是真正必要的吗?
由于在注解中大量用到javadoc工具和java注解关键字,很多人提出:"这是在将javascript转换成java!"
确实在Closure有将Java和其它一些语言的概念转换为javascript,比如类型检查,信息隐藏和继承,对这些功能的支持也使得增加javascript代码和需要更多内存。
不过放心,这重量级的JSDoc在Closure中是可选的。编译器也能编译没有注解的代码,只有注解后的代码在编译时能发现更多错误和编译成更高效代码。
注解也能最后生成一个文档,像gmail和google map这样复杂的应用,有很多开发者超过一年时间来开发,注解可以让开发的代码能更好的维护。