拥抱原型面向对象编程

标签: 程序员 Javascript OOP 面向对象编程 | 发表时间:2012-12-12 11:32 | 作者:
出处:http://blog.jobbole.com

来源: IBM DeveloperWorks

简介: 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 对象模式的优势。

javascript logo

什么是 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% 需要,这里提供它只是为了与下一个示例进行比较 )。有时,这项开销是有益的,但通常都是一个包袱。

使用原型对象系统模仿经典建模非常容易。(也可能会适得其反,尽管这种可能并不容易出现 )例如,您可以使用另一个从 FootballPlayer 继承的对象  runningBack 作为原型,创建一个对象  footballPlayer。在 JavaScript 中,这些对象看起来如清单 5 所示。

清单 5. JavaScript 建模

var footballPlayer = {
    name : "";
    team : "";
};

var runningBack = Object.create(footballPlayer);
runningBack.offensiveTeam = true;

您也可以创建另一个从  footballPlayer 继承的  lineBacker 对象,如清单 6 所示。

清单 6. 对象继承

var lineBacker = Object.create(footballPlayer);
lineBacker.defensiveTeam = true;

如清单 7 所示,通过向  footballPlayer 对象添加行为,可以同时向  lineBacker 和  runningBack 对象添加行为。

清单 7. 添加行为

footballPlayer.run = function () { this.running = true };
lineBacker.run();
lineBacker.running; // => true

在该示例中,您可以将  footballPlayer 视为一个类,也可以为 Emmitt 和 LT 创建对象,如清单 8 所示。

清单 8. 创建对象

var emmitt = Object.create(runningBack);
emmitt.superbowlRings = 3;

var lt = Object.create(emmitt);
lt.mileRun = '5min';

因为  lt 对象继承自  emmitt 对象,您甚至可以将  emmitt 对象视为一个类,如清单 9 所示。

清单 9. 继承和类

emmitt.height = "6ft";
lt.height // => "6ft";

