擁抱原型物件導向程式設計

IBM DeveloperWorks發表於2012-12-12

  簡介: 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% 需要,這裡提供它只是為了與下一個示例進行比較 )。有時,這項開銷是有益的,但通常都是一個包袱。

  使用原型物件系統模仿經典建模非常容易。(也可能會適得其反,儘管這種可能並不容易出現 )例如,您可以使用另一個從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 的一項資產,而不是毒瘤。

相關文章