Javascript 是最低级的Web 编程接口,随处可见。随着Web 日益成为日常生活的一部分,Javascript 也开始变得备受关注。Javascript 是一个经常遭到误解的语言,被认为是一种玩具语言或者一种“不成熟的Java™ 语言”。Javascript 最饱受非议的特性之一是它的原型对象系统。尽管不可否认Javascript 是存在一些缺陷,但原型对象系统并不在其内。在本文中,我们将了解功能强大、简洁、典雅的Javascript 原型的面向对象编程。
对象的世界
当您开始新的一天时(开车去上班,坐在办公桌前执行一个任务,吃一顿饭,逛逛公园),您通常可以掌控您的世界,或者与之交互,不必了解支配它的具体物理法则。您可以将每天面对的各种系统看作是一个单元,或者是一个对象。不必考虑它们的复杂性,只需关注您与它们之间的交互。
历史
Simula 是一种建模语言,通常被认为是第一个面向对象(Object-oriented, OO) 的语言,随后出现的此类语言包括Smalltalk、C++、Java 和C#。那时,大多数面向对象的语言是通过类来定义的。后来,Self 编程语言(一个类似Smalltalk 的系统)开发人员创建了一种可替代的轻量级方法来定义这类对象,并将这种方法称为基于原型的面向对象编程或者原型对象编程。
终于,使用一种基于原型的对象系统将Javascript 开发了出来,Javascript 的流行将基于原型的对象带入了主流。尽管许多开发人员对此很反感,不过仔细研究基于原型的系统,就会发现它的很多优点。
面向对象的编程(Object-oriented, OO) 试图创建工作原理相似的软件系统,面向对象编程是一个功能强大的、广泛流行的、用于软件开发的建模工具。 面向对象编程之所以流行,是因为它反映了我们观察世界的方法:将世界看作是一个对象集合,可与其他对象进行交互,并且可以采用各种方式对其进行操作。面向对象编程的强大之处在于其两个核心原则:
封装允许开发人员隐藏数据结构的内部工作原理,呈现可靠的编程接口,使用这些编程接口来创建模块化的、适应性强的软件。我们可以将信息封装视为信息隐藏。
继承增强封装功能,允许对象继承其他对象的封装行为。我们可以将信息继承视为是信息共享。
这些原则对于大多数开发人员来说是众所周知的,因为每个主流编程语言都支持面向对象编程(在很多情况下是强制执行的)。尽管所有面向对象语言都以这样或那样的形式支持这两个核心原则,但多年来至少形成了2 种定义对象的不同方法。
在本文中,我们将了解原型对象编程和Javascript 对象模式的优势。
什么是Prototypo?类和原型的关系
类提供对象的抽象定义,为整个类或对象集合定义了共享的数据结构和方法。每个对象都被定义为其类的一个实例。类还有根据其定义和(可选)用户参数来构造类对象的责任。
一个典型的示例是Point 类及其子类Point3D,用来分别定义二维点和三维点。清单1 显示了Java 代码中的类。
清单1. Java Point 类
class Point {
private int x;
private int y;
static Point(int x, int y) {
this.x = x;
this.y = y;
}
int getX() {
return this.x;
}
int getY() {
return this.y;
}
void setX(int val) {
this.x = val;
}
void setY(int val) {
this.y = val;
}
}
Point p1 = new Point(0, 0);
p1.getX() // => 0;
p1.getY() // => 0;
// The Point3D class 'extends' Point, inheriting its behavior
class Point3D extends Point {
private int z;
static Point3D(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
int getZ() {
return Z;
}
void setZ(int val) {
this.z = val;
}
}
Point3D p2 = Point3D(0, 0, 0);
p2.getX() // => 0
p2.getY() // => 0
p2.getZ() // => 0
和通过类来定义对象相比,原型对象系统支持一个更为直接的对象创建方法。例如,在Javascript 中,一个对象是一个简单的属性列表。每个对象包含另一个父类或原型的一个特别引用,对象从父类或原型中继承行为。您可以使用 Javascript 模拟Point 示例,如清单2 所示。
清单2. Javascript Point 类
var point = {
x : 0,
y : 0
};
point.x // => 0
point.y // => 0
// creates a new object with point as its prototype, inheriting point's behavior
point3D = Object.create(point);
point3D.z = 0;
point3D.x // => 0
point3D.y // => 0
point3D.z // => 0
传统对象系统和原型对象系统有本质的区别。传统对象被抽象地定义为概念组的一部分,从对象的其他类或组中继承一些特性。相反,原型对象被具体地定义为特定对象,从其他特定对象中继承行为。
因此,基于类的面向对象语言具有双重特性,至少需要2 个基础结构:类和对象。由于这种双重性,随着基于类的软件的发展,复杂的类层次结构继承也将逐渐开发出来。通常无法预测出未来类需要使用的方法,因此,类层次结构需要不断重构,让更改变得更轻松。
基于原型的语言会减少上述双重性需求,促进对象的直接创建和操作。如果没有通过类来束缚对象,则会创建更为松散的类系统,这有助于维护模块性并减少重构需求。
直接定义对象的能力将会加强和简化对象的创建和操作。例如,在清单2 中,仅用一行代码即可声明您的point 对象:var point = { x: 0, y: 0 };。仅使用这一行代码,就可以获得一个完整的工作对象,从Javascript Object.prototype(比如toString 方法)继承行为。要扩展对象行为,只需使用point 将另一个对象声明为其原型。相反,即使最简洁的传统面向对象语言,也必须先定义一个类,然后在获得可操作对象之前将其实例化。要继承有关行为,可能需要定义另一个类来扩展第一个类。
原型模式理论上比较简单。作为人类,我们往往习惯于从原型方面思考问题。例如,Steve Yegge 在博客文章“The Universal Design Pattern”(请参阅参考资料)中讨论过,以橄榄球运动员Emmitt Smith 为例,谁拥有速度、敏捷性和剪力,谁就将成为美国国家橄榄球联盟(National Football League,NFL)所有新成员的榜样。当一个新跑步运动员LT 发挥超常捡到球时,评论员通常会这样说:
“LT 有双Emmitt 的腿。”
“他就像Emmitt 一样自由穿过终点线。”
“他跑一英里只用5 分钟!”
评论员以原型对象Emmitt Smith 为模型来评论新对象LT。在Javascript 中,这类模型看起来如清单3 所示。
清单3. Javascript 模型
var emmitt = {
// ... properties go here
};
var lt = Object.create(emmitt);
// ... add other properties directly to lt
您可以将该示例与经典模型进行比较,在经典模型中您可能会定义一个继承自FootballPlayer 类的RunningBack 类。LT 和Emmitt 可能是RunningBack 的实例。这些Java 代码编写的类看起来如清单4 所示。
清单4. 3 个Java 类
class FootballPlayer {
private string name;
private string team;
static void FootballPlayer() { }
string getName() {
return this.name;
}
string getTeam() {
return this.team;
}
void setName(string val) {
this.name = val;
}
void setTeam(string val) {
this.team = val;
}
}
class RunningBack extends FootballPlayer {
private bool offensiveTeam = true;
bool isOffesiveTeam() {
return this.offensiveTeam;
}
}
RunningBack emmitt = new RunningBack();
RunningBack lt = new RunningBack();
经典模型通常伴随着极大的概念上的负担,但是对类实例emmitt 和lt(您得到的原型模型),并没有提供细小的控制。(公平地说,FootballPlayer 类并不是100% 需要,这里提供它只是为了与下一个示例进行比较 )。有时,这项开销是有益的,但通常都是一个包袱。