[译]用 Closure Compiler 编写更好的 OO 的 JavaScript

原贴:Coding Better Object-Oriented JavaScript with Closure Compiler

作者:Hedger Wang

前面的话

许多程序员觉得OO 的 JavaScript 是种不错的方法,但也明白由于语言自身的本质和它所运行的环境(主要是在 web 浏览器中),编写 OO 风格的 JavaScript 是比较痛苦的。

使用 Google Closure Compiler 不仅可以压缩代码,而且可以像别的编译器那样编译代码!

当编译器的标帜“ADVANCED_OPTIMIZATIONS(高级选项)” 被贴签时,与大多的 JavaScript 压缩工具如 YUI CompressorDojo Compressor 相比,它拥有更多的优化。

我会讲几种通用的 OO 风格的模式,以及是怎样实现的。

通过这篇文章,你将学会使用 Closure Compiler 来编写具有 OO 风格的JS代码。

OO 风格的 JavaScript 决不简易

长期以来,由于 JavaScript 的本质与浏览器这个宿主环境,造成了普通认为:编写纯 OO 的 JavaScript 应用太难了。Nicholas Zakas 是最有名的黑腰带级空手道 JavaScript 程序员之一,曾在 blog 的贴文中写过 OO 风格的 JS 代码所带来的痛苦:

在传统的OO语言中,类、继承是信手拈来。它们的编译器理解其中的工作原理,所以不会因为加几个类或多几个继承而在执行时受任何的影响。而在 JavaScript 中,引用类型、继承是2大要命的伤痛处。

JavaScript 没有类

虽然 OO 风格的JS 在运行时有执行性能上的缺陷,人们还是在尝试着用各种不同的OO风格的JS,以换掉函数化的JS。

John Resig用了一种有趣的方式以确保在“类”函数作为构造函数时用不用 new 关键字都无所谓。

// John Resig 的方法
function User(name){
  if ( !(this instanceof User) )
    return new User(name);
  this.name = name;
}

var userA = new User('John'); // 是一个实例
var userB = User('Jane'); // 也是一个实例

JavaScript没有访问控制

Douglas Crockford(老道)提出了一种模块模式(module pattern)来说明如何来保护与对象的外部进行读或写的私有成员。

// Crockford 的方法
function User(name) {
  var _name = name;

  // 私有 name 的 getter
  this.getName = function() {
    return _name;
  };
}

全局变量是糟粕,过深命名空间(deeply name-spaced)的变量也垃圾得很

全局变量是魔鬼,而用过深命名空间的变量也超慢

// 全局是魔鬼
var myProjectDemoUserName = 'foo';
// 撒旦...
my.project.demo.user.Name = 'foo';

开发人员不要弄得冗余

在文档中加载了整个 YUI DOM 库,却只用到了其中的一个静态方法YAHOO.util.Dom.getStyle(document.body, ‘backgroundColor’)时怎样呢?

简化不是简单

也可以在写 jQuery 库的插件,但很快就会发现,不能去处理 DOM 范围、选择器模式,也没有数据集合、组件框架、类式继承,没有任何构建复杂 Web 应用开发所需要的东东。

只有 JavaScript 忍者能活吗?

所以,我们只好去招聘那些请不动的 JavaScript 忍者或是成为其中之一

那么,JavaScript 代码非得写得这样吗:

  if (!("a" in window)) {
    var a = 1;
  }
  alert(a);

其实这是 JavaScript 的糟粕,代码不能写成这种鬼样。比如说 C++、Java 程序员就不会这样来编写代码。程序员的重心应该是在算法和数据结构,而不是这些旁门左道的技巧。

我们能有所有的精华吗

看来几乎不像是在编写轻量的、通用的、强壮的 JavaScript 来提供功能丰富的、可维护的能力,并且用在很多地方。

想要让 JavaScript 不再是玩具语言吗?

那就使用 Closure Compiler 吧。

让 Closure Compiler 使你变身为 JavaScript 武将

