第五章 类和继承

来源:互联网 发布:房地产网络推广职责 编辑:程序博客网 时间:2024/04/27 02:37

前面章节中的模块都不是以面向对象形式写的,但是在Closure中许多模块是,尽管Javascript支持基于原型编程,Closure库以原型来实现类的编程。为了管理内存,用传统的复制对象来创建对象。

许多Javascript开发者都回避使用面向对象,他们认为本身的弱类型是很优秀的功能。另一些人完全认为面试对象是有缺陷的,认为即使java的面向对象也不是一个好的设计,引用Effective Java中的第14条说:“组合优于继承”,反对者也提到第15条:“对继承写详细文档或都根本就不使用它”。这两点关于继承的使用在Closure库都被认真设计。

不管你支持那一方,理解Closure中类是怎样使用的是很重要的。本章将讲解Closure中类的使用,通过本章,你将能用库的样式创建对象 ,也能更好的理解库原代码。

Closure中类的例子

创建类的第一步是声明一个构造函数。它能被new关键字用来创建对象实例,这个函数中包含this来引用新创建的实例。

第二步是对函数的原型添加属性。类的每个实例在它的原型链中都有一个隐藏的引用来引用构造函数的原型。这使用对于类的所有实例原型中的信息都可用。

Closue例子

这是一个名为House的例子,它包含一个房子信息:

/*** @fileoverview House contains information about a house, such as its* address.* @author bolinfest@gmail.com (Michael Bolin)*/goog.provide('example.House');/*** Creates a new House.* @param {string} address Address of this house.* @param {number=} numberOfBathrooms Defaults to 1.* @param {Array.<Object>=} itemsInTheGarage* @constructor*/example.House = function(address, numberOfBathrooms, itemsInTheGarage) {/*** @type {string}* @private*/this.address_ = address;if (goog.isDef(numberOfBathrooms)) {this.numberOfBathrooms_ = numberOfBathrooms;}/*** @type {Array.<Object>}* @protected*/this.itemsInTheGarage = goog.isDef(itemsInTheGarage) ?itemsInTheGarage : [];};
/*** @type {number}* @private*/example.House.prototype.numberOfBathrooms_ = 1;/*** @type {boolean}* @private*/example.House.prototype.needsPaintJob_ = false;/** @return {string} */example.House.prototype.getAddress = function() {return this.address_;};/** @return {number} */example.House.prototype.getNumberOfBathrooms = function() {return this.numberOfBathrooms_;};/** @return {boolean} */example.House.prototype.isNeedsPaintJob = function() {return this.needsPaintJob_;};/** @param {boolean} needsPaintJob */example.House.prototype.setNeedsPaintJob = function(needsPaintJob) {this.needsPaintJob_ = needsPaintJob;};/** @param {string} color */example.House.prototype.paint = function(color) {/* some implementation */};/** @return {number} */example.House.prototype.getNumberOfItemsInTheGarage = function() {return this.itemsInTheGarage.length;};
此文件以@fileoverview 注解开始,对本文件内容给出了主要描述。这注解是可选的,但使用它是很好的编程习惯,@author也是一样。

就像第3章所说,goog.provide('example.House')确保有一个全局对象并有House属性。如果goog.provide()已经创建了一个House对象,它会重新分配一个空的新对象。

在16行,example.House被分配为一个函数。@constructor注解标明它是一个构造函数,也就是说它能用于new关键字。当new 用在一个没有@constructor注解的函数时,编译器会发出警告。

就像其它基于类的编程一样,构造函数是用来初使化它有四个参数,对参数的赋值都是应用在新创建的实例上。

example.House有四个字段,每一个都以不同方式赋值,address通过构造函数参数,因此为每房子只有一个地址,它没有默认值,因此是必须的参数。

numberOfBathrooms是可选的,由于它有默认值1以example.House.prototype方式声明,因此它能被类的多个实例共享。

itemsInTheGarage如果传入时将设为此值,否则为[]空数组。

needsPaintJob不通过构造函数赋值,它能被共享。

