就像第一章例子所描述一样,JavaScript文件通过base.js引导Closure库,在base.js中创建了goog根对象,所有其它的Closure库属性都存在于对象下。因为定义在base.js 中的所有函数对使用Closure库的任何javascript库都是可用的,因此,这称为基本Closure库。本章会详细讲解这些基本内容。
通过对base.js中的api的讲述,本章也会引领读者理解Closure库高级的的设计理念。每一小节讲述base.js中的一个功能
依赖管理
第一章的“Hello World”例子演示了goog.provide()和goog.require(),它们用来维护Closure中的依赖关系。本节讲述库中依赖系统的具体功能实现。
calcdeps.py
在Java中,可以把相互引用的类编译到一起,javascript不一样,由于javascript依赖于页面中<script>标签的线性引用顺序,因此不能包含前置声明。为了在没有前置声明的情况下保证一个顺序,clacdeps.py使用goog.require()和goog.provide()来输入产生逻辑顺序,并在文件之前加载。如果输入中有循环依赖,clacdeps.py会抛出一个错误,calcdeps.py的官方问题这个里:http://code.google.com/closure/library/docs/calcdeps.html.
calcdeps.py通过--output_mode参数可以产生四种类型输出:
script: 根据依赖顺序将javascript文件中的内容连合到一起,它能够通过<script>标签加载,但它比较大。
list: 根据依赖顺序产生一个javascript文件列表,这结一些工具通过每次处理一个javascript文件很有用
deps: 产生一系统函数goog.addDependency()调用,这在本章后面将讲述,这被用来在运行地产生依赖图。
compiled: 产生一个编译版本,因此需要安装Closure编译器,这将在第12章详细介绍。
clacdeps.py有三种输入类型,每种都有各自的参数,每个参数都可以出现多次:
--input: 用来指定产生输入时需要依赖的文件或目录
--path: 用来指定--input指定的文件需要依赖的文件或目录。在deps模式下,每个文件的依赖都支持--path参数,一般来说,在script,list,呀compiled模式下,只有通过--path指定的文件只会在输入文件中存在,例如:如果--path指定的目录包含所有Closure库,一个文件通过--input指定goog.string,因此只有goog/string/string.js会出现在输出中,而不是全部Closure库。
--dep: 只在deps模式中存在,用来表示已经提供依赖关系的路径,像Closure库,那的deps.js已经生成。通过--dep指定的文件不会被包含在输出中。
还有一个--exclude参数,来来排除在--input和--path中指定目录中的文件或子目录。
实际上,最常用的模式是deps和compiled.deps模式常用来加载本地文件用来测试和开发,compiled模式常用来编译发布最终产品。
goog.global
goog.global是加载Closure库的frame的window对象的别名,因为,在浏览器中,windo可以在javascriipt执行的任务地方被引用,在15.1章节中的ES5规范会讲到,全局对象是一个声明如NaN,Infinity,undefined,eval(),parseInt()等等全局属性的一个对象。全局对象是可变的,任何值都可以做为goog.global的一个属性。window中声明的属性也可能通过ES5中规范的全局对象访问:
goog.global详细介绍
如前面所讲,全局对象是window,在base.js中,goog.global对指定到this中,在浏览器中,this和window都在全局域中代表同一个对象。因此也可以用goog.global代替。
但是,Closure工具可以支持ECMAScript规范,而不仅是javascript,也支持第三方ECMAScript规范,这规范在现代浏览器中和javascript很相似。
全局变量中的this可以是window对象,也可能不是,如在Microsoft Windows Script Host或Firefix扩展中,它可能不能指定任何东西。通过this 指定给goog.global而不是将window对象指定给它,使得Closure在非浏览器环境能更好工作,也能通过编译器对其改名。
COMPILED
compiled是全局域中除goog外的另一个等级常量。像goog.require()中说明的一样,这个变量决定Closure库在执行中是如何进行依赖管理的。
许多开发都错误的将COMPILED状态用来判断是否应该包含高度信息。goog.DEBUG才是正确的用法。
goog.provide(namespace)
goog.provide()用一个字符串声明一个命名空间,由于javascript本身没有命名空间概念,因此这是一个很大进步,但要避免的是当使用多个javascript库时的命令空间冲突。
goog.provide通过点分开的名字产生命名空间,它会先从左到右依次判断该名字在全局中是否存在 ,如果存在都使用存在的,不存在才产生新的对象:
比如如果调用:goog.provide('example.of.another.namespace'),如果example.of已经存在 ,another 将直接加在其上,而不会创建一个新的example.of对象。
在Closure库中,每个Javascript中至少会有一个goog.provide调用,所有添加到这个命名空间中的元素都会被加入到此文件中,对于Java,命名空间是和文件目录一一对应的。这使得通过命名空间能够很容易定位到文件中,这种习惯在Closure中是我们推荐的。
goog.provide()背后的动机
传统上,有两个创建javascript命名空间的方法,第一种是,对同一命名空间的方法使用同一个前缀,并且别的库不能使用。例如,google在使用Closure之前所使用的Greasemonkey API,通过GM_前缀来声明方法。在以前的google map api中,使用G前缀,这很简单但是对全局命名空间却很浪费。
第二种方法,即Closure中所用的,只有一个全局对象,并声明所有方法作为那个对象的属性。一个命名空间下面再分为多个命名空间:
用点分开的命名空间和java中的包一样,这样即使用javascript中根本没有包或命名空间概念,开发者也能引用goog.array 下的各种方法。更重要的是,在全局对象下只有一个goog对象,而不像其它类库一样每个方法一个名字。这使得使用多个库时能很好地避免冲突。
但使用对象命名空间有两个缺点,第一个是增加命名空间的查找,比如调用goog.arry.sort([3,4,5]),首先解释器会先查找goog对象,然后查看其arrar属性,再查找sort属性,最后才调用方法,命名空间越长,查找越深。每二个是使用更多类型,命名空间将更长。
13章中会讲到,编译会重写javascript,并消除命名空间的属性查找。但是,它却不能消除开发者写一个很长命名空间的负担。
goog.require(namespace)
goog.require()和goog.provide()协同工作。在文件中所用的命名空间都必须有一个goog.require()调用。如果一个命名空间在provide之前require,将会出错。
goog.require()和java中的import不一样,在参数中引用的类型不用被加载,比如下面的例子, goog.math.Coordinate不需加载:
如果方法如下重写,就需要明确调用goog.requier加载 goog.math.Coordinate :
再回到第一章中的"Hello World"例子,页面中有两个<script>标签,一个指向base.js,另一个声明sayHello()方法。对于使用未编译的库,goog.require()不会出错,即使goog.provide('goog.do')根本没有被调用--这是为什么呢?
因为COMPILED默认值是false,base.js产生如下代码:
deps.js在base.js相同目录下,它包含许多 goog.addDependency()调用。这是calcdeps.py生成的用来加载Closure依赖。当COMPILED为false时,goog.require()从依赖中选择goog.dom依赖,当找到每一个依赖后,添加另一个<script>标签指向正确的文件。
每11章中讨论用Closure模板生成javascript文件,每个生成文件中都有goog.provide()调用,因此使用模板的文件也可以用goog.require()调用。
goog.addDependency(relativePath, provides, requires)
goog.addDependency()是未编译的javascript中的goog.require()查找依赖用的。当文件被编译后,goog.provide()和goog.require()被用来确定在provided之前不会被required.
当检查完成后, goog.provide() 将被具体对象替换,goog.require()会被移除。编译后,依赖关系将没有用处,当COMPILED为true时,它们会被去除。
未编译的代码依赖goog.addDependency()来加载javascript文件。如下面例子:example.view依赖example.Model:
model.js和view.js在primitive目录中,和Closure库位于同级目录。在primitives目录下,calcdeps.py用如下命令创建deps.js依赖文件:
产生 model-view-deps.js文件,内容如下:
对每一个传递给calcdeps.py的输入文件都会产生一个goog.addDependency()。它文件中的 goog.require() 和goog.provide()值。这用来在客户端生成依赖图,使得goog.require()能够加载依赖:
从上面例子可以看出,上面的依赖会动态生成<script>标签。加载如果多的文件会有很大的性能问题,这种方法只能用于开发环境,这能更好调试代码。
函数
在应用局部,函数有一个强大的功能就是预先包含一些参数,而其它的参数在函数调用时确定,在这一节中,将会讲述“当一个函数被调用时this所指定的对象”,主要讲在函数调用时this所指定的值是怎么被解释的,特别是在对call()和apply()方法调用时。
goog.partial(functionToCall, ...)
goog.partial()函数参数为一个方法名和其它一些参数,很像javascript所有函数中的call()方法,和call()方法不同的是,它用指定的参数执行所传递的方法,goog.partial()将返回一个新的函数,在这函数被调用时,会根据传的和参数执行,下面的例子中,a()和b()将会产生相同效果:
a()和b()在执行时都会弹出"Hello world!"信息,初看,好像goog.partial()仅仅是匿名函数的快捷方式,但是它还能有如下新的特性:
atLeastten()是一个用Math.max()计算10和其它参数的函数,也就是说,goog.partial()创建了atLeaseTen()方法,它不是先用参数10调用Math.max,然后再Math.max()执行其它参数---实际上,在atLeaseten()调用前,Math.max()根本没有被执行。
因此,goog.partial()对于某个函数如果已知几个参数,但其它参数在以后才知道的情况下很有用。这种特性在事件处理中很常见,因为事件处理函数只知道注册的事件监听器,而最终参数只有在事件被触发时才可用。
使用goog.partial()也对防止内存泄露很有帮助。如下面的例子:
deferredAlertFunction()在他的函数域中引用了所有的变量,XMLHttpRequest对象永远不会被使用。对这种在函数中声明函数,会产生很惊奇的效果,如果你不信,请看下面的例子:
正常情况下,createDeferredEval()在执行结束后,由于xhr没有被任何对象组长,它以应该被垃圾回收,但是即使后面的内部方法没有引用XMLHttpRequest,但是它仍然能访问它,因为,内部方法保持着对声明它的作用域的引用。因此,只要内部方法引用存在,它的声明域就会存在,域中的对象就不会被垃圾回收。
如下的例子会更奇怪:
即使在theDeferredFunction()创建前xhr是undfined,但当deferredFunction调用时,它使用它声明域中的最后状态,这此时xhr被指向了XMLHttpRequest。使用goog.partial()能有效解决这个问题,因为新函数被创建时,它的作用域中只包含传递给goog.partial()的参数。
在这个例子中,deferredFunction()有两个域项:evalAndHandle()产生的新域和全局域。当eval()执行时,先检查局部域,再检查全局域,handleResult在evalAndHandle()域中可见,createDeferredEval()在全局域中可见,但是xhr在所有域中都不存在。因为在createDeferredEval()方法结束后,由于没有任何对象引用它,因此它被垃圾回收掉了。
goog.bind(functionToCall, selfObject, ...)
goog.bind()和goog.partial()有点相似,不同的是,第二个参数是由goog.bind()产生并执行的对象,这对避免如下错误很有用:
上面例子中声明在ProgressBar.prototype中,它被设计在ProgressBar对象中执行。当执行是,this将会指向ProgressBar实例。虽然对update()来说它是true,但是是updateAgain() 却不是,因为订时函数setTimeout()会在全局上下文中执行,这就是说此时this将指向全局对象,在浏览器中,即window对象。对这文件最通用解决方法是重命名为self:
因为self没有this那样的特殊意义,在updateAgain()执行时,它不会被全局对象替换。所以update()方法会在原始的ProgressBar对象中执行,在Closure中,更好的解决方法是用goog.bind():
像goog.partial()一样,goog.bind()在限制域中创建函数,因此updateAgainWithGoogBind()将不会维护percentComplete引用 ,前一例子中updateAgain却会并阻止垃圾回收。goog.bind应该在上面例子情况下多被使用。
导出
在13中将会讲到,当编译器开启侵入式重命名后,所有用户声明的变量都会被重命名。为确定一个变量能在编译后以原来名字引用,就得将变量导出。
goog.getObjectByName(name, opt_object)
goog.getObjectByName()传入一个名字返回对象,这对访问另外的javascript库中的变量很有用:
或从全局环境中取得对象:
初看上去好像这样写很冗长,因此还有简单方法实现些功能:
但这样有可能会返回null,因为一些对象可能不存在,如果window已经声明,但是location没有声明,当调用herf时会抛出错误,如果不用goog.getObjectName(),必须像下面这样做安全检查:
对于检查一个浏览器插件是否存在,可以用如下方法:
goog.exportProperty(object, propertyName, value)
goog.exportProperty()用来给对象设置一个属性值,通常对象已经声明了一个属性,使用goog.exportProperty能确定在编译后还通过这个名字访问该属性,对于如下未编译代码:
编译后代码如下:
在编译后的版本中,Lottery对象有两个属性a和doDrawing都指向同一个函数。导出doDrawing属性没有替换重命名的属性。这样做的目的是为了减少代码大小。
注意,当导出一个可变属性时将产生错误的结果,如下面导出Lottery的winningNumber属性:
当编译后,代码变成:
现在考虑使用Lottery库的如下代码:
当使用未编译版本,hijackLottery()将返回正确myNumber值,但编译后,将返回一个随机值,正确做法是导出一个setter方法:
现在对于编译时和未编译版可如下调用:
这并不是所只能导出函数,其实也可以导出一些只读的基本类型属性:
你也可以通过如下方法导出可变函数:
这不是重设Lottery的doDrawing属性,而是给Lottery.setDoDrawingFunction()传入一个新的doDrawoing函数,当doDrawing被调用时将执行传入的函数,这在编译和未编译版本都正确。
goog.exportSymbol(publicPath, object, opt_objectToExportTo)
goog.exportSymbol()和goog.exportProperty()相似,只是它通过一个全路径导出而不是仅仅通过属性名。如果路径中的中间部分不存在,goog.exportSymbol()会自动创建它,下面例子用goog.exportSymbol()代替goog.exportProperty():
使用goog.exportSymbol()的优势在于它能自动创建新的对象Lottery,并添加四个前面列出的属性。和goog.exportProperty()例子不一样,Lottery对象没有属性重命名版本,goog.exportSymbol()创建新的对象,而不是在存在的对象中添加属性。使用goog.exportSymbol()在多数情况下是个更好的方法。
类型断言
本节介绍一些测试变量类型的函数,应优先使用这些函数而不是使用自定义的类型判断方法,因为编译器能在开启类型检查时使用这些功能。例如像下面代码使用example.customNullTest()而不是使用goog.inNull():
在编译时开启类型检查时,将出现如下错误:
在example.getElementCssClassesById()中用goog.isNull()替换example.custonNullTest()方法将避免这些错误。
goog.typeOf(value)
goog.typeOf()方法和javascript中的typeof一样,这有利于跨浏览器,可能返回值是:object, function, array, string, number, boolean, null 或 undefined。每个返回值都有一个相对应的goog.isXXX()函数,除了undefined,因为他是能通过 is(!goog.isDef(value))。无论如何,使用goog.isXXX()检查一个值都是比goog.typeOf()更直接而且会更少出拼写错误。
唯一一个应该最好使用goog.typeOf()的是在switch块中:
goog.isDef(value)
goog.isDef(value)在value!=undefined时返回true, 因此对于null也会返回true,这在其它boolean上下文中却是false,看下面例子:
goog.isDef()在对可选参数的判断上经常用到:
goog.isNull(value)
当value==null时,goog.isNull(value)返回true,因此对于undefined等其它值都会返回false。
goog.isDefAndNotNull(value)
当value即不是null也不是undefined时返回true。
goog.isArray(obj)
在javascript中,判断array中是否存在一个对象很困难,最通常做法如下:
问题是,Array是javascript运行环境所在window中引用 的函数。在一个页面中有多个frame,每一个中都有独立的Array函数。因此一个frame中array对象不能instanceof另一个frame中的对象。goog.isArray()用来判断对象是否存在数组中。
goog.isArrayLike(obj)
在array.js中,Closure中有一个goog.array.ArrayLike类型。用来包含和array相似的javascript 对象。比如包含document.getElementByTagName()返回的参数对象或节点列表。它包含一个数字类型的length属性,这用于array.js中的工具函数对数字索引的数组对象的操作。goog.isArrayLike()用来判断一个对象是不是goog.array.ArrayLike对象。
goog.isDateLike(obje)
goog.isDateLike()在对象为Date对象时返回true,和goog.isArray()一样,由于可能在不同的frame中传递javascript对象 ,instanceof Date不能简单用来判断一个对像是否为Date类型。goog.isDateLike通过检查是否存在getFullYear()函数还判断对象是否是日期型的。这和instanceof Date不完全相等,它可以为goog.date.Date和goo.date.DateTime.
goog.isString(obj), goog.isBoolean(obj), goog.isNumber(obj)
goog.isString(obj), goog.isBoolean(obj), goog.isNumber(obj)使用typeof来判断类型,因此下面将返回false:
Java有对基本类型的封装类型,Javascript也一样,对strings, booleans,和number也有对应的封装类型。在javascript中有两种string类型,在typeof和instanceof中不同:
因为使用封装类型创建新的string会占用更多内存,比文本型string占用更多空间,在Closure中使用instanceof String是不可用的。在switch中使用封闭类型不能正确工作,而且Boolean封装也很混乱:
在switch中其它封装类型Boolean, Number也会返回true,因此即使没有Closure库,也应该避免封装类型,但是他们可用做强制类型转换:
goog.isFunction(obj)
goog.isFunction用来判断对象是否为一个函数,在Closure中,如果为函数,typeof obj必须返回"function",并且obj必须有一个"call"属性,除了RegExps,NodeLists和一些HTML元素,一些浏览器对这些对象用typeof检查function类型。
goog.isObject(obj)
当为非空对象时,goog.isObject()返回true,非对基本类型则相反,那就是说传统对象,function, array或正则表态式都返回true, 而string, number, boolean, null或undefined返回false。
唯一标识符
因为在javascript中,对象是string类型key的字典结构,本章将演示判断对象类型的map对象持技巧。
goog.getUid(obj)
goog.getUid()对传入的对象返回一个唯一标识符。这会给对象添加一个单一的数字类型属性。对应的是goog.getUid(),返回对象中存在的此属性。对样就可以在单一的目录结构中创建对象类型的map:
这种方式有点像函数中使用哈希值,但是,函数中的数据不会计算UID,这和Java不一样,当对象改变时,UID不会改变,有相同属性的对象没有相同的UID。和和Java中有相同hashCode()时equals()相同也不一样。
因为UID作为一个object中的属性,goog.getUid()如果他的UID没有添加之前可能改变对象。
goog.removeUid(obj)
goog.removeUid()将移除goog.getUid()添加的UID。移除对象的UID只有在函数时调用时执行,这确保在其上没有逻辑。对访问可能有UID的对像调用很困难。为避免在基本类型中调用goog.getUid(),通常将其序列化成JSON串。
国际化
在写本书为此,在Closure库中并不是所有实现国际化的代码都开源,那就是现在库中代码中还存在一些翻译文本的原因。在Closure中,另一个方法是用goog.getMsg(),并将消息存在Closure模板中,并在需要本地化消息时使用模板库。
在Closure库中,编译器将每一个本地消息文件编译一每一个Javascript文件中,为其它方案不同,在一个javascript文件中使用的消息变量的翻译文本都在一个本地文件中,现在的Closure库中使用国际化实现成两个文件是可选的。翻译消息可以像如下这样重新声明goog.getMsg()方法:
编译器不用两个文件处理,而是使用单一文件,这样用户可以下载更少javascript。缺点是资源文件在运行时不能改变。
goog.LOCALE
goog.LOCALE默认将javascript编译成“en”. 本地化语言可以为 fr, pt-BR, zh-Hans-CN。 通过把goog.LOCALE设计成常量,编译器能够消除使用本场资源消息的死代码。如果每一个都通过goog.LOCALE.goog.i18n.DateTimeSymbols将会产生更多大空间浪费。
goog.getMsg(str, opt_values)
goog.getMsg()参数为字符串,并用{$placeholder}作为占位符,假如string中包含占位符,opt_values的值最终会替换它们:
一般的,goog.getMsg()应该赋值给一个变量,并以MSG_开头。
像goog.require()和goog.provide()一样,,编译器只请允许goog.getMsg传入文本字符串参数:
对MSG_变量,通常用@desc标注JSDOC,这对翻译很有帮助:
假如在没有任何上下文下翻译器要翻译''File",这是很困难的,因此@desc提供了更多必需的信息。
面向对象
Closure库支持面向对象,第5章中将详细介绍,本节将介绍base.js中支持面向对象编程的成员。
goog.inherits(childConstructorFunction, parentConstructorFunction)
goog.inherits()用来在子类和父类中建立父子关系。
goog.base(self, opt_methodName, var_args)
goog.base()用来调用父类中的方法。在上下文中,不光能调用父类构造函数,也能调用父类中由参数传入的方法。
goog.nullFunction
goog.nullFunction是一个空函数。常用来作为一个类型函数参数的默认值。这是一个在 HTML5数据库中调用成功回调例子:
因为Foo和Bar是不同的类型,他们应该有自己独立的构造函数而不是引用相同的函数。goog.nullFunction也不应该用作函数参数,因为参数将被随时修改。在前一个例子中,goog.nullFunction在添加toString到原型时被 修改了。新的写法如下 :
当一个类被设计用来子类化时,goog.nullFunction可用来可被子类覆盖方法的默认实现。
goog.abstractMethod
goog.abstractMethod将会抛出错误消息:“unimplemented abstract method”、用来声明可在子类中覆盖的原型方法。
goog.addSingletonGetter(constructorFunction)
对于构造函数中没有任何参数的类,goog.addSingletonGetter()函数添加一个getInstance()的静态方法,在任何时候都返回相同一对象,用来 代替构造函数。
附加工具
base.js也提供了一些使用频率较少的一些工具。
goog.DEBUG
goog.DEBUG标记的代码将在代码编译后被编译器移除。默认情况下它的值为true,因此要移除if(goo.DEBUG) 块,编译选项得用: --define goog.DEBUG=false, 这使在开发过程中能提供更详细的错误信息:
这也能够只用在在高度模式下移除toString()方法:
虽然可以用COMPILED 代替goog.DEBUG来标识需要移除的代码,但最好不要这样做,13章中将会讲述,编译后的代码也可以包含高度信息。
goog.now()
返回1970年1月1日0点到目前为止的毫秒数,它是Date.now的别名,但它在单元调试中很有用。
goog.globalEval(script)
将string类型的javascript代码在全局上下文中执行,对于延时加载的javscript字符串,应该慎重使用此方法,因为当前域的变量应该被保护。
goog.getCssName(className, opt_modifier),goog.setCssNameMapping(mapping)
编译器用这两个函数来重命名css,不幸的是,在本书时,Closure还没有提供在样式表中重命名的方法,但是此处有关于这些的讨论:http://groups.google.com/group/closure-compiler-discuss/browse_thread/thread/1eba1d4f9f4f6475/aff6de7330df798a.