如果您在以静态的典型对象为特色的语言(像 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. 声明一个对象

function Point(x, y) {
    this.x = x;
    this.y = y;
}

您可以使用  new 关键字来创建对象,这类似于 Java 代码,如清单 11 所示。

清单 11. 创建对象

var p = new Point(3, 4);
p.x // => 3
p.y // => 4

在 JavaScript 中,函数也是对象,因此,可以将方法添加到构造函数原型中,如清单 12 所示。

清单 12. Adding a method

Point.prototype.r = function() {
    return Math.sqrt((this.x * this.x) + (this.y * this.y));
};

有了构造函数,您就可以使用一个 pseudoclassical 继承模式,如清单 13 所示。

清单 13. Pseudoclassical 继承模式

function Point3D(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
}

Point3D.prototype = new Point(); // inherits from Point

Point3D.prototype.r = function() {
    return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
};

尽管这的确是在 JavaScript 中定义对象的一个有效方法(有时候可能也是最好的方法),但是此方法感觉有点笨拙。与追随原型模式和纯粹使用这种风格来定义对象相比,这为您的代码增加了不必要的东西。简要概括一下,您可以使用对象标识符来定义您的对象,如清单 14 所示。

清单 14. 定义对象

var point = {
    x: 1,
    y: 2,
    r: function () {
        return Math.sqrt((this.x * this.x) + (this.y * this.y));
    }
};

如清单 15 所示,随后可以使用  Object.create 来实现继承。

清单 15. 使用  Object.create 继承

var point3D = Object.create(point);
point3D.z = 3;
point3D.r = function() {
    return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
};

这种对象创建方法在 JavaScript 中很自然,强调了原型对象的优势。但是,原型和 pseudoclassical 模式都有的一个缺点,它们不会提供任何成员隐私。有时候成员隐私无关紧要,但有时候却很重要。清单 16 展示了一个允许您使用私有成员创建对象的模式。在 Douglas Crockford 撰写的书籍  JavaScript: The Good Parts 中,将其称之为函数继承模式。

清单 16. 函数继承模式

var point = function(spec) {
    var that = {};

    that.getTimesSet = function() {
        return timesSet;
    };

    that.getX = function() {
        return spec.x;
    };

    that.setX = function(val) {
        spec.x = val;
    };

    that.getY = function() {
        return spec.y;
    };

    that.setY = function(val) {
        spec.y = val;
    };

    return that;
};

var point3D = function(spec) {
    var that = point(spec);

    that.getZ = function() {
        return spec.z;
    };

    that.setZ = function(val) {
        spec.z = val;
    };

    return that;
};

使用一个构造器来生成您的代码,并在其中定义私有成员,通过将一个  spec 传递给构造器来创建实例,如清单 17 所示。

清单 17. 创建实例

var p = point({ x: 3, y: 4 });
p.getX();  // => 3
p.setX(5);

var p2 = point3D({ x: 1, y: 4, z: 2 });
p.getZ();  // => 2
p.setZ(3);

结束语

本文只是对原型对象编程进行了简要介绍。其他许多语言,比如 Self、Lua、Io 和 REBOL 都实现了原型模式。原型模式可以用任何语言(包括静态类型语言)来实现,这在设计一些需要简单和灵活的系统时很有帮助。

原型对象编程提供强大功能和简单性,并使以明确而又优雅的方式实现了面向对象编程目标。它是 JavaScript 的一项资产,而不是毒瘤。

 

相关文章

相关 [拥抱 原型 面向对象] 推荐:

拥抱原型面向对象编程

- - 博客 - 伯乐在线
来源: IBM DeveloperWorks. 简介: JavaScript 是最低级的 Web 编程接口,随处可见. 随着 Web 日益成为日常生活的一部分,JavaScript 也开始变得备受关注. JavaScript 是一个经常遭到误解的语言,被认为是一种玩具语言或者一种 “不成熟的 Java™ 语言”.

面向对象的Shell脚本

- jejer - 酷壳 - CoolShell.cn
还记得以前那个用算素数的正则表达式吗. 编程这个世界太有趣了,总是能看到一些即别出心裁的东西. 你有没有想过在写Shell脚本的时候可以把你的变量和函数放到一个类中. 不要以为这不可能,这不,我在网上又看到了一个把Shell脚本整成面向对象的东西. Shell本来是不支持的,需要自己做点东西,能搞出这个事事的人真的是hacker啊.

如此理解面向对象编程

- - 酷壳 - CoolShell.cn
从Rob Pike 的 Google+上的一个推看到了一篇叫《 Understanding Object Oriented Programming》的文章,我先把这篇文章简述一下,然后再说说老牌黑客Rob Pike的评论. 先看这篇教程是怎么来讲述OOP的. 它先给了下面这个问题,这个问题需要输出一段关于操作系统的文字:假设Unix很不错,Windows很差.

前端开发之面向对象

- - 新浪UED
Javascript 面向对象. 实例讲解 (PADMAIL项目UI组件设计与实现). 【一】 面向对象的基本概念. 面向对象的英文全称叫做Object Oriented,简称OO. OO其实包括OOA(Object Oriented. Analysis,面向对象分析)、OOD(Object Oriented Design,面向对象设计)和OOP(Object.

面向对象设计模式的核心法则

- tonytech - Michael`s blog
有本经典的书叫《设计模式》,讲了经典的21种设计模式,建议大家都看看. 就一个类而言,应该仅有一个引起它变化的原因. 如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力. 这种耦合会导致脆弱的设计,当变化发生时,设计会遭到意想不到的破坏. 软件设计真正要做的许多内容,就是发现职责并把那些职责互相分离.

面向对象的 JavaScript 编程及其 Scope 处理

- zhibin - IBM developerWorks 中国 : Web development : Articles,Tutorials
在面向对象的 JavaScript 编程中,我们常常会将一些数据结构和操作封装成对象以达到继承和重用的目的. 然而层层封装和继承再加上 JavaScript 中特殊关键字 this 的使用,使得 JavaScript 当前运行 Context 看起来非常的混乱. 很多 developer 为了获取运行时正确的 Context,常常不得已将 function 或者 Object 声明在全局 Global Context 中.

看透面向对象的复用技术

- - 博客 - 伯乐在线
来源: RayChase 的博客. 本文翻译自 这篇文章,这篇文章写于1998年,作者是Scott Ambler,真的挺久远了. 看看上个世纪末的时候, 程序员的视角和观点. 想从面向对象复用技术中真正获益,你就必须理解不同种类的复用,并且自如地在不同场合下使用它们. 复用性是面向对象技术带来的很棒的潜在好处之一.

面向对象设计原则:不要STUPID,坚持GRASP和SOLID

- - CSDN博客架构设计推荐文章
不要STUPID,坚持GRASP和SOLID. 有人可能会说:这是描述设计原则的一个专业术语,由我们可爱的代码整洁之道传教者鲍勃(罗伯特C. 马丁)大叔提出,是一组用于指导我们如何写出“好代码”的原则. 在编程界充满了这样由单词首字母组成的缩略词. 其它类似的例子还有DRY(Don’t Repeat Yourself.

JavaScript 游戏中的面向对象的设计

- - 博客 - 伯乐在线
来源: IBM developerworks. 简介: 从程序角度考虑,许多 JavaScript 都基于循环和大量的 if/else 语句. 在本文中,我们可了解一种更聪明的做法 — 在 JavaScript 游戏中使用面向对象来设计. 本文将概述原型继承和使用 JavaScript 实现基本的面向对象的编程 (OOP).

面向对象设计的设计原则

- - 博客园_知识库
  在设计面向对象的程序的时,模式不是一定要套的,但是有一些原则最好是遵守. 这些原则已知的有七个,包括:开闭原则、里氏代换原则、依赖倒转原则、合成/聚合复用原则、迪米特法则、接口隔离原则,单一职责原则. Martin引入了SOLID的说法,包括了其中的五个原则. 另外两个,这里把他们算成额外的两个规则.