原文出处:Joe Lennon
从头开始学习 Dojo,第 2 部分
使用 Dojo 掌握面向对象开发
什么是面向对象开发?
面向对象编程(Object-Oriented Programming,OOP)是一个软件开发范式,它基于称为 “对象” 的数据结构的定义,由数据属性和函数组成。这些属性(成员变量)和函数(或方法)定义软件与那个对象可能进行的交互。OOP 的主要好处是,简化您的代码结构,有助于代码重用和维护。
“面向对象” 基础知识
面向对象编程的基本前提是:在您的软件中创建一些对象,这些对象定义一系列应用于该对象的属性和一系列能够检索或修改该对象的属性的方法或函数。car
可能是一个简单的对象示例。与一个 car
关联的数据属性可能包括它的 manufacturer
、model number
、registration number
、color
、cubic capacity
,等等。一个 car
对象提供的方法可能包括 accelerate
、brake
、change gear
、turn
、stop
等等。在 OOP 中,理念是您定义对所有汽车都通用的基本属性和方法,每辆汽车都将采用那个定义的形式,尽管每辆汽车都采用不同的值。本文稍后将介绍,软件开发中有不同的面向对象途径。
常用 OOP 术语
为最大限度地利用本文,您至少应该熟悉面向对象编程及其概念。下面简要描述讨论面向对象开发时使用的一些常用术语。需要说明的是,并非所有 OOP 类型都包含每个概念;例如,基于原型的对象语言(比如 JavaScript)中就没有 “类”。
类
在基于类的面向对象开发中,一个类定义组成一个对象的不同属性和函数。类定义用于生成对象的模板,因此它们应该定义这些对象能够遵守的公共属性和动作。类通常由成员变量和方法构成。
成员变量
对象的成员变量就是该对象的属性。在前面提到的汽车示例中,这些属性包括该汽车的 manufacturer
model
、color
、cubic capacity
,等等。
方法
方法是对象能够执行的动作。例如,一辆汽车能够 accelerate
、brake
、turn
,等等。通常,方法将修改成员变量的值。例如,当一个 car
对象使用 accelerate
方法加速时,它的当前速度属性将增加。许多对象都有一个称为 constructor 的方法,该方法在对象创建后立即被调用。
实例或对象
实例或对象即实际对象本身,而不是用于定义对象的模板。例如,您可能有一个名为 myCar
的对象,它拥有一个汽车模板的属性和方法。在一个对象的实例中,属性将实际拥有值。例如,myCar
可能拥有一个值为 silver
的 color 属性,一个值为 2500
的 cubic capacity 属性。一个对象的属性的当前值称为该对象的状态,该状态可以在该对象的整个生命周期内变化。
继承性
在基于类的 OOP 中,继承性是这样一个过程:子类继承其父类的成员变量和方法。除了继承这些属性和动作之外,子类可以定义自己的成员变量和方法,并提供父类的属性的默认值。例如,您可能有一个 FourByFour
类,它是 Car
类的一个子类。这个子类可以将其父类的 drivetrain 属性的默认值设置为 4WD
(四轮驱动)。另外,它可以定义另一个名为 transfer case 的属性,该属性仅适用 4x4 汽车;并定义一个方法,该方法允许您更改其他普通车辆上没有的低速档(low range gear)。
封装
在基于类的 OOP 中,成员变量通常被定义为私有变量,以免被从类本身的范围外访问或修改。有一些称为 “修改器” 的特殊方法,它们允许您定义可以检索或修改类中的私有成员函数的值的方法。这些方法(通常称为 getters 和 setters)允许程序员使用隐藏的信息,使应用程序和其他类只能访问某些属性。这种技术通常称为 “封装”。
抽象
抽象是通过只定义那些在您对象的当前上下文中对该对象很重要的属性和方法来减小对象的复杂性的过程。例如,当您定义一个 Car
类时,可以通过定义一辆汽车拥有的、对其他类型的车辆(比如有蓬货车、卡车、摩托车等)也常见的所有属性,来将这个类进一步抽象为一个 Vehicle
类。这样,Car
类将从 Vehicle
类继承这些属性,就像一个 Motorcycle
类或 Van
类那样。
多态性
在 OOP 上下文中,多态性意味着可以从它的超类继承方法,而不必提供所有方法的相同实现。例如,您有两个 Car
类的两个子类,一个用于自动挡汽车(我们称其为 ATCar
),另一个用于手动挡汽车(我们称其为 MTCar
)。所有 Car
对象都能加速,因此 ATCar
和 MTCar
都将从它们的父类继承 accelerate
方法。但是,在一个 ATCar
中,当引擎达到某个 RPM 级别时,您的 accelerate 方法将自动调用 change gear 方法。结果,您在 ATCar
子类中覆盖了 accelerate
方法的父类定义,而在 MTCar
中,该方法就是子类从 Car
类继承而来的方法。
本文并不是面向对象编程的一个全面指南。如果您不熟悉上述概念,那么在继续阅读本文之前,有必要跳到 参考资料 部分阅读更多关于 OOP 的资料。
面向对象的 JavaScript
上一小节介绍的许多概念都是称为基于类的面向对象编程的特定 OOP 范式所专用的。但是,并非所有编程语言都遵守这个范式。另一种常见 OOP 类型是基于原型的面向对象编程,JavaScript 语言中就使用该范式。在本小节中,您将了解 JavaScript 的面向对象实现,一个用例,以及您可能会遇到的一些障碍,特别是当您来自一个基于类的 OOP 环境时。
JavaScript 并不只是一种基本脚本语言
当 JavaScript 首次流行时,它主要用作一种在基本 web 页面上执行一些简单技巧的方法。大多数 JavaScript 用户都不是软件开发人员,他们是图形或 web 设计师,他们可能拥有丰富的 HTML 经验,但他们的编程语言知识很少,甚至没有。HTML 本身在支持生成动态效果方面的功能是非常有限的,而这正是 JavaScript 优势所在。然而,大多数设计师并不实际学习如何用 JavaScript 编程,而只是尽量找到它们需要的代码段,仅仅学习调试该代码段以满足自身需要的所需的知识,然后使用那段小代码。当设计师这样做过几次之后,他们就会误以为他们自己的 JavaScript 水平不错了。
在 web 早期,JavaScript 的功能很有限。但是,它现在已经成长为一个成熟的、功能齐全的编程语言,它不再只用于为网站编写一些简单技巧,而是用于驱动整个富互联网应用程序。事实上,作为一种语言,JavaScript 今天还在以各种方式应用。例如,CouchDB 面向文档数据库管理系统就使用 JavaScript 函数来查询数据库中的数据。
许多 Java 开发人员(以及使用其他更传统的编程语言的开发人员)可能会轻视 JavaScript,认为它是一种基本脚本语言。尽管 JavaScript 已经发展得非常强大,认为它只是用于在网站上执行一些小技巧的误解仍然存在。这是因为大多数使用 JavaScript 的 web 开发人员将使用 jQuery、Prototype 或 Dojo 这样的库来避免为它们编写 JavaScript 的工作。事实上,很多 web 开发人员可能被视为编写 jQuery 应用程序的专家,但对于 JavaScript 本身,他们实际上几乎没有什么专长。很多这样的开发人员没有意识到的是,JavaScript 实际上非常强大,且包含开箱即用的面向对象特性。在本小节中,您将了解这些特性。
基于原型的 OOP
JavaScript 实现的面向对象编程类型不同于 Java™ 代码中使用的类型。尽管 Java 编程基于一个基于类的 OOP 模型,但 JavaScript 基于一个不怎么基于类的 OOP 模型,这个模型称为基于原型的对象方向。对象只在需要的时候才被声明,而不是定义一组用于创建对象的类模板。当一个对象需要从另一个对象继承特性时,它可以简单地克隆一个原型对象的特性。原型 OOP 的一个关键优势是,对象原型可以在运行时修改,这意味着对象结构的定义不是严格的。大多数基于类的 OOP 编程语言不允许类在运行时动态改变(但有几个例外,比如 Perl、Python 和 Ruby)。
在下一小节中,您将学习如何使用原型模型在 vanilla JavaScript 中进行 OOP。
纯 JavaScript 中基本的面向对象示例
在本系列 第 1 部分 中,您学习了 Firefox 的 Firebug 插件,以及如何使用控制台来运行 JavaScript 代码,而不必编辑、保存和运行文件并在一个 web 页面上执行事件。打开 Firefox 并启动 Firebug。您当前所处的 web 页面无关紧要,因为这个示例不需要任何特殊 JavaScript 库。
您将创建一个充当一个对象原型的函数,而不是在 JavaScript 中声明类。然后您可以使用 new 关键字创建这个对象的一个实例。要查看这样一个示例,将清单 1 中的代码输入 Firebug 控制台:
清单 1. JavaScript 中的一个基本对象原型
function Car() { } var myCar = new Car();
console.log(myCar);
这应该在控制台日志中生成以下输出:Object {}
。
实际上,您可以单击这个输出,它将把您带到 Firebug 的另一部分,您可以在那里检查一个对象的各个属性。如果您单击这个特殊对象,您将看到一条消息 “There are no properties to show for this object”。
这段代码的实际作用是定义一个对象函数 Car()
。然后,它使用 new 的操作符实例化一个对象,最后,它将该对象输出到 Firebug 控制台,您可以在那里进一步检查它。但是,这个对象还不够完善,因此,我们让它更有趣一些(见清单 2)。
清单 2. 定义您的对象中的一个成员变量
function Car() { }
Car.prototype.current_speed = 0; var myCar = new Car();
console.log(myCar);
这次,控制台日志将输出更有趣的内容:Object { current_speed=0 }
。
单击输出将把您带到 DOM 检查器窗口,您将在该窗口中看到属性 current_speed
及其值 0
。 使用 Car
原型函数创建的任何对象将默认拥有一个值为 0 的 current_speed
属性。
如果不能加速,一辆汽车将毫无用处,因此我们向这个原型添加一个方法(见清单 3)。
清单 3. 向原型添加一个方法
function Car() { }
Car.prototype.current_speed = 0;
Car.prototype.accelerate = function(increment) { this.current_speed += increment;
} var myCar = new Car();
myCar.accelerate(30);
myCar.accelerate(20); console.log(myCar);
控制台输出应该生成以下内容:Object { current_speed=50 }
。
单击输出不仅将向您显示 current_speed
属性,还将显示该对象可用的新 accelerate
方法。在清单 3 中的示例中,您能看到如何调用一个对象的方法:使用圆点表示法,比如 object.method(arg[0], arg[1], ..., arg[N])
;在本例中为 myCar.accelerate(20)
。这种表示法可用于访问一个对象的任何属性。将行 console.log(myCar)
更改为 console.log(myCar.current_speed)
,您将看到控制台输出显示值 50
,而不是实际对象本身的一个表示。
下面,我们讨论如何在 JavaScript 中实现一个构造器函数。构造器是对象被实例化之后立即调用的函数。当您创建对象函数时,这翻倍(double)成为一个构造器函数。清单 4 展示了一个应用示例。
清单 4. 添加一个构造器函数体
function Car(reg_no) { this.reg_no = reg_no; console.log('Car with registration no. '+this.reg_no+' created.');
} Car.prototype.reg_no = '';
Car.prototype.current_speed = 0;
Car.prototype.accelerate = function(increment) { this.current_speed += increment;
} var myCar = new Car('10C500');
myCar.accelerate(30);
myCar.accelerate(20); console.log(myCar.current_speed);
在清单 4 中,Car
的 constructor
函数接受一个参数作为其注册编号。然后,它将对象实例的注册编号设置为该参数值,并向 Firebug 控制台输出一条消息,确认实例已被创建。Firebug 控制台窗口中应该显示如清单 5 所示的输出。
清单 5. 输出
Car with registration no. undefined created.
50
为演示如何在一个对象原型上实现继承性,我们来创建一个更高级的 Car
原型,并添加加速、减速和换挡三个方法(见清单 6)。
清单 6. 一个更完整的 Car
原型
function Car(reg_no) { this.reg_no = reg_no;
} Car.prototype.reg_no = '';
Car.prototype.current_speed = 0;
Car.prototype.current_gear = 0; Car.prototype.accelerate = function(increment) { this.current_speed += increment;
}
Car.prototype.decelerate = function(decrement) { this.current_speed -= decrement;
}
Car.prototype.increaseGear = function() { this.current_gear++;
}
Car.prototype.decreaseGear = function() { this.current_gear--;
}
现在我们以此为基础创建一个称为 ATCar
的继承对象原型,该原型描述一辆自动挡汽车。这个示例并不是一辆完美的汽车,因为它基于速度而不是 RPM 进行换挡,但它能够帮助您理解原型 OOP 中的继承性和多态性(见清单 7)。
清单 7. 一个 ATCar
对象原型,继承自 Car
function ATCar(reg_no) { Car.call(this, reg_no);
}
ATCar.prototype = new Car();
ATCar.prototype.constructor = ATCar; ATCar.prototype.accelerate = function(increment) { Car.prototype.accelerate.call(this, increment); if(increment >= 10) this.increaseGear();
} ATCar.prototype.decelerate = function(decrement) { Car.prototype.decelerate.call(this, decrement); if(this.current_speed === 0) this.current_gear = 0; else if(decrement >= 10) this.decreaseGear();
}
您需要注意的第一件事是:构造器使用 Car
对象原型上的 call 函数。只要一个 ATCar 被实例化,这通常会调用父原型定义上的构造器。这让您确信您实现了继承性,这样,ATCar
原型就不需要了解 Car
对象原型的内部工作原理。下面,您将把 ATCar
函数的 prototype 属性设置为 Car
函数的一个新实例。本质上,这将告知 JavaScript :您想在 ATCar
原型中继承 Car
原型的属性和方法。
在清单 7 中,您还覆盖了 accelerate
和 decelerate
方法,这样,在自动挡汽车上,加速和减速将导致自动换挡。在这两个方法中,您首先调用父原型的方法,这样您就不必重新实现实际加速,从而不必编写重复代码。由于这个示例只有一行代码,因此这在本例中并不那么重要,但是想象一下,如果这是一个复杂的函数,重复编写它将有多么痛苦!
最后,我们看看这个示例的应用情况(见清单 8)。
清单 8. 使用自动挡汽车原型
var myCar = new ATCar('10C500');
myCar.accelerate(30);
myCar.accelerate(20);
myCar.decelerate(5); console.log(myCar);
这将输出已经创建的新对象。单击该对象查看细节。结果应该如图 1 所示。
图 1. myCar
对象属性的 Firebug 视图
![myCar 对象属性的 Firebug 视图](https://i-blog.csdnimg.cn/blog_migrate/0e11db1863b1aabe16b465495891378d.png)
本文的主要目的是展示如何使用 Dojo 的基于类的 OOP 模拟特性,因此,上述内容只是纯 JavaScript 实现对象方向的原型途径的一个简介。要了解关于基于原型的 OOP 的更多信息,请参阅 参考资料。
原型 OOP 的问题
对于基于原型的面向对象编程比基于类的 OOP 好还是差,不同的人有不同的观点。根据您作为一个程序员的偏好,赞成或反对的观点都很容易驳斥。基于原型的对象方向的一个最常见的问题 — 特别是在 JavaScript 中 — 是许多开发人员对它缺乏基本了解。但是,随着对 JavaScript 有深入理解的开发人员的人数逐步上升,这个问题将不再是问题。尽管如此,许多熟悉基于类的 OOP 语言(比如 Java 语言)的程序员仍然乐于坚持使用一个基于类的系统,Dojo 提供了一些很棒的特性来模拟这种类型的系统,以便您以类似的方式编写 JavaScript 代码。下一节解释如何使用 Dojo 的这些特性在 JavaScript 中创建基于类的应用程序。
使用 Dojo 模拟基于类的 OOP
在深入讨论 Dojo 的基于类的模拟之前,重要的是要注意到,到目前为止,Dojo 仍然是一个 JavaScript 库。Java 代码和 JavaScript 不是一回事;事实上,它们的差别很大。Dojo 并不试图迫使 JavaScript 像 Java 代码那样操作,相反,它允许 Java(和其他基于类的 OOP 语言)开发人员以一种他们熟悉的方式使用 JavaScript OOP,而底层结构仍然以一种原型方式工作。
使用 dojo.declare
创建类
要使用 Dojo 创建类,可以使用 dojo.declare
函数。现在,我们使用这个函数来创建一个 Car
类(见清单 9)。
清单 9. 使用 dojo.declare 创建一个 Car
类
dojo.declare("Car", null, { }); var myCar = new Car();
console.log(myCar);
这是创建一个类并实例化该类的一个对象的基本 shell。dojo.declare
函数接受 3 个参数:
- 类名
- 类继承的超类
- 包含该类的所有属性和方法的一个对象
在清单 9 中的示例中,您声明了一个名为 Car 的类,它不从任何超类继承,也没有任何成员变量和方法。如果您通过在 Firebug 控制台中单击输出来查看 myCar
对象的对象属性,您将看到类似图 2 的画面。
图 2. 从一个 Dojo
类创建的一个基本对象
![Firebug 窗口,显示 Car 为一个已声明的类,以及它的构造器。](https://i-blog.csdnimg.cn/blog_migrate/1fbbbf8effc721dbdaabe3902c6d3010.png)
如您所见,在 Dojo 中创建一个类将默认向从该类生成的任何对象赋予一些属性和方法。您现在的类不怎么有趣,因此,我们来添加一些属性和方法,以及一个构造器,就像上一小节中演示面向原型的方法时所做的那样(见清单 10)。
清单 10. 一个更完整的 Car
类
dojo.declare("Car", null, { reg_no: "", current_speed: 0, current_gear: 0, constructor: function(reg_no) { this.reg_no = reg_no; }, accelerate: function(increment) { this.current_speed += increment; }, decelerate: function(decrement) { this.current_speed -= decrement; }, increaseGear: function() { this.current_gear++; }, decreaseGear: function() { this.current_gear--; }
});
如您所见,与 vanilla JavaScript 声明对象原型的方法相比,这种类声明方式更容易。在继续讨论继承性之前,我们先检查一下对象是否可以被实例化,以及那些方法是否有效(见清单 11)。
清单 11. 使用 Car
类
var myCar = new Car("10C500");
myCar.accelerate(30);
myCar.accelerate(20);
myCar.decelerate(5);
console.log(myCar.reg_no+" travelling at "+myCar.current_speed+" mph");
这在 Firebug 控制台中生成以下输出:10C500 travelling at 45 mph
。
继承性和多继承性
下面,创建您的自动挡子类,就像前面使用纯 JavaScript 那样,但这次是使用 dojo.declare
函数(见清单 12)。
清单 12. 使用 Dojo 创建 ATCar
子类
dojo.declare("ATCar", Car, { accelerate: function(increment) { this.inherited(arguments); if(increment >= 10) this.increaseGear(); }, decelerate: function(decrement) { this.inherited(arguments); if(decrement >= 10) this.decreaseGear(); }
});
如清单 12 所示,子类中只提供所有被覆盖的或新的属性和方法(在本例中,您只是覆盖加速和减速方法以自动换挡)。Dojo 负责自动调用超类中的构造器。如果您需要添加一个构造器函数,可以向子类添加一个构造器函数,但您不必担心调用超类构造器,因为那将自动进行。您将注意到,在两个被覆盖的方法中,行 this.inherited(arguments)
都被调用,这将调用超类中的相同方法。这将使您避免重新编写代码来执行实际加速,只需像自动挡汽车那样方便换挡即可。
我们看看这个新子类的运行情况(见清单 13)。
清单 13. 运行中的新子类
var myCar = new ATCar("10C500");
myCar.accelerate(30);
myCar.accelerate(20);
myCar.decelerate(5);
console.log(myCar.reg_no+" travelling at "+myCar.current_speed+" mph in gear "+myCar.current_gear);
这将生成以下输出:10C500 travelling at 45 mph in gear 2
。
Dojo 还支持多继承性。多继承性允许一个子类从多个父类派生,从每个父类继承属性和方法。严格说来,只有一个父类被认为是超类(数组中的第一个),但每个父类的构造器都将被调用,调用顺序与这些父类在数组中的顺序一致。
为演示多继承性,我们以一个 Smartphone 为例,除了接打电话和收发文本消息外,它还有很多功能(见清单 14)。通常,它应该还有播放音乐、观看视频等功能。为简单起见,我们假设一个 Phone 能打电话,一个 MediaPlayer 能播放视频,而一个 Smartphone 具有上述两个功能。
清单 14. Dojo 中的多继承性
dojo.declare("Phone", null, { phone_number: "", minutes_remaining: 0, constructor: function(properties) { this.phone_number = properties.phone_number; this.minutes_remaining = properties.minutes_remaining; console.log("Phone "+this.phone_number+" powered on. You have
"+this.minutes_remaining+" minute(s) remaining."); }
}); dojo.declare("MediaPlayer", null, { disk_space: 0, songs:[], constructor: function(properties) { this.disk_space = properties.disk_space; this.songs = properties.songs; console.log("Media Player powered on. You have "+this.songs.length+" songs,
with "+this.disk_space+" GB free space left."); }
}); dojo.declare("Smartphone", [Phone, MediaPlayer], { phone_id: "", constructor: function(properties) { this.phone_id = properties.phone_id; console.log("Smartphone ID "+this.phone_id+" boot up complete."); }
}); var songs = [ {artist:"U2",title:"Vertigo"}, {artist:"Coldplay",title:"Yellow"}
]; var myPhone = new Smartphone({ phone_number:"(555) 123-4567", minutes_remaining: 60, disk_space: 2.5, songs: songs, phone_id: "4345FDFD7JAPO76"
}); console.log(myPhone);
这里值得指出的第一点是:dojo.declare
是如何实现多继承性的。如您所见,一组类被传递,而不只是将父类作为第二个参数传递。这些父类的构造器将以它们在数组中的顺序自动被调用。重要的是要注意,如果每个父类构造器都接受不同的参数,那么 Dojo 将不能区分应该传递给每个构造器函数的参数。因此, 如果您需要将不同的参数传递给不同的构造器,您应该在简单 JavaScript 中以 “键/值” 对的形式添加参数并在构造器中以那种方式使用它们。
清单 15 是将在 Firebug 控制台中显示的输出。
清单 15. 多继承性示例输出
Phone (555) 123-4567 powered on. You have 60 minute(s) remaining.
Media Player powered on. You have 2 songs, with 2.5 GB free space left.
Smartphone ID 4345FDFD7JAPO76 boot up complete.
Object { phone_number="(555) 123-4567", more...}
单击最后一行中的链接将显示 myPhone
对象的属性,如图 3 所示。
图 3. 从多个父类继承而来的 myPhone
对象
![从多个父类继承而来的 myPhone 对象](https://i-blog.csdnimg.cn/blog_migrate/8f1718b4becb38923aec06d1eacb9f60.png)
在图 3 中,不同类的不同属性均出现在 Smartphone 类的实例化对象中。Phone
类的 phone_number
和 mintues_remaining
属性在那里,来自 MediaPlayer
类的 disk_space
和 songs
在那里,来自 Smartphone
子类的 phone_id
成员变量也在那里。如果这些类有一些方法,那些方法也应该在那里显示。
使用 dojo.mixin
来改进多继承性示例
Dojo 提供了一个不错的工具函数 dojo.mixin
,它允许您通过从左到右合并对象属性来混合对象(见清单 16)。
清单 16. 一个基本 dojo.mixin 示例
var objA = { a: 1, b: 2 };
var objB = { b: 3, c: 4 };
dojo.mixin(objA, objB);
console.log(objA);
将 objB
混合到 objA
中之后,objA
中的属性将如图 4 所示。
图 4. 使用 dojo.mixin
混合对象
![两列:第一列带有 abc,第二列带有 1 3 4](https://i-blog.csdnimg.cn/blog_migrate/3f9d0e0e0b2ad8a6c32f5834042b8564.png)
最初在 objA
中被设置为 2
的 b
属性已经被来自 objB
的值 3
所覆盖。而且,c
属性已经被添加。这个基本示例完成后,我们来看看如何在您的多继承性示例中使用 dojo.mixin
。
在上一个示例中创建 Phone
类时,您可能会回想起清单 17 中那个类的构造器中的两行。
清单 17. Phone
类构造器中的行
this.phone_number = properties.phone_number;
this.minutes_remaining = properties.minutes_remaining;
由于只有两行,这还不太麻烦,但如果行比较多该怎么办呢?必须以这种方式分配属性难道不是一件很令人痛苦的事吗?而这正是 dojo.mixin
函数真正有用的地方!使用下面的行替换这两行(以及 MediaPlayer
和 Smartphone
类中类似的行):dojo.mixin(this, properties);
。
结果与以前完全相同,但已经被传递到构造器的各个属性不会出现混乱。这很简洁,不是吗?
Dojo 中的打包和模块化开发
开发大型应用程序时,您很可能需要使用带有许多成员变量和方法的类。如果您来自一个 Java 开发环境,您可能更愿意遵循以下理念:不同的类应该驻留在不同的文件中,按照包进行分组。然后,当继承或其他目的需要时,再 “导入” 类,以确保它们仅在必要时才被加载。使用 JavaScript 时,没有这样的开箱即用打包和模块系统,但幸运的是,Dojo 提供了一个解决方案。
例如,在 Car
类示例中,使用 Java 代码时您可能将该类存储在一个包中,如清单 18 所示。
清单 18. 在 Java 编程中打包类
package com.ibm.developerworks.dojoseries;
public class Car { //Car class code goes here
}
稍后可以将这个类导入其他 Java 类中,如清单 19 所示。
清单 19. 在 Java 编程中导入类
package com.ibm.developerworks.dojoseries;
import com.ibm.developerworks.dojoseries.Car;
public class ATCar extends Car { //ATCar class code goes here
}
Dojo 通过 dojo.provide
和 dojo.require
函数提供了一个类似的打包系统。我们看看清单 19 中的 Java 代码在 Dojo 中是什么样子。首先,我们看看清单 20 中的 Car
类。
清单 20. 在 Dojo 中打包类
dojo.provide("com.ibm.developerworks.dojoseries.Car");
dojo.declare("com.ibm.developerworks.dojoseries.Car", null, { //Car class code goes here
});
您可能已经注意到,这与 Java 代码非常相似,尽管类的整个包路径在 dojo.provide
语句中提供,而不只是到包含包的路径。包路径很重要,因为它还决定当 Dojo 试图使用 dojo.require
加载这个类时在哪里寻找它。因此,对于清单 20 中的示例,Car.js 文件应该存储在相对路径 com/ibm/developerworks/dojoseries/Car.js 中。如果它没有存储在那个位置,那么 Dojo 在需要它时就不能正确加载它。下面,我们看看如何导入这个类并从它创建一个子类(见清单 21)。
清单 21. 在 Dojo 中导入类
dojo.provide("com.ibm.developerworks.dojoseries.ATCar");
dojo.require("com.ibm.developerworks.dojoseries.Car");
dojo.declare("com.ibm.developerworks.dojoseries.ATCar", com.ibm.developerworks.dojoseries.Car, { //ATCar class code goes here
});
您将注意到,这里再次使用 dojo.provide 语句来确定这个类的加载路径。这个特殊的类将被存储在相对路径 com/ibm/develoeprworks/dojoseries/ATCar.js 中。然后,您使用 dojo.require
来加载 Car
类 — 使用它的完整包路径。最后,您声明这个子类,将完整路径作为第二个参数传递给其父类。由于这个类现在还没有被 Dojo 加载,因此它在 DOM 中可用且可以通过它的名称直接加载,不需要被放置到一个字符串中。
尽管从技术上讲类名可以与 dojo.provide
语句中提供的路径不同(注意,使用 dojo.require
进行的任何类加载必须使用 dojo.provide
中设置的完全限定路径),但我们强烈建议不要这样做,因为这样只会导致混乱。
下一节解释如何使用 Dojo 的构建系统来打包您的应用程序,以确保它实现最佳性能和速度。
全部打包 — 使用 Dojo 的构建系统
使用 Dojo 的对象方向特性时,很有可能需要将您的类分隔到一些不同的文件中,以便使您的代码组织和管理任务变得更轻松。但是,重要的是要注意,加载许多 JavaScript 小文件可能会对您的应用程序造成严重的性能影响。每当一个 web 浏览器需要下载和执行一个 JavaScript 文件时,它都必须发送一个单独的 HTTP 请求,等待服务器做出响应,然后处理响应。因此,加载一个大文件通常比加载许多小文件更快。
使用一个大文件的问题是这可能是一个版本控制和代码组织噩梦,更不用说它可能包含您的应用程序实际上不需要的大量代码了。这个问题的解决方案就是在小文件大小和最小化您的应用程序发出的 HTTP 请求的数量之间找到适当的平衡。
这正是 Dojo 的构建系统发挥作用的地方,它允许您定义一些层,每一层都合并来自几个 JavaScript 源文件的源代码并缩小生成的单个文件, 从而保持文件大小最小。这个缩小过程能够极大地缩小您的代码,方法有二:一是移除所有不必要的空白和注释;二是将本地变量重命名为更短的名称并尽可能重构它们在函数中的使用。通过使用这个构建系统,可以使您的源代码组织良好便于开发,但是在部署到生产环境时要确保它以最优的性能水平运行。
使用 Dojo 构建系统的一个指南可能需要一篇单独的文章。幸运的是,Dojo 文档全面覆盖了这个主题。关于如何使用这个构建系统的更多信息,请参阅 参考资料。
结束语
在这个关于使用 Dojo 工具包开发基于 web 的富应用程序的三部分文章系列的第 2 部分中,您学习了对象方向的基础知识,了解了 JavaScript 如何使用一个基于原型的 OOP 方法,并认识到许多传统开发人员长期持有的、关于 JavaScript 不是一个强大的支持 OOP 的语言的认识是错误的。然后,您了解了 JavaScript 的面向对象特性,如何定义对象原型,以及如何实现继承性。您发现了如何使用 Firebug 来测试 JavaScript 代码而不必将代码保存到一个文件中。接下来,您了解了如何使用 Dojo 的 dojo.declare
函数来编写拥有 Java 开发人员熟悉的风格的 JavaScript 类。您还了解了如何执行继承性和多继承性,以及 dojo.mixin
如何使合并对象属性成为小菜一碟。您还了解到 Dojo 的包和模块函数如何将您的类分隔到单独的源文件中,这与您在传统 OOP 语言(比如 Java 语言和 C++)中所做的非常类似。最后,您发现,通过在生产环境中最小化文件大小和 HTTP 请求,Dojo 构建系统帮助您以一种促进整洁的代码组织而不会影响性能的方式进行开发。
在这个系列的第三个、也是最后一个部分中,您将看到 Dojo 的小部件平台 Dijit 如何用于创建交互式富互联网应用程序。Dijit 构建于 Dojo 的面向对象特性之上,因此您在本文中所学到的知识将在下一部分中派上用场。
下载资源
- 文章源代码 (dojo2.source.zip | 2KB)
相关主题
- 访问 Dojo Toolkit 主页。
- 观看一些 Dojo Toolkit 演示。
- Introducing The Dojo Toolkit:从 Opera 开发人员网站阅读优秀的 Dojo Toolkit 入门读物。
- Introduction to the Dojo toolkit, Part 1: Setup, core, and widgets:从 Javaworld 阅读另一篇关于 Dojo 工具包的优秀入门读物。
- Dojo 1.5: Ready to power your web app:阅读 Sitepen 上的这篇文章,了解 Dojo 1.5 中的一些新特性。
- Introduction to the Dojo Toolkit: Tutorial:阅读 Ajax Matters 上的这个入门教程。
- “使用 Dojo 国际化 Web 应用程序”(developerWorks,2008 年 8 月):找到一种使用 Dojo 工具箱的 i18n 特性在网站和 Web 应用程序的上下文中实现本地语言支持的方法。
- “利用 Dojo Toolkit 使用 web 服务”(developerWorks,2010 年 9 月):了解如何通过 Dojo Toolkit 使用服务,在 web 页面上实现 Ajax。
- 从 Douglas Crockford 了解 JavaScript 中的原型继承。
- 在 DojoCampus.org 上阅读 Read Nathan Toone 在 理解 dojo.declare、dojo.require 和 dojo.provide上的条目。
- 阅读 Dojo Confessions (Or: How I gave up my jQuery Security Blanket and Lived to Tell the Tale)(作者 Rebecca Murphey),了解作者如何从 jQuery 转向 Dojo。
- Enterprise Dojo(作者 Dan Lee)展示如何使用 Dojo 编写高度聚合的 JavaScript 代码。
- Dojo: 使用 Dojo JavaScript Library 来构建 Ajax 应用程序,作者 James E. Harmon,Addison-Wesley Professional 出版。
- Getting StartED with Dojo,作者 Kyle Hayes 和 Peter Higgins,来自 Friends of ED。
- “编写一个定制的 Dojo 应用程序”(developerWorks,2008 年 12 月):阅读这篇文章,了解更多 Dojo 信息。
- “Using the Dojo Toolkit with WebSphere Portal”(developerWorks,2007 年 11 月)介绍在 WebSphere Portal 应用程序中如何安装、配置、使用 Dojo Toolkit。
- developerWorks 的 developerWorks 中国网站 Web 开发专区 专门提供关于各种 Web 解决方案的文章。
- 下载 Dojo Toolkit。本文使用的是版本 1.5。
- 阅读 Dojo Toolkit API 文档。
- 获取 Firefox。
- 获取 Firebug。
- IBM - Dojo 扩展样例:可以使用 Dojo Extension Feature Set 启用一个 IBM WebSphere Portlet Factory 模型来利用 Dojo JavaScript Toolkit 提供的功能。
- 查看 HTML5 专题,了解更多和 HTML5 相关的知识和动向。