拥抱原型面向对象编程 |
|
![]() |
发起人:suk 回复数:0 浏览数:3019 最后更新:2012/12/17 10:22:39 by suk |
| 选择查看 | 帖子排序: |
|
suk 发表于 2012/12/17 10:22:39
|
|
拥抱原型面向对象编程 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 类
和通过类来定义对象相比,原型对象系统支持一个更为直接的对象创建方法。例如,在JavaScript 中,一个对象是一个简单的属性列表。每个对象包含另一个父类或原型的一个特别引用,对象从父类或原型中继承行为。您可以使用 JavaScript 模拟Point 示例,如清单2 所示。 清单2. JavaScript Point 类
传统对象系统和原型对象系统有本质的区别。传统对象被抽象地定义为概念组的一部分,从对象的其他类或组中继承一些特性。相反,原型对象被具体地定义为特定对象,从其他特定对象中继承行为。 因此,基于类的面向对象语言具有双重特性,至少需要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 模型
您可以将该示例与经典模型进行比较,在经典模型中您可能会定义一个继承自FootballPlayer 类的RunningBack 类。LT 和Emmitt 可能是RunningBack 的实例。这些Java 代码编写的类看起来如清单4 所示。 清单4. 3 个Java 类
经典模型通常伴随着极大的概念上的负担,但是对类实例emmitt 和lt(您得到的原型模型),并没有提供细小的控制。(公平地说,FootballPlayer 类并不是100% 需要,这里提供它只是为了与下一个示例进行比较 )。有时,这项开销是有益的,但通常都是一个包袱。 使用原型对象系统模仿经典建模非常容易。(也可能会适得其反,尽管这种可能并不容易出现 )例如,您可以使用另一个从FootballPlayer 继承的对象runningBack 作为原型,创建一个对象footballPlayer。在JavaScript 中,这些对象看起来如清单5 所示。 清单5. JavaScript 建模
您也可以创建另一个从footballPlayer 继承的lineBacker 对象,如清单6 所示。 清单6. 对象继承
如清单7 所示,通过向footballPlayer 对象添加行为,可以同时向lineBacker 和runningBack 对象添加行为。 清单7. 添加行为
在该示例中,您可以将footballPlayer 视为一个类,也可以为Emmitt 和LT 创建对象,如清单8 所示。 清单8. 创建对象
因为lt 对象继承自emmitt 对象,您甚至可以将emmitt 对象视为一个类,如清单9 所示。 清单9. 继承和类
如果您在以静态的典型对象为特色的语言(像Java 代码)中尝试使用上述示例,则必须使用装饰模式,还需要使用更多的概念开销,并且仍然无法作为一个实例直接从emmitt 对象继承。相反,基于原型的语言(比如JavaScript)中使用的属性模式使您能够以更为自由的方式装饰您的对象。 JavaScript 不是Java 语言 JavaScript 和它的一些特性(比如,原型对象)成为了历史错误和营销决策的不幸受害者。例如,Brendan Eich(JavaScript 之父) 在一篇博客文章中曾谈到为什么需要一种新语言:“来自上层工程管理者的绝对指令是:这种语言必须‘看起来像Java’。这排除了Perl、Python、Tcl 和Scheme。” 因此,JavaScript 看起来像Java 代码,而且它的名称也与Java 有一定关联,这会让那些对这两种语言都不熟悉的人感到迷惑。尽管JavaScript 表面上看起来像Java 语言,但从更深层次上看,它一点也不像Java,这导致一些人无法达到预期目标。用Brendan Eich 的话来说: 我并不感到骄傲,但是我很高兴我选择了Scheme-ish 的一流函数和Self-ish(尽管有些奇怪)原型作为主要组成部分。Java 的影响,特别是它对y2k Date 对象以及原语与对象的区别(例如,string 与String)的影响非常大。 难以实现的期望是很难处理的。如果您期望获得一个静态的企业级语言,像Java 那样,但最终得到的是一个语法上像Java,行为上更像Scheme 和Self 的语言,您当然会感到惊讶。如果您喜欢动态语言,这可能会是一个很受欢迎的惊喜;如果您不喜欢或者您只是对它们不熟悉,使用JavaScript 编程可能不怎么令人愉快。 JavaScript 也有一些本质上的缺陷:强制使用global 变量、作用域问题、== 不一致问题等。对于这些问题,JavaScript 程序员开发了一组模式和最佳实践,帮助实现可靠的软件开发。下一小节我们将讨论几个使用、避免使用和充分利用JavaScript 原型对象系统的模式。 JavaScript 对象模式 为了使JavaScript 看起来像是Java 代码,设计人员提供了一些构造函数,这在典型语言中是必需的,但在原型语言中通常是不必要的开销。对于下列模式,可以使用构造函数来声明对象,清单10 所示。 清单10. 声明一个对象
您可以使用new 关键字来创建对象,这类似于Java 代码,如清单11 所示。 清单11. 创建对象
在JavaScript 中,函数也是对象,因此,可以将方法添加到构造函数原型中,如清单12 所示。 清单12. Adding a method
有了构造函数,您就可以使用一个pseudoclassical 继承模式,如清单13 所示。 清单13. Pseudoclassical 继承模式
尽管这的确是在JavaScript 中定义对象的一个有效方法(有时候可能也是最好的方法),但是此方法感觉有点笨拙。与追随原型模式和纯粹使用这种风格来定义对象相比,这为您的代码增加了不必要的东西。简要概括一下,您可以使用对象标识符来定义您的对象,如清单14 所示。 清单14. 定义对象
如清单15 所示,随后可以使用Object.create 来实现继承。 清单15. 使用Object.create 继承
这种对象创建方法在JavaScript 中很自然,强调了原型对象的优势。但是,原型和pseudoclassical 模式都有的一个缺点,它们不会提供任何成员隐私。有时候成员隐私无关紧要,但有时候却很重要。清单16 展示了一个允许您使用私有成员创建对象的模式。在Douglas Crockford 撰写的书籍JavaScript: The Good Parts 中,将其称之为函数继承模式。 清单16. 函数继承模式
使用一个构造器来生成您的代码,并在其中定义私有成员,通过将一个spec 传递给构造器来创建实例,如清单17 所示。 清单17. 创建实例
结束语 本文只是对原型对象编程进行了简要介绍。其他许多语言,比如Self、Lua、Io 和REBOL 都实现了原型模式。原型模式可以用任何语言(包括静态类型语言)来实现,这在设计一些需要简单和灵活的系统时很有帮助。 原型对象编程提供强大功能和简单性,并使以明确而又优雅的方式实现了面向对象编程目标。它是JavaScript 的一项资产,而不是毒瘤。 |