现在的位置: 首页 > 综合 > 正文

第三章 Closure基本库 第四章 常用工具类

2013年11月22日 ⁄ 综合 ⁄ 共 26646字 ⁄ 字号 评论关闭

就像第一章例子所描述一样,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中规范的全局对象访问:

// Define a global function for a JSONP callback.
goog.global['callback'] = function(json) { /* ... */ };
// setTimeout() is not defined by the ES5 specification, but by the W3C
// Window Object specification: http://www.w3.org/TR/Window/
window.setTimeout(someFunction, 100);
// Similarly, location is a property specific to a browser-based environment,
// and is defined explicitly for window by the W3C.
window.location = 'http://www.example.com/';

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通过点分开的名字产生命名空间,它会先从左到右依次判断该名字在全局中是否存在 ,如果存在都使用存在的,不存在才产生新的对象:

// Calling this function:
goog.provide('example.of.a.long.namespace');
// Is equivalent to doing:
//
// var example = goog.global.example || (goog.global.example = {});
// if (!example.of) example.of = {};
// if (!example.of.a) example.of.a = {};
// if (!example.of.a.namespace) example.of.a.namespace = {}; 
// Because goog.provide() has been called, it is safe to assume that
// example.of.a.long.namespace refers to an object, so it is safe to
// add new properties to it without checking for null.
example.of.a.long.namespace.isOver = function() { alert('easy, eh?'); };

比如如果调用: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中所用的,只有一个全局对象,并声明所有方法作为那个对象的属性。一个命名空间下面再分为多个命名空间:

// Creates a namespace named "goog".
var goog = {};
// Creates a new namespace named "goog.array".
goog.array = {};
// Adds a function named binarySearch to the "goog.array" namespace.
goog.array.binarySearch = function(arr, target, opt_compareFn) { /*...*/ };
// Adds a function named sort to the "goog.array" namespace.
goog.array.sort = function(arr, opt_compareFn) { /*...*/ };

用点分开的命名空间和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.provide('example.radius');
/**
 * @param {number} radius
 * @param {goog.math.Coordinate} point
 * @return {boolean} whether point is within the specified radius
 */
example.radius.isWithinRadius = function(radius, point) {
  return Math.sqrt(point.x * point.x + point.y * point.y) <= radius;
};

如果方法如下重写,就需要明确调用goog.requier加载 goog.math.Coordinate :

goog.provide('example.radius');
goog.require('goog.math.Coordinate');
/**
 * @param {number} radius
 * @param {goog.math.Coordinate} point
 * @return {boolean} whether point is within the specified radius
 */
example.radius.isWithinRadius = function(radius, point) {
  var origin = new goog.math.Coordinate(0, 0);
  var distance = goog.math.Coordinate.distance(point, origin);
  return distance <= radius;
};

再回到第一章中的"Hello World"例子,页面中有两个<script>标签,一个指向base.js,另一个声明sayHello()方法。对于使用未编译的库,goog.require()不会出错,即使goog.provide('goog.do')根本没有被调用--这是为什么呢?

因为COMPILED默认值是false,base.js产生如下代码:

document.write('<script type="text/javascript" src="deps.js"></script>');

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:

// File: model.js
goog.provide('example.Model');
example.Model.getUserForEmailAddress = function(emailAddress) {
  if (emailAddress == 'bolinfest@gmail.com') {
    return { firstName: 'Michael', lastName: 'Bolin' };
  }
};
// File: view.js
goog.provide('example.View');
goog.require('example.Model');
example.View.displayUserInfo = function(emailAddress) {
  var user = example.Model.getUserForEmailAddress(emailAddress);
  document.write('First Name: ' + user.firstName);
  // etc.
};

model.js和view.js在primitive目录中,和Closure库位于同级目录。在primitives目录下,calcdeps.py用如下命令创建deps.js依赖文件:

