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函数声明:
-
-
-
-
-
-
-
-
-
-
- goog.bind = function(fn, selfObj, var_args) {
-
- };
每一个参数和返回类型都类型和描述写明。java开发者可能会马上觉得这和javadoc语法很相似,实际上Closure代码是用JSDoc文档化的。它是基于javados的标准的专门针对javascript的语法。java和javascript都支持下面这两种注释格式:
- // 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;
编译器也支持枚举类型。枚举是键值对。和Java一样,枚举的名字是单一的值,但值可以是任何东西。枚举类型用@enum注解标注,并且每一个值都必需是正确的枚举类型:
-
- example.CardinalDirection = {
- NORTH: Math.PI / 2,
- SOUTH: 3 * Math.PI / 2,
- EAST: 0,
- WEST: Math.PI
- };
-
- example.CardinalDirectionName = {
- NORTH: 'N',
- SOUTH: 'S',
- EAST: 'E',
- WEST: 'W'
- };
当一个函数声明要使用this关键字的时候,@this注解用来声明这种类型。这在Jquery对一系列Dom元素频繁使用回调方法时中很常用 。下面的例子是Jquery中用来删除所有<img>元素的:
-
- example.removeElement = function() { this.parentNode.removeChild(this); };
-
-
- $('IMG').each(example.removeElement);
Closure类型系统不创建新类就能引入新的类型,像Java这样的强类型语言很难区别类开类型。@typedef注解用来声明一个复杂类型的别名:
上面例子中,exapmle.Point是{x:number,y:number}的别名,用来记录一个包含数字属性x,y的一个对象。现在可以用example.Point来替换{x:number,y:number},这样函数将更简洁:
-
-
-
-
-
-
-
- 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泛型。
目前只有Array和Object在参数语法中支持。
许多浏览器中的对象,比如document,类型名字和W3C DOM规范中定义的名字一样,document的类型对象是HTMLDocument,DOM元素和节点的类型对象是Element和Node.编译器知道引用的外部文件中所引用的类型。
一般情况下,一个外部声明类和接口的Javascript文件不包含实现部分,它的实现会在运行时提供。比如你不用实现document对象,它会由浏览器提供,但是编译器需要知道HtmlDocument对象的声明才能检查document中方法调用时参数的正确性。
开发者也可以声明自己的类型,最简单的方式是用@typedef标示,但是,最一般的方法是声明一个新类,接口或枚举来创建新类型。前面讲了用枚举来创建新类型,第5章将要介绍创建类和接口来创建新类型。类和接口声明可以用@constructor和@interface注解标识。Closure Library 中许多文件都声明了类和接口,因此可以用作类型表达式。
另两种类型是null和undefined。管道字符|用来连接联合类型,联合类型用来表明参数可以有多个类型:
-
-
-
-
-
-
-
-
- example.convertStringToInteger = function(str) {
- var value = parseInt(str, 10);
- return isNaN(value) ? 0 : value;
- };
骑过用联合类型, example.convertStringToInteger将可以接受string, null或undefined三种类型,但它返回一个非null的数字。因为参数是否非空很常见,因此有一个快捷方式用来标明联合类型是否包含null:
除了基本类型的所有类型,如Object,Array和HTMLDocument默认都可以为空,这些类型统称为对象类型,因此?前缀对对象类型是多余的:
如果要声明一个非空对象对开,可以用!前缀:
同样的,因为基本类型默认是非空的,因此!前缀是多余的,因此,{!number}和{number}都是非空数字。
函数类型
在javascript中,函数可以作为参数传递给别一函数,因此Closure类型系统中有丰富的语法对其提供支持:
这个例子只是简单声明了一个函数参数,并没有提供它的参数和返回值声明。使用Function注解,类型不能提供更详细的功能,近来,由于对类型表达式的加强,function注解用来描述函数表达式,它能提供更详细的参数说明,函数类型的参数可以用括号包围:
函数表达式的返回值可以用冒号加上类型表示:
最后this类型也可以用this:再加上类型:
尽管上面看上去包括this:共有3个参数,但实际上只有两个,因为this:是被类型系统所用的特殊参数。
记录类型
像example.Point例子中所示,类型表达式可能是一个包含属性值的对象,这样的类型叫记录类型。记录类型声明和javascript中的对象声明一样,只是它的值可以不声明或者声明为类型表达式:
- ** @typedef {{row: number, column: string, piece}} */
- example.chessSquare;
上面例子用@typedef声明example.chessSquare为一个对象,有一个number类型的属性row和一个string类型的属性column,piece没有类型声明,因此可以为任何内容。
就像以前提到的一样,记录类型的值可以是任何类型,包括另一个记录类型,函数类型,也可以是自身的引用:
-
- example.chessMove;
-
-
- example.chessBoard;
值得注意的是,记录类型中的属性并不是对象拥有的所有属性,他只列出了最少必须的属性。如下面的例子,threeDimensionalPoint能传递给 example.translate(),因为它满足example.Point类型声明:
-
-
-
-
-
-
- var threeDimensionalPoint = { x: 3, y: 4, z: 5};
-
-
- var twoDimensionalPoint = example.translate(threeDimensionalPoint, -2);
特殊的@param类型
指定可选参数
在javascript中,function通常提供可选参数。通过指定一参数是可选的,在没有传入所有参数时编译器不会发出警告。通过在一个类型表达式的后面加一下=后缀就表明参数是可选的:
-
-
-
-
-
- example.createNewSpreadsheet = function(author, title) {
- title = title || 'New Spreadsheet';
-
- };
因为title参数是可选的,因此example.createNewSpreadsheet('
[email protected]').在编译不会有警告。如果两个参数都是必须的,将会有如下错误:
- 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('[email protected]');
- ^
- 1 error(s), 0 warning(s), 86.6% typed
尽管一个方法中可以有任意多个可选参数,但是可选参数不应改出现在必须参数之前,如果出现在之前,代码必须写成如下形式:
-
-
-
-
-
- example.createNewSpreadsheet = function(title, author) {
- if (arguments.length == 1) {
- author = title;
- title = undefined;
- }
-
- };
这样的代码很难跟踪和维护。如果有大量的可靠参数,最好的方法是将其它移到一个必须的对象参数中:
- **
- * @param {{author: (string|undefined), title: (string|undefined),
- * numRows: (number|undefined)
- */
- example.createNewSpreadsheetWithRows = function(properties) {
- var author = properties.author || '[email protected]';
- var title = properties.title || 'New Spreadsheet';
- var numRows = properties.numRows || 1024;
-
- };
-
- example.createNewSpreadsheetWithRows({title: '2010 Taxes'});
不幸的是,上面的代码却不能工作,因为编译不能将未声明值当成已声明值undefined,编译器将发出如下类型检查错误:
- 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
因为目前在Closure类型系统中我们还不能声明一个可选对象,我们变通的方法是声明一个泛型对象并在文档中描述其属性:
-
-
-
-
-
-
-
-
- example.createNewSpreadsheetWithRows = function(properties) {
- var author = properties.author || '[email protected]';
- var title = properties.title || 'New Spreadsheet';
- var numRows = properties.numRows || 1024;
-
- };
-
- example.createNewSpreadsheetWithRows({title: '2010 Taxes'});
@notypecheck注解告诉编译器对example.createNewSpreadsheetWithRows()忽略类型检查,没有它,编译器将发出如下错误:
- spreadsheet-without-notypecheck.js:13: ERROR - Property
- author never defined on Object
- var author = properties.author || '[email protected]';
- ^
- 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:
-
-
-
-
-
- example.buyTicket = function(isRefundable, maxPrice) {
-
- maxPrice = goog.isNumber(maxPrice) ? maxPrice : 1000;
- isRefundable = (isRefundable === undefined) ? true : isRefundable;
- if (isRefundable) {
- example.buyRefundableTicket(maxPrice);
- } else {
- example.buyNonRefundableTicket(maxPrice);
- }
- };
尽管不是必须要求,null常常被用来表示可选参数的值,当调用example.buyTicket(null,250)时,isRefundable当为false,而不是默认的true.为避免这种问题,最好将可选的boolean默认值保持为false,如果默认值被使用,可用undefined代替。
-
-
-
-
- example.buyTicket = function(isNonRefundable, maxPrice) {
- maxPrice = goog.isNumber(maxPrice) ? maxPrice : 1000;
- if (isNonRefundable) {
- example.buyNonRefundableTicket(maxPrice);
- } else {
- example.buyRefundableTicket(maxPrice);
- }
- };
不定数量参数
Closure的方法也支持不定数量参数。即使这些参数没有名字,他也也能够通过方法中的arguments对象访问,编译器要求使用@param标签注解不定参数中的第一个,其它参数的注解是...,其类型表达式是可选的。
-
-
-
-
-
- example.calculateExpenses = function(category, purchases) {
-
-
-
-
- var sum = 0;
-
-
- for (var i = 1; i < arguments.length; ++i) {
- sum += arguments[i];
- }
- alert('The total spent on ' + category + ' is ' + sum + ' dollars.');
- return sum;
- };
-
- example.calculateExpenses();
-
-
- example.calculateExpenses('breakfast');
- example.calculateExpenses('nachos', 25, 32, 11, 40, 12.50);
因为所有可变参数都是数字,所以purchases可以用如下注解:
- * @param {...number} purchases
这可以更明确的注解 example.calculateExpenses()方法。
除了用@param标注注解注解参数外,也可以注解函数的可选和可变参数:
在Closure Library中,可选参数通常用opt_前缀命名,可变参数长度通常用var_args表示。这是Google在类型系统中的代码习惯。在新代码中使用opt_前缀是应该鼓励的,这会使代码更可读:
-
-
-
-
- example.createNewSpreadsheet = function(author, opt_title) {
-
- var title = opt_title || 'New Spreadsheet';
-
- };
可变参数使用var_arg将减少很多麻烦,也可也选择一个能更好描述功能的名字,如purchases in example.calculateExpenses()中的那样。
字类型和类型转换
在Closure中用类型表达式指定类型。如果T在的所有属性都属于S的话,可以说S是T的字类型。在这种情况下,类型T中的Javascript对象中的属性不是用键-值对表示的。但是类型T以应该被声明。例如,下面的两种类型:
-
- example.Date;
-
-
- 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的子类型。
当一个函数类型中的参数中一个类是另一个类的子类时,他们的关系和上面的相反:
-
- example.DateFunction;
-
- example.DateTimeFunction;
上面情况下,example.DateFunction是example.DateTimeFunction的子类,因为example.DateTimeFunction包含example.DateFunction中的所有属性,下面是一个更详细的例子:
-
- example.writeDate = function(date) {
- document.write(date.year + '-' + date.month + '-' + date.date);
- };
-
- example.writeDateTime = function(dateTime) {
- document.write(dateTime.year + '-' + dateTime.month + '-' + dateTime.date +
- ' ' + dateTime.hour + ':' + dateTime.minute + ':' + dateTime.second);
- };
-
-
-
-
- example.applyDateFunction = function(f, date) { f(date); };
-
-
-
-
- example.applyDateTimeFunction = function(f, dateTime) { f(dateTime); };
-
- var date = {year: 2010, month: 12, date: 25};
-
- var dateTime = {year: 2010, month: 12, date: 25, hour: 12, minute: 13, second: 14};
下面两种写法能正确运行:
- / Writes out: 2010-12-25
- example.applyDateFunction(example.writeDate, date
-
- example.applyDateTimeFunction(example.writeDateTime, dateTime);
现在看看 example.DateFunction为 example.DateTimeFunction子类下发生的情况:
-
- example.applyDateTimeFunction(example.writeDate, dateTime);
尽管没有像上例子有日期输出,但也没有错误出现。如果 example.DateTimeFunction 是 example.DateFunction的子类:
-
- example.applyDateFunction(example.writeDateTime, date);
这个程序会产生不正确的结果,因为 example.DateTimeFunction 不是 example.DateFunction的子类型。
Colsure编译器用这样的基本原则来确定一个类型能否成为另一个的子类型。不幸的是,一些替换可能是安全的,但 Closure编译不能保证其安全性。例如,如果程序中声明一个非空属性,但编译器却不这么认为:
-
-
-
-
- example.setInnerHtml = function(element, html) {
- element.innerHTML = html;
- };
- var greetingEl = document.getElementById('greeting');
-
-
- example.setInnerHTML(greetingEl, 'Hello world!');
在externs/gecko_dom.js中,getElementById()方法传入下个string参数并返回HTMLElement。因为返回类型为!HTMLElement,这个方法也可能返回null。开发人员能保证些方法返回非空,但编译器却不能。
编译器提供了一种@type中的特殊用法来解决此问题:
- var greetingEl = (document.getElementById('greeting'));
-
-
-
- example.setInnerHTML(greetingEl, 'Hello world!');
这是一个类型转换的例子,编译器将一种类型转换为另一种类型。在编译过程中,对象的数据没有任何改变:仅仅是对象中与编译类型相关的才会变化。通过用/** @type
TYPE_EXPRESSION */封装一个表达式,编译器将结果和 TYPE_EXPRESSION声明的进行比较。实际上,子类型将在第5章介绍。父类和子类的关系也会改变父类弄和子类型。
这有点像C语言中的类型转换,但和Java不一样,因为Java会在运行时抛出ClassCastException错误,在javascript和c中转换错误不会有警告。程序也会继续执行。为了避免这些错误,应尽量不要使用类型转换。
All类型
有一种特殊的类型叫ALL类型,它请允许任何类型:
-
-
-
-
-
- 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用来声明一个常量:
-
-
-
-
- var MAX_AMPLIFIER_VOLUME = 11;
因为MAX_AMPLIFIER_VOLUME是一个常量,如果有地方重新声明它的话,编译器会抛出错误。编译器为了使代码更小,会将其内连。
如果用@define注解,编译器能在编译时声明变量,只能在boolean,string或number变量使用这个标记:
-
- example.USE_HTTPS = true;
-
- example.HOSTNAME = 'example.com';
-
- example.PORT = 80;
-
-
-
-
- example.URL = 'http' + (example.USE_HTTPS ? 's' : '') + '://' +
- example.HOSTNAME + ':' + example.PORT + '/';
在上面例子中,example.URL可能为 https://example.com:80/,但开发时可能不支持HTTPS,因此用--define如下编译:
- java -jar compiler.jar --js example.js --define example.USE_HTTPS=false \
- --define example.HOSTNAME='localhost' --define example.PORT=8080
编译后,example.URL值为:http://localhost:8080/.
过期变量
像Java一样,@deprecated用来标识不再使用的方法和属性:
-
- example.enable = function() { };
-
- example.disable = function() { };
当过期变量被使用时编译器会发出警告。
版权信息
版权信息用来出现在编译后的Javascript文件开始,用@license或@preserve标注:
这些是真正必要的吗?
由于在注解中大量用到javadoc工具和java注解关键字,很多人提出:"这是在将javascript转换成java!"
确实在Closure有将Java和其它一些语言的概念转换为javascript,比如类型检查,信息隐藏和继承,对这些功能的支持也使得增加javascript代码和需要更多内存。
不过放心,这重量级的JSDoc在Closure中是可选的。编译器也能编译没有注解的代码,只有注解后的代码在编译时能发现更多错误和编译成更高效代码。
注解也能最后生成一个文档,像gmail和google map这样复杂的应用,有很多开发者超过一年时间来开发,注解可以让开发的代码能更好的维护。