构造函数必须第一个声明,其它方法和属性可以任意顺序。每一个方法中至少包含一个到this的引用,当指向类的实例,如果它不含this,它会被重写为静态方法。为了理解this在方法中是如何工作的,参考如下例子:

var whiteHouse = new example.House('1600 Pennsylvania Avenue', 35);whiteHouse.setNeedsPaintJob(true);
第二行和如下相同:

whiteHouse.setNeedsPaintJob.call(whiteHouse, true);
对象whiteHouse没有setNeedsPaintJob方法,但是它的原型中有,在example.House.prototype中的setNeedsPaintJob用如下方法分配:

function(needsPaintJob) {this.needsPaintJob_ = needsPaintJob;}
这个方法根本不知道对象的原型,它只知道this引用。
Java中等价例子
package example;/*** House contains information about a house, such as its address.* @author bolinfest@gmail.com (Michael Bolin)*/
public class House {private final String address;private final int numberOfBathrooms;private boolean needsPaintJob;protected Object[] itemsInTheGarage;public House(String address) {this(address, 1);}public House(String address, int numberOfBathrooms) {this(address, numberOfBathrooms, new Object[0]);}public House(String address, int numberOfBathrooms,Object[] itemsInTheGarage) {this.address = address;this.numberOfBathrooms = numberOfBathrooms;this.needsPaintJob = false;this.itemsInTheGarage = itemsInTheGarage;}public String getAddress() {return address;}public int getNumberOfBathrooms() {return numberOfBathrooms;}public boolean isNeedsPaintJob() {return needsPaintJob;}public void setNeedsPaintJob(boolean needsPaintJob) {this.needsPaintJob = needsPaintJob;}public void paint(String color) { /* some implementation */ }public int getNumberOfItemsInTheGarage() {return itemsInTheGarage.length;}}

静态成员
静态成员是和类相关联,而不是和实例关联。如house.js例子所示:

example.House.prototype.numberOfBathrooms_ = 1;

尽管numberOfBathrooms_ 是一个字段,不同实例有不同的值,默认值是声明在原型中,可以不通过类实例更改。同样,实例方法也不是真正受限于类的实例。

var obj = {};example.House.prototype.setNeedsPaintJob.call(obj, true);alert(obj.needsPaintJob_); // alerts true
虽然静态成员和原型继承是相对立的,但在Closure库中也是支持的,它的添加方法是:静态成员作为构造函数属性添加而不是原型:

/*** This would be referred to as an instance method of House because it is* defined on House's prototype.* @param {example.House} house2* @return {number}*/example.House.prototype.calculateDistance = function(house2) {return goog.math.Coordinate.distance(this.getLatLng(), house2.getLatLng());};/*** This would be referred to as a static method of House because it is* defined on House's constructor function.* @param {example.House} house1* @param {example.House} house2* @return {number}*/example.House.calculateDistance = function(house1, house2) {return goog.math.Coordinate.distance(house1.getLatLng(), house2.getLatLng());};
你可能注意到这两个方法很相似,并想知道是否可以从第一个产生第二个。编译器高级模式中对此有特殊处理逻辑,它会在可能情况下重写实例方法和它们调用。这使得它能减少编译后代码大小,因此this不能被重命名,但是house能。另一个是它能提高运行时性能,编译器能内联不含有this的函数块。

单例模式

像第三章所示,goog.addSingletonGetter()能够创建一个返回单例的静态方法。它能直接通过构造函数调用:

