1. 程式人生 > >JavaScript的弱類物件及繼承實現方式

JavaScript的弱類物件及繼承實現方式

這篇文章是Yahoo!的一名資深開發人員寫的,對於JavaScript的弱類物件及其繼承方式講得非常透徹,文章寫得很好,而自己最近又很有點翻譯慾望,於是也一併翻譯過來了。另外,MooTools 1.2.1已經發布了,修復了一些bug。

請尊重個人勞動,轉載請註明出處:http://fdream.net,譯者:Fdream

Java和JavaScript是相差極大的兩種語言,儘管他們的名字非常像,而且都有類C的語法風格,很多時候這讓人們很迷惑。(Fdream注:曾有人在論壇上問Java和JavaScript是什麼關係?我一師兄的回答非常經典:雷鋒和雷峰塔的關係。)我們來看看兩者最主要的區別——物件是怎樣建立的。在Java中,你有類。然後是物件,又叫例項,都是基於那些類建立的。而在JavaScript中,沒有類存在,物件更像是一個包含鍵值對(key-value pair)的雜湊表(hash table)。然後繼承是什麼樣的呢?好,我們一步一步來。

JavaScript物件

當你考慮一個JavaScript物件時,想一下hash。它們和物件完全一樣——它們都是名值對(name-value pair)集合,值可以是其它任何東西,包括物件和函式。當一個物件的屬性是函式的時候,你也可以叫它們方法。

這是一個空物件:

    var myobj = {};

現在,你可以開始給這個物件新增一些有意義的功能:

    myobj.name = "My precious";
    myobj.getName = function() {return this.name};

注意這樣一些事情:

  • 在方法中,this指向當前物件,和期望一樣
  • 你可以在任何時候新增、修改、刪除屬性,不限於建立的時候

另一種建立物件並同時新增屬性或者方法的方式是這樣的:

    var another = {
      name: 'My other precious',
      getName: function() {
        return this.name;
      }
    };

這種語法就叫做“物件列舉表示法”(object literal notation)——你把所有的東西都包含在花括號 { 和 } 之間,並用逗號在物件內部區分每個屬性。鍵值對(key:value pair)則以冒號分割。這中語法也不是建立物件的唯一方式。

建構函式

另一種建立JavaScript物件的方式就是使用建構函式(constructor function)。這裡是一個建構函式示例:

    function ShinyObject(name) {
      this.name = name;
      this.getName = function() {
        return this.name;
      }
    }

現在,我們可以更像Java那樣建立一個物件:

    var my = new ShinyObject('ring');
    var myname = my.getName();       // "ring"

建立一個建構函式的語法和其他函式沒有任何區別,唯一的區別是它們的用法不一樣。如果你用new關鍵字來呼叫一個方法,它會建立並且返回一個物件。通過使用this關鍵字,你可以在它返回之前修改這個物件。作為約定俗成的習慣,建構函式的命名通常以一個大寫字母開頭,以區分於其他一般函式和方法。

哪一種方式更好呢?物件列舉還是建構函式?這完全取決於你指定的任務。例如,如果你需要建立許多不同的,但是類似的物件,使用類類(class-like)的建構函式可能才是正確的選擇。但是,如果你的物件更接近於一個單例(singleton),物件列舉方式肯定要更簡單更簡短。

好了,如果沒有類,那麼哪來繼承呢?在我們回答這個問題之前,這裡還有一點驚喜——在JavaScript中,函式(function)也是實際物件。

(實際上,在JavaScript中,幾乎所有東西都是一個物件,出了一些元資料型別——字串(string)、布林值(boolean)、數字(number)和undefined。函式(function)是物件,陣列(array)是物件,甚至null也是一個物件。而且,元資料型別也可以轉換並作為物件使用,因此”string.length“是有效的。)

函式物件和原型物件

在JavaScript中,函式是物件。他們可以賦值給變數,你可以給它們新增屬性和方法等等。這裡是一個函式的示例:

    var myfunc = function(param) {
      alert(param);
    };

這和下面的幾乎一樣:

    function myfunc(param) {
      alert(param);
    }

不管你通過什麼方式建立這個函式,它最後都成為了一個myfunc物件,你可以得到它們的屬性和方法:

    alert(myfunc.length);     // 顯示 1, 引數個數
    alert(myfunc.toString()); // 顯示這個函式的原始碼

一個有趣的屬性是——每個函式物件都有一個prototype屬性。一旦你建立一個函式,它就會自動獲得一個prototype屬性,這個屬性指向一個空的物件。當然,你可以修改那個空物件的屬性。

    alert(typeof myfunc.prototype); // 顯示 "object"
    myfunc.prototype.test = 1; // 這是完全可以的

問題是:這個原型物件有什麼用呢?只有當你把一個函式作為建構函式呼叫來建立一個物件時有用。當你這麼做的時候,這個物件自動地獲得一個祕密連結指向原型物件的屬性,並可以把這些屬性當作自己的屬性一樣訪問。迷惑了?讓我們看一個例子:

一個新函式:

    function ShinyObject(name) {
      this.name = name;
    }

給這個函式的原型屬性增加一些功能:

    ShinyObject.prototype.getName = function() {
      return this.name;
    };

把這個函式作為建構函式使用,來建立一個物件:

    var iphone = new ShinyObject('my precious');
    iphone.getName(); // returns "my precious"

正如你所看到的,新的物件自動獲得了原型物件的屬性。當一些功能可以”免費“地獲得的時候,這就有點像是程式碼重用和繼承了。

通過原型繼承

現在我們來看看,你如果通過使用原型來實現繼承。

這裡是一個建構函式,將會作為父類(parent)使用:

    function NormalObject() {
      this.name = 'normal';
      this.getName = function() {
        return this.name;
      };
    }

這裡是第二個建構函式:

    function PreciousObject(){
      this.shiny = true;
      this.round = true;
    }

現在是繼承部分:

    PreciousObject.prototype = new NormalObject();

可不是嘛!現在你可以建立一個珍寶(precious)物件,然後它們會得到所有普通物品(normal)物件的功能:

    var crystal_ball = new PreciousObject();
    crystal_ball.name = 'Ball, Crystal Ball.';
    alert(crystal_ball.round); // true
    alert(crystal_ball.getName()); // "Ball, Crystal Ball."

注意到我們為什麼需要使用new來建立一個物件,然後把它賦值給原型,因為原型僅僅只是一個物件。不像一個建構函式繼承於其它的,本質上,我們從一個物件繼承。JavaScript沒有類從其他類繼承,只有物件從其他物件繼承。

如果你有幾個建構函式都要從NormalObject繼承,你也許需要每次都建立一個new NormalObject(),但是這是沒有必要的。甚至連整個NormalObject建構函式都不是必須的。另外一種實現方式就是建立一個單例普通物件,然後把它作為其他物件的基類使用。

    var normal = {
      name: 'normal',
      getName: function() {
        return this.name;
      }
    };

然後PreciousObject就可以像這樣繼承了:

    PreciousObject.prototype = normal;

通過複製屬性繼承

因為繼承只是為了程式碼重用,因此還有一種實現方式就是簡單地複製屬性。

假設你有這些物件:

    var shiny = {
       shiny: true,
       round: true
    };
    var normal = {
      name: 'name me',
      getName: function() {
        return this.name;
      }
    };

怎樣讓shiny得到normal的屬性呢?這裡有一個簡單的extend()函式,可以迴圈遍歷並賦值屬性:

    function extend(parent, child) {
      for (var i in parent) {
        child[i] = parent[i];
      }
    }
    extend(normal, shiny); // inherit
    shiny.getName(); // "name me"

現在這個屬性賦值看起來像是額外的開銷,而且效能也不是很好,但是事實是,在大多數情況下它還是很好的。你也可以看見——這是一種實現混合繼承和多重繼承的簡單方式。

Crockford的beget object

Douglas Crockford,一代JavaScript大師,JSON的創造者,提出了這樣一種有趣的begetObject()方式來實現繼承:

    function begetObject(o) {
      function F() {}
      F.prototype = o;
      return new F();
    }

這裡你建立了一個臨時建構函式,因此你可以使用原型功能,這個目的在於你建立了一個新的物件,不過不是一個全新的物件,而是從其它物件那裡繼承了一些已經存在的功能。

父物件:

    var normal = {
      name: 'name me',
      getName: function() {
        return this.name;
      }
    };

一個從父物件繼承的新物件:

    var shiny = begetObject(normal);

給這個新物件增加更多功能:

    shiny.round = true;
    shiny.preciousness = true;

YUI的extend()

讓我們來總結一下另一種方式來實現繼承,這可能是最接近Java的,因為在這種方法中,它看起來像一個建構函式繼承自其它建構函式,因此她看起來有一點像從一個類繼承。

在非常受歡迎的YUI JavaScript庫(Yahoo! User Interface)中已經使用了這種方法,這裡是一個簡單的版本:

    function extend(Child, Parent) {
      var F = function(){};
      F.prototype = Parent.prototype;
      Child.prototype = new F();
    }

通過這個方法,你傳遞兩個建構函式,第一個(子類)將會通過原型(prototype)屬性得到第二個(父類)的所有屬性和方法。

總結

讓我們很快地總結一下我們剛才所學的有關JavaScript的內容:

  • JavaScript中沒有類
  • 物件從物件繼承
  • 物件列舉表示法 var o = { };
  • 建構函式提供類Java語法 var o = new Object();
  • 函式是物件
  • 所有的函式物件都有一個prototype屬性
  • 最後,有很多方式來實現繼承,你可以任意挑選,這完全取決於你的手頭任務、你個人喜好、團隊喜好、你的心情或者當前的月相。

作者及宣告