Closure Compiler 是可以使 JavaScript 下载、运行更快的工具。它是真正的 JavaScript 编译器。将源代码编译为机器代码的替换,将 JavaScript 编译为更优的 JavaScript。会解析并分析你的 JavaScript,移除死代码、重写并最小化。也可以检查语法、变量引用、类型,并警告 JavaScript 常见的相关陷阱。

官网上已经有了很多不错的资源来告诉你如何使用编译器。写这篇文章,其实是为了写一本书中的某章节。让我们先来解决之前说到的问题。

用 @constructor 来标注函数作为类

想要在运行时会检查构造函数,可以使用编译器来为你服务。而且不用在运行时去检查,要记住的这个核心理念是:在编译器完成时就可以了。

/**
 * @constructor
 */
function MyClass() {
}

// Pass.
var obj1 = new MyClass();

// ERROR: Constructor function (this:MyClass):
// class should be called with the "new" keyword.
var obj2 = MyClass(); // Error.

用 @private 进行访问控制

// File demo1.js //

/**
 * A User.
 * @constructor
 */
function User() {

  /**
   * The creation date.
   * @private
   * @type {Date}
   */
  this._birthDay = new Date();
}

/**
 * @return {number} The creation year.
 */
User.prototype.getBirthYear = function() {
  return this._birthDay.getYear();
};

// File demo2.js //

// Create a user.
var me = new User();

// Print out its birth year.
document.write(me.getBirthYear().toString());

编译器会确保私有成员 _birthDay 在整个应用程序的外部不会读或写。只在相同的标为 @private的 JS 代码中才可以访问到对象。当然,也可以用 @protected 在代码里标注。

用 @extend 来管理类继承

假设我们有3个类:ShapeBoxCube

Shape 类定义了一个通用的方法:getSize()

Box 类继承 Shape 类。

Cube 类继承Box

/**
 * Helper function that implements (pseudo)Classical inheritance inheritance.
 * @see http://www.yuiblog.com/blog/2010/01/06/inheritance-patterns-in-yui-3/
 * @param {Function} childClass
 * @param {Function} parentClass
 */
function inherits(childClass, parentClass) {
  /** @constructor */
  var tempClass = function() {
  };
  tempClass.prototype = parentClass.prototype;
  childClass.prototype = new tempClass();
  childClass.prototype.constructor = childClass;
}

//////////////////////////////////////////////////////////////////////////////

/**
 * The shape
 * @constructor
 */
function Shape() {
  // No implementation.
}

/**
 * Get the size
 * @return {number} The size.
 */
Shape.prototype.getSize = function() {
  // No implementation.
};

//////////////////////////////////////////////////////////////////////////////

/**
 * The Box.
 * @param {number} width The width.
 * @param {number} height The height.
 * @constructor
 * @extends {Shape}
 */
function Box(width, height) {
  Shape.call(this);

  /**
   * @private
   * @type {number}
   */
  this.width_ = width;

  /**
   * @private
   * @type {number}
   */
  this.height_ = height;
}
inherits(Box, Shape);

/**
 * @return {number} The width.
 */
Box.prototype.getWidth = function() {
  return this.width_;
};

/**
 * @return {number} The height.
 */
Box.prototype.getHeight = function() {
  return this.height_;
};

/** @inheritDoc */
Box.prototype.getSize = function() {
  return this.height_ * this.width_;
};

////////////////////////////////////////////////////////////////////////////

/**
 * The Box.
 * @param {number} width The width.
 * @param {number} height The height.
 * @param {number} depth The depth.
 * @constructor
 * @extends {Box}
 */
function Cube(width, height, depth) {
  Box.call(this, width, height);

  /**
   * @private
   * @type {number}
   */
  this.depth_ = depth;
}
inherits(Cube, Box);

/**
 * @return {number} The width.
 */
Cube.prototype.getDepth = function() {
  return this.depth_;
};

/** @inheritDoc */
Cube.prototype.getSize = function() {
  return this.depth_ * this.getHeight() * this.getWidth();
};

