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函数声明:
每一个参数和返回类型都类型和描述写明。java开发者可能会马上觉得这和javadoc语法很相似,实际上Closure代码是用JSDoc文档化的。它是基于javados的标准的专门针对javascript的语法。java和javascript都支持下面这两种注释格式:在Java中,包含javadoc的注释必须用/***/声明。这通常叫着文档注释。像javadoc一样,javascript也用/** 和/*包含JSDoc注释。许多编辑器支持对/*..*/和/**...*/用不同方式显示这两种注释。这会底帮助将普通注释块放在文档注释块的错误。
就像你妈妈常常告诉你的一样,它真正依赖其中的内容,在JSDoc中,文档注释中真正依赖的是JSDoc标记。JSDOC标记@character,紧接是可识别的JSDoc标记名,具体值可以是任何东西,直到下一个标记开始。
所有本意中的标记都是块标记,必须从一行开始,下面的块就不正确:
正确写法如下: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注解标注:
编译器也支持枚举类型。枚举是键值对。和Java一样,枚举的名字是单一的值,但值可以是任何东西。枚举类型用@enum注解标注,并且每一个值都必需是正确的枚举类型: 当一个函数声明要使用this关键字的时候,@this注解用来声明这种类型。这在Jquery对一系列Dom元素频繁使用回调方法时中很常用 。下面的例子是Jquery中用来删除所有<img>元素的: Closure类型系统不创建新类就能引入新的类型,像Java这样的强类型语言很难区别类开类型。@typedef注解用来声明一个复杂类型的别名: 上面例子中,exapmle.Point是{x:number,y:number}的别名,用来记录一个包含数字属性x,y的一个对象。现在可以用example.Point来替换{x:number,y:number},这样函数将更简洁:在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将可以接受string, null或undefined三种类型,但它返回一个非null的数字。因为参数是否非空很常见,因此有一个快捷方式用来标明联合类型是否包含null:
除了基本类型的所有类型,如Object,Array和HTMLDocument默认都可以为空,这些类型统称为对象类型,因此?前缀对对象类型是多余的: 如果要声明一个非空对象对开,可以用!前缀: 同样的,因为基本类型默认是非空的,因此!前缀是多余的,因此,{!number}和{number}都是非空数字。 函数类型
在javascript中,函数可以作为参数传递给别一函数,因此Closure类型系统中有丰富的语法对其提供支持:
这个例子只是简单声明了一个函数参数,并没有提供它的参数和返回值声明。使用Function注解,类型不能提供更详细的功能,近来,由于对类型表达式的加强,function注解用来描述函数表达式,它能提供更详细的参数说明,函数类型的参数可以用括号包围: 函数表达式的返回值可以用冒号加上类型表示: 最后this类型也可以用this:再加上类型: 尽管上面看上去包括this:共有3个参数,但实际上只有两个,因为this:是被类型系统所用的特殊参数。 记录类型
像example.Point例子中所示,类型表达式可能是一个包含属性值的对象,这样的类型叫记录类型。记录类型声明和javascript中的对象声明一样,只是它的值可以不声明或者声明为类型表达式:
上面例子用@typedef声明example.chessSquare为一个对象,有一个number类型的属性row和一个string类型的属性column,piece没有类型声明,因此可以为任何内容。就像以前提到的一样,记录类型的值可以是任何类型,包括另一个记录类型,函数类型,也可以是自身的引用:
值得注意的是,记录类型中的属性并不是对象拥有的所有属性,他只列出了最少必须的属性。如下面的例子,threeDimensionalPoint能传递给 example.translate(),因为它满足example.Point类型声明: 特殊的@param类型指定可选参数
在javascript中,function通常提供可选参数。通过指定一参数是可选的,在没有传入所有参数时编译器不会发出警告。通过在一个类型表达式的后面加一下=后缀就表明参数是可选的:
因为title参数是可选的,因此example.createNewSpreadsheet('bolinfest@gmail.com').在编译不会有警告。如果两个参数都是必须的,将会有如下错误: 尽管一个方法中可以有任意多个可选参数,但是可选参数不应改出现在必须参数之前,如果出现在之前,代码必须写成如下形式: 这样的代码很难跟踪和维护。如果有大量的可靠参数,最好的方法是将其它移到一个必须的对象参数中: 不幸的是,上面的代码却不能工作,因为编译不能将未声明值当成已声明值undefined,编译器将发出如下类型检查错误: 因为目前在Closure类型系统中我们还不能声明一个可选对象,我们变通的方法是声明一个泛型对象并在文档中描述其属性: @notypecheck注解告诉编译器对example.createNewSpreadsheetWithRows()忽略类型检查,没有它,编译器将发出如下错误: 尽管用函数包含很多可选参数比对象包含可选参数玩高效,但它不能通过编译器检查,将来可能有新的注解来完成此功能 。可选参数
在javascript函数中,如果一个方法有一个参数,当调用时没有传参数给方法,这个参数的值为undefined,因此,对一boolean型参数默认值一般为false.下面的方法将会使用默认值为true:
尽管不是必须要求,null常常被用来表示可选参数的值,当调用example.buyTicket(null,250)时,isRefundable当为false,而不是默认的true.为避免这种问题,最好将可选的boolean默认值保持为false,如果默认值被使用,可用undefined代替。不定数量参数
Closure的方法也支持不定数量参数。即使这些参数没有名字,他也也能够通过方法中的arguments对象访问,编译器要求使用@param标签注解不定参数中的第一个,其它参数的注解是...,其类型表达式是可选的。
因为所有可变参数都是数字,所以purchases可以用如下注解: 这可以更明确的注解 example.calculateExpenses()方法。除了用@param标注注解注解参数外,也可以注解函数的可选和可变参数:
在Closure Library中,可选参数通常用opt_前缀命名,可变参数长度通常用var_args表示。这是Google在类型系统中的代码习惯。在新代码中使用opt_前缀是应该鼓励的,这会使代码更可读: 可变参数使用var_arg将减少很多麻烦,也可也选择一个能更好描述功能的名字,如purchases in example.calculateExpenses()中的那样。 字类型和类型转换
在Closure中用类型表达式指定类型。如果T在的所有属性都属于S的话,可以说S是T的字类型。在这种情况下,类型T中的Javascript对象中的属性不是用键-值对表示的。但是类型T以应该被声明。例如,下面的两种类型:
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的子类型。
当一个函数类型中的参数中一个类是另一个类的子类时,他们的关系和上面的相反:
Colsure编译器用这样的基本原则来确定一个类型能否成为另一个的子类型。不幸的是,一些替换可能是安全的,但 Closure编译不能保证其安全性。例如,如果程序中声明一个非空属性,但编译器却不这么认为:
在externs/gecko_dom.js中,getElementById()方法传入下个string参数并返回HTMLElement。因为返回类型为!HTMLElement,这个方法也可能返回null。开发人员能保证些方法返回非空,但编译器却不能。
编译器提供了一种@type中的特殊用法来解决此问题:
这是一个类型转换的例子,编译器将一种类型转换为另一种类型。在编译过程中,对象的数据没有任何改变:仅仅是对象中与编译类型相关的才会变化。通过用/** @type
TYPE_EXPRESSION */封装一个表达式,编译器将结果和 TYPE_EXPRESSION声明的进行比较。实际上,子类型将在第5章介绍。父类和子类的关系也会改变父类弄和子类型。
这有点像C语言中的类型转换,但和Java不一样,因为Java会在运行时抛出ClassCastException错误,在javascript和c中转换错误不会有警告。程序也会继续执行。为了避免这些错误,应尽量不要使用类型转换。
All类型
有一种特殊的类型叫ALL类型,它请允许任何类型:
因为All类型比{Object}更通用,因为obj也要要用{Object}声明,通过All类型,可以 goog.json.serialize()传入基本类型和undefined。
JSDoc标记不是全处理类型
JSDoc不光是用来处理类型,也可以用来注解常量,过期变量,和licensing信息。其它注解标记将在第5张介绍,如:@extends, @implements, @inheritDoc, @interface, @override, @private, 和@protected.
常量
@const用来声明一个常量:
因为MAX_AMPLIFIER_VOLUME是一个常量,如果有地方重新声明它的话,编译器会抛出错误。编译器为了使代码更小,会将其内连。
如果用@define注解,编译器能在编译时声明变量,只能在boolean,string或number变量使用这个标记:
在上面例子中,example.URL可能为 https://example.com:80/,但开发时可能不支持HTTPS,因此用--define如下编译:
编译后,example.URL值为:http://localhost:8080/.
过期变量
像Java一样,@deprecated用来标识不再使用的方法和属性:
当过期变量被使用时编译器会发出警告。
版权信息
版权信息用来出现在编译后的Javascript文件开始,用@license或@preserve标注:
这些是真正必要的吗?
由于在注解中大量用到javadoc工具和java注解关键字,很多人提出:"这是在将javascript转换成java!"
确实在Closure有将Java和其它一些语言的概念转换为javascript,比如类型检查,信息隐藏和继承,对这些功能的支持也使得增加javascript代码和需要更多内存。
不过放心,这重量级的JSDoc在Closure中是可选的。编译器也能编译没有注解的代码,只有注解后的代码在编译时能发现更多错误和编译成更高效代码。
注解也能最后生成一个文档,像gmail和google map这样复杂的应用,有很多开发者超过一年时间来开发,注解可以让开发的代码能更好的维护。