python ../closure-library-r155/bin/calcdeps.py \
  --output_mode deps \
  --dep ../closure-library-r155/goog/ \
  --path model.js \
  --path view.js > model-view-deps.js

产生 model-view-deps.js文件,内容如下:

// This file was autogenerated by calcdeps.py
goog.addDependency("../../primitives/model.js", ['example.Model'], []);
goog.addDependency("../../primitives/view.js", ['example.View'], ['example.Model']);

对每一个传递给calcdeps.py的输入文件都会产生一个goog.addDependency()。它文件中的 goog.require() 和goog.provide()值。这用来在客户端生成依赖图,使得goog.require()能够加载依赖:

<!doctype html>
<html>
<head></head>
<body>
  <script src="../closure-library-r155/goog/base.js"></script>
  <!--
    When base.js is loaded, it will call:
    document.write('<script src="../closure-library-r155/goog/deps.js"></script>');
    The deps.js file contains all of the calls to goog.addDependency() to build
    the dependency graph for the Closure Library. The deps.js file will be
    loaded after the base.js script tag but before any subsequent script tags.
  -->
 <!--
    This loads the two calls to goog.addDependency() for example.Model and
    example.View.
  -->
  <script src="model-view-deps.js"></script>
  <script>
  // When this script block is evaluated, model-view-deps.js will already have
  // been loaded. Using the dependency graph built up by goog.addDependency(),
  // goog.require() determines that example.View is defined in
  // ../../primitives/view.js, and that its dependency, example.Model, is
  // defined in ../../primitives/model.js. These paths are relative to base.js,
  // so it will call the following to load those two files:
  //
  // document.write('<script ' +
  // 'src="../closure-library-r155/goog/../../primitives/model.js"><\/script>');
  // document.write('<script ' +
  // 'src="../closure-library-r155/goog/../../primitives/view.js"><\/script>');
  //
  // Like deps.js, model.js and view.js will not be loaded until this script
  // tag is fully evaluated, but they will be loaded before any subsequent
  // script tags.
  goog.require('example.View'); // calls document.write() twice
 // The example.View namespace cannot be used here because view.js has not
  // been loaded yet, but functions that refer to it may be defined:
  var main = function() {
    example.View.displayUserInfo('bolinfest@gmail.com');
  };
  // So long as view.js is loaded before this function is executed, this is not
  // an issue because example.View.displayUserInfo() is not evaluated
  // until main() is called. The following, however, would be evaluated
  // immediately:
  alert(typeof example); // alerts 'undefined'
  </script>
  <script>
  // Both model.js and view.js will be loaded before this <script> tag,
  // so example.Model and example.View can be used here.
  alert(typeof example); // alerts 'object'
  main(); // calls function that uses example.View
  </script>
</body>
</html>

从上面例子可以看出,上面的依赖会动态生成<script>标签。加载如果多的文件会有很大的性能问题,这种方法只能用于开发环境,这能更好调试代码。

函数

在应用局部,函数有一个强大的功能就是预先包含一些参数,而其它的参数在函数调用时确定,在这一节中,将会讲述“当一个函数被调用时this所指定的对象”,主要讲在函数调用时this所指定的值是怎么被解释的,特别是在对call()和apply()方法调用时。

goog.partial(functionToCall, ...)

goog.partial()函数参数为一个方法名和其它一些参数,很像javascript所有函数中的call()方法,和call()方法不同的是,它用指定的参数执行所传递的方法,goog.partial()将返回一个新的函数,在这函数被调用时,会根据传的和参数执行,下面的例子中,a()和b()将会产生相同效果:

var a = function() {
alert('Hello world!');
};
var b = goog.partial(alert, 'Hello world!');

a()和b()在执行时都会弹出"Hello world!"信息,初看,好像goog.partial()仅仅是匿名函数的快捷方式,但是它还能有如下新的特性:

// Recall that Math.max() is a function that takes an arbitrary number of
// arguments and returns the greatest value among the arguments given.
// If no arguments are passed, then it returns -Infinity.
var atLeastTen = goog.partial(Math.max, 10);
atLeastTen(-42, 0, 7); // returns 10: equivalent to Math.max(10, -42, 0, 7);
atLeastTen(99); // returns 99: equivalent to Math.max(10, 99);
atLeastTen(); // returns 10: equivalent to Math.max(10);

atLeastten()是一个用Math.max()计算10和其它参数的函数,也就是说,goog.partial()创建了atLeaseTen()方法,它不是先用参数10调用Math.max,然后再Math.max()执行其它参数---实际上,在atLeaseten()调用前,Math.max()根本没有被执行。

因此,goog.partial()对于某个函数如果已知几个参数,但其它参数在以后才知道的情况下很有用。这种特性在事件处理中很常见,因为事件处理函数只知道注册的事件监听器,而最终参数只有在事件被触发时才可用。

使用goog.partial()也对防止内存泄露很有帮助。如下面的例子:

var createDeferredAlertFunction = function() {
// Note the XMLHttpRequest constructor is not available in Internet Explorer 6.
var xhr = new XMLHttpRequest();
return function() {
alert('Hello world!');
};
};
var deferredAlertFunction = createDeferredAlertFunction();

deferredAlertFunction()在他的函数域中引用了所有的变量,XMLHttpRequest对象永远不会被使用。对这种在函数中声明函数,会产生很惊奇的效果,如果你不信,请看下面的例子:

var createDeferredEval = function() {
// Note the XMLHttpRequest constructor is not available in Internet Explorer 6.
var xhr = new XMLHttpRequest();
return function(handleResult, str) {
// Note there are no references to the XMLHttpRequest within this function.
var result = eval(str);
handleResult(result);
};
};
var deferredFunction = createDeferredEval();
// Alerts the toString() of XMLHttpRequest.
deferredFunction(alert, 'xhr');

正常情况下,createDeferredEval()在执行结束后,由于xhr没有被任何对象组长,它以应该被垃圾回收,但是即使后面的内部方法没有引用XMLHttpRequest,但是它仍然能访问它,因为,内部方法保持着对声明它的作用域的引用。因此,只要内部方法引用存在,它的声明域就会存在,域中的对象就不会被垃圾回收。

如下的例子会更奇怪:

var createDeferredEval = function() {
var xhr = undefined;
var theDeferredFunction = function(handleResult, str) {
         // Note there are no references to the XMLHttpRequest within this function.
         var result = eval(str);
         handleResult(result);
    };
    xhr = new XMLHttpRequest();
    return theDeferredFunction;
};
var deferredFunction = createDeferredEval();
// Still alerts the toString() of XMLHttpRequest.
deferredFunction(alert, 'xhr');

即使在theDeferredFunction()创建前xhr是undfined,但当deferredFunction调用时,它使用它声明域中的最后状态,这此时xhr被指向了XMLHttpRequest。使用goog.partial()能有效解决这个问题,因为新函数被创建时,它的作用域中只包含传递给goog.partial()的参数。

var evalAndHandle = function(handleResult, str) {
// Note there are no references to the XMLHttpRequest within this function.
var result = eval(str);
handleResult(result);
};
var createDeferredEval = function() {
var xhr = new XMLHttpRequest();
return goog.partial(evalAndHandle, alert);
};
var deferredFunction = createDeferredEval();
// Alerts the toString() of the alert function.
deferredFunction('handleResult');
// Alerts the toString() of the createDeferredEval function.
deferredFunction('createDeferredEval');
// Fails because eval('xhr') throws an error because there is no variable
// named xhr in its scope.
deferredFunction('xhr');