////////////////////////////////////////////////////////////////////////////

var cube = new Cube(3, 6, 9);
document.write(cube.getSize().toString());

上面的 JavaScript 代码有些长,但是源代码的大小会被简单的看作是输入的字符数。

那些文档中描述编码的注释、变量的名称、方法的名称会被编译器重新命名或移除掉。

3层级的类继承树会被看作是简单的函数,编译器会进行优化。

下面是编译后的代码:

function d(a, b) {
  function c() {
  }
  c.prototype = b.prototype;
  a.prototype = new c
}
function e() {
}
e.prototype.a = function() {
};
function f(a, b) {
  this.c = a;
  this.b = b
}
d(f, e);
f.prototype.a = function() {
  return this.b * this.c
};
function g(a, b, c) {
  f.call(this, a, b);
  this.d = c
}
d(g, f);
g.prototype.a = function() {
  return this.d * this.b * this.c
};
document.write((new g(3, 6, 9)).a().toString());

虽然所有的变量和方法都改名了,但是你也注意到:有些方法被移除掉,有些方法合成了一行内。比如:

Cube.prototype.getSize = function() {
  return this.depth_ * this.getHeight() * this.getWidth();
};

变成了:

g.prototype.a = function() {
  return this.d * this.b * this.c
};

显然,2个 getter 方法 getWidth()getHeight() this._widththis._height安全的替换掉。因此,那些 getter 已经没有用,并且被编译器移除掉了。

同时使用了 @private 和 getter 的方法是指私有属性 _width 对开发人员来说是只读的,无妨对其添加一个 getter 方法。

使用 @interface 和 @implements

我们对编写 OO 风格的 JavaScript 感兴趣了后,将上面的示例改成下面的代码。

// skip example code.

////////////////////////////////////////////////////////////////////////////

/**
 * The shape
 * @interface
 */
function Shape() {
}

/**
 * Get the size
 * @return {number} The size.
 */
Shape.prototype.getSize = function() {};

////////////////////////////////////////////////////////////////////////////

/**
 * The Box.
 * @param {number} width The width.
 * @param {number} height The height.
 * @constructor
 * @implements {Shape}
 */
function Box(width, height) {
  Shape.call(this);

  /**
   * @private
   * @type {number}
   */
  this.width_ = width;

  /**
   * @private
   * @type {number}
   */
  this.height_ = height;
}

/**
 * @return {number} The width.
 */
Box.prototype.getWidth = function() {
  return this.width_;
};

// skip example code.

由于 @interface 只用在编译的时候,经过编译后的代码更小了,并且不会输出包含接口的代码。

function d(a, b) {
  this.c = a;
  this.b = b
}
d.prototype.a = function() {
  return this.b * this.c
};
function e(a, b, c) {
  d.call(this, a, b);
  this.d = c
}
(function(a, b) {
  function c() {
  }
  c.prototype = b.prototype;
  a.prototype = new c
})(e, d);
e.prototype.a = function() {
  return this.d * this.b * this.c
};
document.write((new e(3, 6, 9)).a().toString());

使用包(命名空间的 JS 对象)

想要对 JS 对象使用命名空间的话,命名层级过深的问题不会影响运行时的性能,因为编译器会帮你解决掉。

// Create namespaces.
var demo = {};
demo.example = {};
demo.example.exercise = {};

/**
 * @constructor
 */
demo.example.exercise.Foo = function() {
  demo.example.exercise.Foo.print(this.value1);
  demo.example.exercise.Foo.print(this.value2);
};

/**
 * Static method
 * @param {string} str String to print.
 */
demo.example.exercise.Foo.print = function(str) {
  document.write(str);
};

/**
 * @type {string}
 */
demo.example.exercise.Foo.prototype.value1 = 'abc';

/**
 * @type {string}
 */
demo.example.exercise.Foo.prototype.value2 = 'def';

var foo = new demo.example.exercise.Foo();

编译后的代码:

