简介: 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
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
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
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
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
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 个基础结构:类和对象。由于这种双重性,随着基于类的软件的发展,复杂的类层次结构继承也将逐渐开发出来。通常无法预测出未来类需要使用的方法,因此,类层次结构需要不断重构,让更改变得更轻松。