在这个例子中,deferredFunction()有两个域项:evalAndHandle()产生的新域和全局域。当eval()执行时,先检查局部域,再检查全局域,handleResult在evalAndHandle()域中可见,createDeferredEval()在全局域中可见,但是xhr在所有域中都不存在。因为在createDeferredEval()方法结束后,由于没有任何对象引用它,因此它被垃圾回收掉了。

goog.bind(functionToCall, selfObject, ...)
goog.bind()和goog.partial()有点相似,不同的是,第二个参数是由goog.bind()产生并执行的对象,这对避免如下错误很有用:

ProgressBar.prototype.update = function(statusBar) {
if (!this.isComplete()) {
var percentComplete = this.getPercentComplete();
statusBar.repaintStatus(percentComplete);
// Update the statusBar again in 500 milliseconds.
var updateAgain = function() { this.update(statusBar); };
setTimeout(updateAgain, 500);
}
};

上面例子中声明在ProgressBar.prototype中,它被设计在ProgressBar对象中执行。当执行是,this将会指向ProgressBar实例。虽然对update()来说它是true,但是是updateAgain() 却不是,因为订时函数setTimeout()会在全局上下文中执行,这就是说此时this将指向全局对象,在浏览器中,即window对象。对这文件最通用解决方法是重命名为self:

ProgressBar.prototype.update = function(statusBar) {
if (!this.isComplete()) {
var percentComplete = this.getPercentComplete();
statusBar.repaintStatus(percentComplete);
// Update the statusBar again in 500 milliseconds.
var self = this;
var updateAgain = function() { self.update(statusBar); };
setTimeout(updateAgain, 500);
}
};

因为self没有this那样的特殊意义,在updateAgain()执行时,它不会被全局对象替换。所以update()方法会在原始的ProgressBar对象中执行,在Closure中,更好的解决方法是用goog.bind():

ProgressBar.prototype.update = function(statusBar) {
if (!this.isComplete()) {
var percentComplete = this.getPercentComplete();
statusBar.repaintStatus(percentComplete);
// Update the statusBar again in 500 milliseconds.
var updateAgainWithGoogBind = goog.bind(this.update, this, statusBar);
setTimeout(updateAgainWithGoogBind, 500);
}
};

像goog.partial()一样,goog.bind()在限制域中创建函数,因此updateAgainWithGoogBind()将不会维护percentComplete引用 ,前一例子中updateAgain却会并阻止垃圾回收。goog.bind应该在上面例子情况下多被使用。
导出

在13中将会讲到,当编译器开启侵入式重命名后,所有用户声明的变量都会被重命名。为确定一个变量能在编译后以原来名字引用,就得将变量导出。

goog.getObjectByName(name, opt_object)

goog.getObjectByName()传入一个名字返回对象,这对访问另外的javascript库中的变量很有用:

var GMap2 = goog.getObjectByName('google.maps.GMap2');
var map = new GMap2(document.body);

或从全局环境中取得对象:

// window.location will not be defined if the code is executed in Rhino
var href = goog.getObjectByName('window.location.href');

初看上去好像这样写很冗长,因此还有简单方法实现些功能:

var href = window.location.href;

但这样有可能会返回null,因为一些对象可能不存在,如果window已经声明,但是location没有声明,当调用herf时会抛出错误,如果不用goog.getObjectName(),必须像下面这样做安全检查:

var href = (window && window.location && window.location.href) || null;

对于检查一个浏览器插件是否存在,可以用如下方法:

var workerPool = goog.getObjectByName('google.gears.workerPool');


goog.exportProperty(object, propertyName, value)
goog.exportProperty()用来给对象设置一个属性值,通常对象已经声明了一个属性,使用goog.exportProperty能确定在编译后还通过这个名字访问该属性,对于如下未编译代码:

goog.provide('Lottery');
Lottery.doDrawing = function() {
Lottery.winningNumber = Math.round(Math.random() * 1000);
};
// In uncompiled mode, this is redundant.
goog.exportProperty(Lottery, 'doDrawing', Lottery.doDrawing);