function a() {
  document.write(this.a);
  document.write(this.b)
}
a.prototype.a = "abc";
a.prototype.b = "def";
new a;

也许,想要保留 JS 代码而避免与页面中其他脚本产生冲突的话,可以使用标帜 -output_wrapper,也不是全局的对象(除非是明确的导出)。

编译后的代码如下:

(function() {function a() {
  document.write(this.a);
  document.write(this.b)
}
a.prototype.a = "abc";
a.prototype.b = "def";
new a;})()

编译器会确保那些长的命名空间、属性、方法已经重命名,尽可能多的短名称。

进行类型检查是在构建时,而非运行时

在构建时进行类型检查可以减少不必要的在运行时进行的类型检查。比如:

function User() {
}

function UsersGroup() {
  this.users_ = [];
}

UsersGroup.prototype.add = function(user) {
  // Make sure that only user can be added.
  if (!(user instanceof User)) {
    throw new Error('Only user can be added.');
  }
  this.users_.push(user);
};

var me = new User();
var myGroup = new UsersGroup();
myGroup.add(me);

这种方法可以完成。

/**
 * @constructor
 */
function User() {
}

/**
 * @constructor
 */
function UsersGroup() {
  /**
   * @private
   * @type {Array.<User>}
   */
  this.users_ = [];
}

/**
* @param {User} user
*/
UsersGroup.prototype.add = function(user) {
  this.users_.push(user);
};

注意 this.users_ 的数据类型为 @type {Array.<user>} 表示是 User 的一个列表。

应该使用有意义的数据结构,而不是视任何事物为原生的对象,否则非常容易出错。

使用 @enum

有些时候你想要处理多种情形:

function Project(status) {
  this.status_ = status;
}

Project.prototype.isBusy = function() {
  switch (this.status_) {
    case 'busy':;
    case 'super_busy':
      return true;
    default:
      return false;
  }
};

var p1 = new Project('busy');
var p2 = new Project('super_busy');
var p3 = new Project('idle');

document.write(p1.isBusy().toString());
document.write(p2.isBusy().toString());
document.write(p3.isBusy().toString());

可以考虑使用 @enum

/**
 * @constructor
 * @param {Project.Status} status
 */
function Project(status) {
  /**
   * @type {Project.Status}
   * @private
   */
  this.status_ = status;
}

/**
 * @enum {number}
 */
Project.Status = {
  BUSY: 0,
  SUPER_BUSY: 1,
  IDLE: 2
};

/**
 * @return {boolean}
 */
Project.prototype.isBusy = function() {
  switch (this.status_) {
    case Project.Status.BUSY:;
    case Project.Status.SUPER_BUSY:
      return true;
    default:
      return false;
  }
};

var p1 = new Project(Project.Status.BUSY);
var p2 = new Project(Project.Status.SUPER_BUSY);
var p3 = new Project(Project.Status.IDLE);

document.write(p1.isBusy().toString());
document.write(p2.isBusy().toString());
document.write(p3.isBusy().toString());

编译后为:

function a(b) {
  this.a = b
}
function c(b) {
  switch(b.a) {
    case 0:
    ;
    case 1:
      return true;
    default:
      return false
  }
}
var d = new a(1), e = new a(2);
document.write(c(new a(0)).toString());
document.write(c(d).toString());
document.write(c(e).toString());

枚举变量被原始的数替换。使用枚举能够编写更多的可维护性的代码。

使用 @define 启用或禁用消息的记录

如果你想要在一个类中记录某些重要的消息的话,像每一位谨慎的程序员那样都可以做到。

/**
 * namespace for the Logger.
 */
var Logger = {};

/**
 * Whether logging should be enabled.
 * @define {boolean}
 */
Logger.ENABLED = true;

/**
 * the log API.
 * @param {...*} args
 */
Logger.log = function(args) {
  if (!Logger.ENABLED) {
    // Don't do anything if logger is disabled.
    return;
  }
  var console = window['console'];
  if (console) {
    console['log'].apply(console, arguments);
  }
};

