天天看點

JavaScript的弱類對象及繼承實作方式

轉自:http://fdream.net/blog/article/604.aspx

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

原文位址:JavaScript’s class-less objects

請尊重個人勞動,轉載請注明出處: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屬性
  • 最後,有很多方式來實作繼承,你可以任意挑選,這完全取決于你的手頭任務、你個人喜好、團隊喜好、你的心情或者目前的月相。

作者及聲明

Stoyan Stefanov是一名資深Yahoo!開發者,YSlow工具的上司、開源貢獻者,部落格作者和技術作者,最近由Packt出版的《Object-Oriented JavaScript》(《面向對象的JavaScript》)的作者。

shiny object的例子靈感來源于Jim Bumgardner的《Theory of the Precious Object 》。

參考《Douglas Crockford’s beget object》

繼續閱讀