编译后代码如下:

var a = {}; // Lottery namespace
a.a = function() { /* ... */ }; // doDrawing has been renamed to 'a'
a.doDrawing = a.a; // doDrawing exported on Lottery

在编译后的版本中,Lottery对象有两个属性a和doDrawing都指向同一个函数。导出doDrawing属性没有替换重命名的属性。这样做的目的是为了减少代码大小。

注意,当导出一个可变属性时将产生错误的结果,如下面导出Lottery的winningNumber属性:

Lottery.winningNumber = 747;
goog.exportProperty(Lottery, 'winningNumber', Lottery.winningNumber);
Lottery.getWinningNumber = function() { return Lottery.winningNumber; };
goog.exportProperty(Lottery, 'getWinningNumber', Lottery.getWinningNumber);

当编译后,代码变成:

var a = {};
a.b = function() { a.a = Math.round(Math.random() * 1000); };
a.doDrawing = a.b;
a.a = 747;
a.winningNumber = a.a;
a.c = function() { return a.a; };
a.getWinningNumber = a.c;

现在考虑使用Lottery库的如下代码:

var hijackLottery = function(myNumber) {
Lottery.doDrawing();
Lottery.winningNumber = myNumber;
return Lottery.getWinningNumber();
};

当使用未编译版本,hijackLottery()将返回正确myNumber值,但编译后,将返回一个随机值,正确做法是导出一个setter方法:

Lottery.setWinningNumber = function(myNumber) {
Lottery.winningNumber = myNumber;
};
goog.exportProperty(Lottery, 'setWinningNumber', Lottery.setWinningNumber);

现在对于编译时和未编译版可如下调用:

var hijackLottery = function(myNumber) {
Lottery.doDrawing();
Lottery.setWinningNumber(myNumber);
return Lottery.getWinningNumber();
};

这并不是所只能导出函数,其实也可以导出一些只读的基本类型属性:

Lottery.MAX_TICKET_NUMBER = 999;
goog.exportProperty(Lottery, 'MAX_TICKET_NUMBER', Lottery.MAX_TICKET_NUMBER);

你也可以通过如下方法导出可变函数:

Lottery.doDrawingFunction_ = function() {};
Lottery.setDoDrawingFunction = function(f) {
Lottery.doDrawingFunction_ = f;
};
goog.exportProperty(Lottery, 'setDoDrawingFunction',Lottery.setDoDrawingFunction);
Lottery.doDrawing = function() {
Lottery.doDrawingFunction_.apply(null, arguments);
};
goog.exportProperty(Lottery, 'doDrawing', Lottery.doDrawing);

这不是重设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.doDrawing', Lottery.doDrawing);
goog.exportSymbol('Lottery.getWinningNumber', Lottery.getWinningNumber);
goog.exportSymbol('Lottery.setWinningNumber', Lottery.setWinningNumber);
goog.exportSymbol('Lottery.MAX_TICKET_NUMBER', Lottery.MAX_TICKET_NUMBER);

使用goog.exportSymbol()的优势在于它能自动创建新的对象Lottery,并添加四个前面列出的属性。和goog.exportProperty()例子不一样,Lottery对象没有属性重命名版本,goog.exportSymbol()创建新的对象,而不是在存在的对象中添加属性。使用goog.exportSymbol()在多数情况下是个更好的方法。

类型断言

本节介绍一些测试变量类型的函数,应优先使用这些函数而不是使用自定义的类型判断方法,因为编译器能在开启类型检查时使用这些功能。例如像下面代码使用example.customNullTest()而不是使用goog.inNull():