/**
 * A User.
 * @param {string} name
 * @constructor
 */
function User(name) {
  Logger.log('New User', name);
}

var me = new User('me');

代码会编译为:

function b() {
  var a = window.console;
  a && a.log.apply(a, arguments)
}
new function(a) {
  b("New User", a)
}("me");

你可以添加标帜 –define Logger.ENABLED=false 来禁用记录器。也可以添加标帜 –jscomp_error unknownDefines 来捕获未知的 @define

java -jar compiler.jar \
  --js src/demo.js \
  --js_output_file compiled/demo.js \
  --warning_level VERBOSE \
  --formatting PRETTY_PRINT \
  --jscomp_error accessControls \
  --jscomp_error checkTypes \
  --jscomp_error unknownDefines
  --define Logger.ENABLED=false
  --compilation_level ADVANCED_OPTIMIZATIONS;

对开发人员来说,允许在生成代码时启用记录器,或是完全由编译器带所有的记录器调用到生产布署。

使用类型转换

有些时候你想要把 JSON 对象转换为未知的引用类型。比如:

/**
 * The Model definition.
 * @constructor
 */
function UserModel() {
  /**
   * @type {string}
   */
  this.firstName = '';

  /**
   * @type {string}
   */
  this.lastName = '';
}

/////////////////////////////////////////////////////////////////////////////

/**
 * The User constructor.
 * @constructor
 * @param {string} firstName
 * @param {string} lastName
 */
function User(firstName, lastName) {
  /**
   * @type {string}
   */
  this.fullName = firstName + ' ' + lastName;
}

/**
 * A static method that creates a User from a model.
 * @param {UserModel} model
 * @return {User} The user created.
 */
User.createFromUserModel = function(model) {
  return new User(model.firstName, model.lastName);
};

/////////////////////////////////////////////////////////////////////////////

// Cast a simple JSON Object as {UserModel}.
var data = /** @type {UserModel} */({
  firstName : 'foo',
  lastName : 'bar'
});

// Create a user from the model.
var user = User.createFromUserModel(data);

document.write(user.fullName);

正如你的意料之中,model definition 会移除掉,属性 firstNamelastName 也会重命名。

var a = {a:"foo", c:"bar"};
document.write((new function(b, c) {
  this.b = b + " " + c
}(a.a, a.c)).b);

在上面的示例中,纯对象转换为未知的引用类型,可以给该对象更详细的指定。

jQuery 1.4 中添加了新的 API isPlainObject 它是在运行时进行类型检查,而我将会不推荐,如果你有编译器在手的话,其实在 JS 中看来似乎是解决一大难题

还有...

还有很多其他使用的东西,比如对常量使用 @const

在此本人推荐 Closure 工具官网去学习更多的知识。

另外,有一本不错的书《Closure 权威指南》(打个广告)有所有 closure 工具的详细内容。

总结

本人已经使用 Google Closure Compiler 有两年多了,它完全改变了 JavaScript 开发方式。

总之,在此有以下的东西要分享:

  1. 想要坚持不错的、统一的代码、风格(比如缩进)、80或120字符宽度限制等等,请参考 Google JavaScript Style Guide(中文)。

    请确保代码的可读性和可维护性。

  2. 编写兼具描述性与信息性的文档,有些时候需要编写更多的 JsDoc
  3. 把时间和精力更多的放在不错的 OO 设计、算法、数据结构上,而不是浪费在细微的优化代码或是使用任何的忍者技,那样会不易读或搞昏。
  4. 编写代码快速而频繁,并且生成代码。
  5. 使用大型 JavaScript 库没有错误,只要你可以用编译器来生成代码。其实能够得到更少的代码。
  6. 在 Closure Compiler 的 ADVANCED_OPTIMIZATIONS 模式中确保代码的兼容。

    确保很多质量更高的代码,会使生成整合其他现有的编译器兼容的 JS 代码更加容易。

(完)

你可能感兴趣的:(JavaScript)