goog.provide('example.MonaLisa');/** @constructor */example.MonaLisa = function() {// Probably a really fancy implementation in here!};goog.addSingletonGetter(example.MonaLisa);// Now the constructor has a method named getInstance() defined on it that// will return the singleton instance.var monaLisa1 = example.MonaLisa.getInstance();var monaLisa2 = example.MonaLisa.getInstance();// alerts true because getInstance() will always return the same instancealert(monaLisa1 === monaLisa2);
注意goog.addSingletonGetter()的参数是example.MonaLisa而不是new example.MonaLisa(),因为goog.addSingletonGetter()会延迟实例化实例。因此如果example.MonaLisa.getInstance()未调用时,example.MonaLisa将不会被实例化。goog.addSingletonGetter()对实例化的对象没有任何参数,因此它只能用来构造不含参数的对象。

但是请注意,goog.addSingletonGetter()不能阻止使用构造函数构建对象。它通过getInstance()返回对象来代替通过构造方法。

Closure子类示例
如果成员只对子类可见,可以用@protected标明,按惯例,私有成员在Closure中声明用下划线开关,受保护成员则不用。

Closure 示例

/*** @fileoverview A Mansion is a larger House that includes a guest house.* @author bolinfest@gmail.com (Michael Bolin)*/goog.provide('example.Mansion');goog.require('example.House');/*** @param {string} address Address of this mansion.* @param {example.House} guestHouse This mansion's guest house.* @param {number=} numberOfBathrooms Number of bathrooms in this mansion.* Defaults to 10.* @constructor* @extends {example.House}*/example.Mansion = function(address, guestHouse, numberOfBathrooms) {if (!goog.isDef(numberOfBathrooms)) {numberOfBathrooms = 10;}example.House.call(this, address, numberOfBathrooms);/*** @type {example.House}* @private*/this.guestHouse_ = guestHouse;};goog.inherits(example.Mansion, example.House);
/*** Donates all of the items in the garage.* @param {example.Goodwill} Goodwill Organization who receives the donations.*/example.Mansion.prototype.giveItemsToGoodwill = function(goodwill) {// Can access the itemsInTheGarage field directly because it is protected.// If it were private, then the superclass would have to expose a public or// protected getter method for itemsInTheGarage.var items = this.itemsInTheGarage;for (var i = 0; i < items.length; i++) {goodwill.acceptDonation(items[i]);}this.itemsInTheGarage = [];};/** @inheritDoc */example.Mansion.prototype.paint = function(color) {example.Mansion.superClass_.paint.call(this, color);this.getGuestHouse_.paint(color);};
对一个子类,它的构造函数如下改变:

1,Jsdoc注释有@extends注解,这对编译器很有帮助。对于类型检查也很有用,如一个接受example.House的方法也接受example.Mansion。

2,方法中应改显示调用父类构造函数:

example.House.call(this, address, numberOfBathrooms);
3,在构造函数下面应改调用goog.inherits(),它有两个参数,子类构造函数和父类构造函数:

goog.inherits = function(childCtor, parentCtor) {/** @constructor */function tempCtor() {};tempCtor.prototype = parentCtor.prototype;childCtor.superClass_ = parentCtor.prototype;childCtor.prototype = new tempCtor();childCtor.prototype.constructor = childCtor;};
在goog.inherits()中,它给子类构造函数添加了一个superClass_属性并指向父类的原型,因此example.Mansion.superClass_.paint和example.House.prototype.pant相同。
在子类中声明字段

在字类和父类中字段是共享的,因此要确保不要重名

goog.provide('example.Record');goog.provide('example.BankRecord');goog.require('goog.string');/** @constructor */example.Record = function() {/*** A unique id for this record* @type {string}* @private*/this.id_ = goog.string.createUniqueString();};/** @return {string} */example.Record.prototype.getId = function() {return this.id_;};/*** @param {number} id* @constructor* @extends {example.Record}*/example.BankRecord = function(id) {example.Record.call(this);// THIS IS A PROBLEM BECAUSE IT COLLIDES WITH THE SUPERCLASS FIELD/*** A unique id for this record* @type {number}* @private*/
this.id_ = id;};goog.inherits(example.BankRecord, example.Record);/** @return {number} */example.BankRecord.prototype.getBankRecordId = function() {return this.id_;};
在这个例子中,example.BankRecord中的id_将覆盖example.Record中的,重定义id_后它getId()方法返回一个数字而不是字符串,这种错误编译器无法检测到。

@override和@inheritDoc

@overide用于子类覆盖父类时:

/** @return {example.House} */example.Person.prototype.getDwelling = function() { /* ... */ };/*** In this example, RichPerson is a subclass of Person.* @return {example.Mansion}* @override*/example.RichPerson.prototype.getDwelling = function() { /* ... */ };
@override也可用于有相同方法签名,但是文档需要更新情况:

/** @return {string} A description of this object. */Object.prototype.toString = function() { /* ... */ };/*** @return {string} The format of the string will be "(x,y)".* @override*/Point.prototype.toString = function() { /* ... */ };
如果文档没有任何更新,可以用@inheritDoc


使用goog.base()简单调用父类

在example.Mansion调用父类很麻烦,可用如下方法:

/*** @param {string} address Address of this mansion.* @param {example.House} guestHouse This mansion's guest house.* @param {number=} numberOfBathrooms Number of bathrooms in this mansion.* Defaults to 10.* @constructor* @extends {example.House}*/example.Mansion = function(address, guestHouse, numberOfBathrooms) {if (!goog.isDef(numberOfBathrooms)) {numberOfBathrooms = 10;}goog.base(this, address, numberOfBathrooms);/*** @type {example.House}* @private*/this.guestHouse_ = guestHouse;};goog.inherits(example.Mansion, example.House);
同样,goog.base也可替换example.Mansion.superClass_.paint.call:

/** @inheritDoc */example.Mansion.prototype.paint = function(color) {goog.base(this, 'paint', color);this.guestHouse_.paint(color);};
抽象方法

要声明一个只用于子类覆盖的方法,将其值声明为:goog.abstractMethod:

goog.provide('example.SomeClass');/** @constructor */example.SomeClass = function() {};/*** The JSDoc comment should explain the expected behavior of this method so* that subclasses know how to implement it appropriately.*/example.SomeClass.prototype.methodToOverride = goog.abstractMethod;

如果子类不覆盖此方法将会在运行时方法调用时报错:

goog.provide('example.SubClass');/*** @constructor* @extends {example.SomeClass}*/example.SubClass = function() {goog.base(this);};goog.inherits(example.SubClass, example.SomeClass);var subClass = new example.SubClass();// There is no error from the Compiler saying this is an abstract/unimplemented// method, but executing this code will yield a runtime error thrown by// goog.abstractMethod.subClass.methodToOverride();

到写本书时为止,编译器不会在编译时发出警告。

接口

声明接口很简单:

goog.provide('example.Shape');/*** An interface that represents a two-dimensional shape.* @interface*/example.Shape = function() {};/*** @return {number} the area of this shape*/example.Shape.prototype.getArea = function() {};

要实现一个接口:

goog.provide('example.Circle');/*** @param {number} radius* @constructor* @implements {example.Shape}*/example.Circle = function(radius) {this.radius = radius;};/** @inheritDoc */example.Circle.prototype.getArea = function() {return Math.PI * this.radius * this.radius;};

对于方法参数为example.Shape时可用example.Circle:

goog.provide('example.ShapeUtil');/** @param {!example.Shape} shape */example.ShapeUtil.printArea = function(shape) {document.write('The area of the shape is: ' + shape.getArea());};// This line is type checked successfully by the Closure Compiler.example.ShapeUtil.printArea(new example.Circle(1));

多继承
Closure中不支持多继承,但开发者可以有方案实现它,可借助于goo.mixin():

goog.provide('example.Phone');goog.provide('example.Mp3Player');goog.provide('example.AndroidPhone');/** @constructor */example.Phone = function(phoneNumber) { /* ... */ };example.Phone.prototype.makePhoneCall = function(phoneNumber) { /* ... */ };/** @constructor */example.Mp3Player = function(storageSize) { /* ... */ };example.Mp3Player.prototype.playSong = function(fileName) {var mp3 = this.loadMp3FromFile(fileName);mp3.play();return mp3;};/*** @constructor* @extends {example.Phone}*/example.AndroidPhone = function(phoneNumber, storageSize) {example.Phone.call(this, phoneNumber);example.Mp3Player.call(this, storageSize);};goog.inherits(example.AndroidPhone, example.Phone);goog.mixin(example.AndroidPhone.prototype, example.Mp3Player.prototype);

枚举

枚举用@enum标注:

/** @enum {number} */example.CardinalDirection = {NORTH: Math.PI / 2,SOUTH: 3 * Math.PI / 2,EAST: 0,WEST: Math.PI};
枚举方法:

/*** @param {example.CardinalDirection} direction* @return {example.CardinalDirection}*/example.CardinalDirection.rotateLeft90Degrees = function(direction) {return (direction + (Math.PI / 2)) % (2 * Math.PI);};
这会在example.CardinalDiretion中添加新属性,因此goog.object.forEache遍历它时也会包含rotateLeft90Degrees(),可以将枚举放在数组中来迭代:

/** @type {Array.<example.CardinalDirection>} */example.CardinalDirection.values = [example.CardinalDirection.NORTH,example.CardinalDirection.SOUTH,example.CardinalDirection.EAST,example.CardinalDirection.WEST];
不幸的是它要求example.CardinalDirection 和example.CardinalDirection.values同时维护,但它能保证迭代正确。

goog.Disposable


如果一个对象不再被使用,它要求显示被释放内存,它需要继承goog.Disposable,它含有两个方法:

goog.Disposable.prototype.dispose = function() {if (!this.disposed_) {    this.disposed_ = true;this.disposeInternal();}};goog.Disposable.prototype.disposeInternal = function() {    // No-op in the base class.};
第一个方法dispose释放对象引用,实际上调用 disposeInternal完成,disposed_标识可以确保disposeInternal只被调用 一次。

覆盖disposeInternal()方法:

覆盖disposeInternal()要完成5个工作:

1,调用父类disposeInternal方法

2,释放类中引用的所有对象

3,移除类的监听器

4,移除类的DOM节点

5,移除类的COM对象

goog.provide('example.AutoSave');goog.require('goog.Disposable');/*** @param {!Element} container into which the UI will be rendered.* @param {!goog.ui.Button} saveButton Button to disable while saving.* @constructor* @extends {goog.Disposable}*/example.AutoSave = function(container, saveButton) {// Currently, goog.Disposable is an empty function, so it may be tempting// to omit this call; however, the Closure Compiler will remove this line// for you when Advanced Optimizations are enabled. It is better to// leave this call around in case the implementation of goog.Disposable()// changes.goog.Disposable.call(this);/*** @type {!Element}*/this.container = container;/*** @type {function(Event)}*/this.eventListener = goog.bind(this.onMouseOver, this);// Although this usage follows the standard set by the W3C Event model,// this is not the recommended way to manage events in the Closure Library.// The correct way to add event listeners is explained in the next chapter.container.addEventListener('mouseover', this.eventListener, false);/*** @type {!goog.ui.Button}*/this.saveButton = saveButton;};goog.inherits(example.AutoSave, goog.Disposable);/** @type {XMLHttpRequest} */example.AutoSave.prototype.xhr;/** @type {Element} */example.AutoSave.prototype.label;/*** Dialog to display if auto-saving fails; lazily created after the first* failure.* @type {goog.ui.Dialog}*/example.AutoSave.prototype.failureDialog;example.AutoSave.prototype.render = function() {this.container.innerHTML = '<span style="display:none">Saving...</span>';this.label = this.container.firstChild;};/** @param {Event} e */example.AutoSave.prototype.onMouseOver = function(e) { /* ... */ };/** @inheritDoc */example.AutoSave.prototype.disposeInternal = function() {// (1) Call the superclass's disposeInternal() method.example.AutoSave.superClass_.disposeInternal.call(this);// (2) Dispose of all Disposable objects owned by this class.goog.dispose(this.failureDialog);// (3) Remove listeners added by this class.this.container.removeEventListener('mouseover', this.eventListener, false);// (4) Remove references to COM objects.this.xhr = null;// (5) Remove references to DOM nodes, which are COM objects in IE.delete this.container;this.label = null;};







































































原创粉丝点击