/**
* @param {!Element} el
* @return {!Array.<string>}
*/
example.getElementCssClasses = function(el) {
return el.className.split(' ');
};
/**
* @param {*} arg
* @return {boolean}
*/
example.customNullTest = function(arg) {
return arg === null;
};
/**
* @param {string} id
* @return {Array.<string>}
*/
example.getElementCssClassesById = function(id) {
var el = document.getElementById(id);
// At this point, the Compiler knows that el is either an Element or null.
if (example.customNullTest(el)) {
return null;
} else {
// If goog.isNull() were used in the conditional, the Compiler would be able
// to infer that el must be non-null in this branch of the conditional.
return example.getElementCssClasses(el);
}
};

在编译时开启类型检查时,将出现如下错误:

type-assertions.js:33: ERROR - actual parameter 1 of example.getElementCssClasses
does not match formal parameter
found : (HTMLElement|null)
required: Element
return example.getElementCssClasses(el);
                                                        ^

在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()更直接而且会更少出拼写错误。

// Misspelling of 'string' will not be caught by the Compiler.
if (goog.typeOf(value) == 'strnig')
// Misspelling of isString will be caught by the Compiler.
if (goog.isStrnig(value))

唯一一个应该最好使用goog.typeOf()的是在switch块中:

switch (goog.typeOf(someArg)) {
case 'string': doStringThing(someArg); break;
case 'number': doNumberThing(someArg); break;
case 'boolean': doBooleanThing(someArg); break;
// etc.
}

goog.isDef(value)

goog.isDef(value)在value!=undefined时返回true, 因此对于null也会返回true,这在其它boolean上下文中却是false,看下面例子:

var obj = { "a": undefined };
goog.isDef(obj); // true
goog.isDef(obj.a); // false
('a' in obj) // true
goog.isDef(obj.b); // false
('b' in obj) // false
goog.isDef(undefined); // false
goog.isDef(0); // true
goog.isDef(false); // true
goog.isDef(null); // true
goog.isDef(''); // true

goog.isDef()在对可选参数的判断上经常用到:

/**
* @param {number} bill The cost of the bill.
* @param {number=} tipAmount How much to tip. Defaults to 15% of the bill.
*/
var payServer = function(bill, tipAmount) {
if (goog.isDef(tipAmount)) {
pay(bill + tipAmount);
} else {
pay(bill * 1.15);
}
};

goog.isNull(value)

当value==null时,goog.isNull(value)返回true,因此对于undefined等其它值都会返回false。

goog.isDefAndNotNull(value)

当value即不是null也不是undefined时返回true。

goog.isArray(obj)

在javascript中,判断array中是否存在一个对象很困难,最通常做法如下:

var isArray = function(arr) {
return arr instanceof 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:

       goog.isString(new String('I am a capital S String'));

Java有对基本类型的封装类型,Javascript也一样,对strings, booleans,和number也有对应的封装类型。在javascript中有两种string类型,在typeof和instanceof中不同:

var s = 'I am an ordinary string';
typeof s; // evaluates to 'string'
s instanceof String; // evaluates to false
var S = new String('I am a string created using the new operator');
typeof S; // evaluates to 'object'
S instanceof String; // evaluates to true

因为使用封装类型创建新的string会占用更多内存,比文本型string占用更多空间,在Closure中使用instanceof String是不可用的。在switch中使用封闭类型不能正确工作,而且Boolean封装也很混乱:

var wrapperString = new String('foo');
var wrapperBoolean = new Boolean(false);
// This code alerts 'You lose!' because switch tests using === and
// wrapperBoolean is considered true in a boolean context.
switch(wrapperString) {
case 'foo':
break;
default:
if (wrapperBoolean) alert('You lose!');
}

在switch中其它封装类型Boolean, Number也会返回true,因此即使没有Closure库,也应该避免封装类型,但是他们可用做强制类型转换:

var b = Boolean(undefined); // b is false
typeof b; // evaluates to 'boolean'

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:

/** @typedef {Object} */
example.Map;
/** @return {example.Map} */
example.createMap = function() {
return {};
};
/**
* @param {example.Map} map
* @param {!Object} key
* @param {!Object} value
*/
example.put = function(map, key, value) {
map[goog.getUid(key)] = value;
};
/**
* @param {example.Map} map
* @param {!Object} key
* @return {!Object|undefined}
*/
example.get = function(map, key) {
return map[goog.getUid(key)];
};

这种方式有点像函数中使用哈希值,但是,函数中的数据不会计算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()方法:

// File with application logic:
var MSG_HELLO_WORLD = goog.getMsg('hello world');
// File with translations:
goog.getMsg = function(str, values) {
switch (str) {
case 'hello world': return 'hola mundo';
default: throw Error('No translation for: ' + str);
}
};

编译器不用两个文件处理,而是使用单一文件,这样用户可以下载更少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的值最终会替换它们:

// Example with no placeholders.
var MSG_FILE_MENU = goog.getMsg('File');
// Example with two placeholders.
var MSG_SAMPLE_SENTENCE = goog.getMsg(
'The quick brown {$quickAnimal} jumps over the lazy {$lazyAnimal}',
{'quickAnimal': 'fox', 'lazyAnimal': 'dog'});

一般的,goog.getMsg()应该赋值给一个变量,并以MSG_开头。

像goog.require()和goog.provide()一样,,编译器只请允许goog.getMsg传入文本字符串参数:

// The Compiler will reject this because the argument to goog.getMsg is an
// expression rather than a string literal.
var MSG_GREETING = goog.getMsg(useFormalGreeting ? 'Sir' : 'Mr.');
// The above should be rewritten as follows:
var MSG_SIR = goog.getMsg('Sir');
var MSG_MISTER = goog.getMsg('Mr.');
var msgGreeting = useFormalGreeting ? MSG_SIR : MSG_MISTER;

对MSG_变量,通常用@desc标注JSDOC,这对翻译很有帮助:

/** @desc Label for the File menu. */
var MSG_FILE_MENU = goog.getMsg('File');

假如在没有任何上下文下翻译器要翻译''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数据库中调用成功回调例子:

var database = openDatabase({"name": "wiki"});
database.transaction(function(tx) {
// Use goog.nullFunction rather than null to avoid the risk of a
// null pointer error.
tx.executeSql('SELECT * FROM user', [], goog.nullFunction, alert);
});

goog.nullFunction用来保存数据,尽管在使用空方法的地方都可以使用这个函数,但是构造对象时应避免使用:

/** @constructor */
var Foo = goog.nullFunction;
Foo.prototype.toString = function() { return 'Foo'; };
/** @constructor */
var Bar = goog.nullFunction;
Bar.prototype.toString = function() { return 'Bar'; };
var foo = new Foo();
alert(foo); // alerts 'Bar'
alert(foo instanceof Foo); // alerts true
alert(foo instanceof Bar); // alerts true

因为Foo和Bar是不同的类型,他们应该有自己独立的构造函数而不是引用相同的函数。goog.nullFunction也不应该用作函数参数,因为参数将被随时修改。在前一个例子中,goog.nullFunction在添加toString到原型时被 修改了。新的写法如下 :

/** @constructor */
var Foo = function() {};
Foo.prototype.toString = function() { return 'Foo'; };
/** @constructor */
var Bar = function() {};
Bar.prototype.toString = function() { return 'Bar'; };

当一个类被设计用来子类化时,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, 这使在开发过程中能提供更详细的错误信息:

if (badThingHappened) {
if (goog.DEBUG) {
throw new Error('This is probably because the database on your ' +
'development machine is down. Check to make sure it is running ' +
'and then restart your server.');
}
}

这也能够只用在在高度模式下移除toString()方法:

if (goog.DEBUG) {
SomeClass.prototype.toString = function() {
return 'foo: ' + this.foo_ + ', bar: ' + this.bar_;
};
}

虽然可以用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.

第四章 常用工具类

【上篇】
【下篇】

抱歉!评论已关闭.