[JavaScript] JavaScript 的物件導向設計 (1):體驗篇

JavaScript 自從 Netscape 開發它以來,就幾乎已經確立它在 Web-based 前端應用程式的龍頭地位,即便在瀏覽器大戰第一回中勝出的微軟所開發的 VBScript 也無法取代它,除了它本身簡潔的描述式直譯語言特性外,它也是目前為止較多人認識,真正可跨平台的語言之一,隨著 Web 2.0 以及前端無刷新使用者介面的強勁需求,JavaScript 也已經成為一位合格的 Web Developer 必須要學會且熟練的程式語言,正因為它日益重要,它是否能被物件導向化就成為當初在制訂標準以及瀏覽器實作上的重點項目。畢竟物件導向語言 (C#, Java, VB.NET, Object Pascal, …) 還是程式語言的主力之一,而且物件導向程式語言的可重覆使用性 (reusability) 是最高的,所以 JavaScript 中運用物件導向的能力,將會成為 JavaScript 的基本功之一。

JavaScript 自從 Netscape 開發它以來,就幾乎已經確立它在 Web-based 前端應用程式的龍頭地位,即便在瀏覽器大戰第一回中勝出的微軟所開發的 VBScript 也無法取代它,除了它本身簡潔的描述式直譯語言特性外,它也是目前為止較多人認識,真正可跨平台的語言之一,隨著 Web 2.0 以及前端無刷新使用者介面的強勁需求,JavaScript 也已經成為一位合格的 Web Developer 必須要學會且熟練的程式語言,正因為它日益重要,它是否能被物件導向化就成為當初在制訂標準以及瀏覽器實作上的重點項目。畢竟物件導向語言 (C#, Java, VB.NET, Object Pascal, …) 還是程式語言的主力之一,而且物件導向程式語言的可重覆使用性 (reusability) 是最高的,所以 JavaScript 中運用物件導向的能力,將會成為 JavaScript 的基本功之一。

早期的時候,我自己 (以及很多書) 用的方法都是 function 導向的,也就是要什麼功能,寫函式就對了,一整個就是程序導向 (procedure-oriented) 的寫法,有寫過 C 語言的人就很清楚,它的可重覆使用性基本上很低,要達到可重覆使用的需求,一支 function 可以寫一堆指令,所以一支大型的 .js 檔案,function 上百個是常有的事,但其實它並不是不能物件導向,只是早期 JavaScript Interpreter 的速度並不快,JavaScript 的發展也遠不及 server-side technologies (ASP.NET, PHP, JSP, …),大廠的焦點都放在伺服器端開發上,對 JavaScript 和 DOM 則一笑置之,沒有投入太多心力。不過在前端使用者經驗 (user experience) 發展之下,JavaScript 開始逐漸被重視,如 AJAX 技術,它是現階段唯一可以在不刷新網頁的情況下做資料更新與控制 DOM 以修改資訊的技術,AJAX 也會用到大量的 DOM 操作,所以瀏覽器的開發團隊也開始針對 JavaScript 和 DOM 進行效能改良,目前做的最好的應該算是 Google 的 V8 JavaScript Engine,就連 server-side 的 JavaScript Server - node.js 也使用 V8 來做 JavaScript Interpreter,足見其引擎的高效能。

因為瀏覽器處理 JavaScript 的速度變快了,那麼將 JavaScript 物件導向化也不再是夢想,而是必備的條件之一了,舉凡 jQuery, ExtJS, YUI 套件等 JavaScript Framework,都已大量採用 JavaScript 物件導向的設計方式。而在 HTML5 + CSS3 這種會重度依賴 JavaScript 的新網頁設計方式逐漸成為標準之際,如果還不學會 JavaScript 物件導向設計,那麼未來在使用 JavaScript 上會落後別人很大一截,寫出來的 JavaScript 還是會回到以前程序導向的做法,很難 Do More, Write Less。

要做物件導向,當然要滿足幾個要求:

1. 要能製作物件,物件要具有屬性,方法和事件。
2. 要具有封裝,繼承與多型的能力。
3. 要具有較強的可重覆使用性。

在 JavaScript 中,要宣告物件很簡單,不過和我們寫 C#/Java 時的認知不太一樣:

   1:  function myObject()
   2:  {
   3:      // object declarations.
   4:  }

我們可以在裡面加入一些變數,例如:

   1:  function myObject()
   2:  {
   3:      var pi = 3.14159; // field.
   4:  }

在 myObject 中宣告的變數基本上就是物件導向封裝 (encapsulation) 中所要求的成員變數 (member variable),這個變數在外界是無法存取的,外界也不需要知道這個變數的存在,若是要對它控制,我們可以使用 get/set 的方法來做:

   1:  function myObject() {
   2:   
   3:      var _pi = 3.14159;
   4:   
   5:      this.getPI = function () { return _pi; }
   6:      this.setPI = function (v) { _pi = v; }
   7:   
   8:  }

注意到了嗎?在 myObject 中宣告的 getPI 和 setPI 都有用 this 做宣告,這表示這兩個方法 (method) 是公開方法,外界可以看的到,也可以取用,當然,在 myObject 中也可以包含私有方法 (private method),像這樣:

   1:  function myObject() {
   2:   
   3:      var _pi = 3.14159;
   4:   
   5:      // public method.
   6:      this.getPI = function () { return _pi; }
   7:      this.setPI = function (v) { _pi = v; }
   8:   
   9:      // private method.
  10:      function internalMethod(x) {
  11:          return x * 2;
  12:      }
  13:   
  14:  }

有了方法,當然也可以有屬性 (Property),和宣告方法很像,不過在 JavaScript 中,屬性有自己的變數儲存空間,也就是說,在物件中宣告同名的變數和屬性時,呼叫方式會有所不同,得到的結果也會有所不同。

   1:  function myObject() {
   2:   
   3:      // field
   4:      var _pi = 3.14159;
   5:   
   6:      // property
   7:      this.pi = _pi;
   8:   
   9:      // public method.
  10:      this.getPI = function () { return _pi; }
  11:      this.setPI = function (v) { _pi = v; }
  12:   
  13:      // private method
  14:      function internalMethod(x) {
  15:          return x * 2;
  16:      }
  17:   
  18:  }

這些基本的元素都有了,那麼還可不可以有事件 (Event) 呢?答案當然是肯定的,不過事件因為是可有可無,所以如果物件要引發事件的話,必須要先確認它有值,而且它的值是函式的指標。

   1:  function myObject() {
   2:   
   3:      var _pi = 3.14159;
   4:   
   5:      // event
   6:      this.OnAfterAddValue = null;
   7:      this.pi = _pi;
   8:   
   9:      this.getPI = function () { return _pi; }
  10:      this.setPI = function (v) { _pi = v; }
  11:   
  12:      this.addValue = function (x, y) {
  13:          return x + y;
  14:      };
  15:   
  16:      this.addValueInternal = function (x, y) {
  17:          document.writeln("invoke in addValueInternal(): " + internalMethod(x + y) + "<br />");
  18:   
  19:          // fire event
  20:          if (this.OnAfterAddValue != null && (typeof this.OnAfterAddValue === "function")) {
  21:              this.OnAfterAddValue();
  22:          }
  23:   
  24:      };
  25:   
  26:      this.getValue = function (x) {
  27:          return _pi * x;
  28:      };
  29:   
  30:      function internalMethod(x) {
  31:          return x * 2;
  32:      }
  33:   
  34:  }

現在物件有了,那我可以在建構時就設定資料嗎?當然可以,物件可以擁有建構式 (Constructor),一般來說,一個物件只能擁有一個建構式,如果要擁有多個建構式,那麼就需要較複雜的設計了。

   1:  // constructor with a parameter.
   2:  function myObject(pi) {
   3:   
   4:      var _pi = 3.14159;
   5:   
   6:      // constructor data check.
   7:      if (_pi != null && _pi != "undefined" && (!isNaN(parseFloat(pi))))
   8:          _pi = parseFloat(pi);
   9:   
  10:      this.OnAfterAddValue = null;
  11:      this.pi = _pi;
  12:   
  13:      this.getPI = function () { return _pi; }
  14:      this.setPI = function (v) { _pi = v; }
  15:   
  16:      this.addValue = function (x, y) {
  17:          return x + y;
  18:      };
  19:   
  20:      this.addValueInternal = function (x, y) {
  21:          document.writeln("invoke in addValueInternal(): " + internalMethod(x + y) + "<br />");
  22:   
  23:          if (this.OnAfterAddValue != null && (typeof this.OnAfterAddValue === "function")) {
  24:              this.OnAfterAddValue();
  25:          }
  26:   
  27:      };
  28:   
  29:      this.getValue = function (x) {
  30:          return _pi * x;
  31:      };
  32:   
  33:      function internalMethod(x) {
  34:          return x * 2;
  35:      }
  36:   
  37:  }

物件完成後,我們就可以將它獨立成一個 .js 檔案,並且在網頁中引用:

   1:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   2:  <html xmlns="http://www.w3.org/1999/xhtml">
   3:  <head>
   4:      <title></title>
   5:      <script type="text/javascript" src="myObject.js"></script>
   6:      <script type="text/javascript">
   7:   
   8:          function init() {
   9:   
  10:              var o = new myObject();
  11:              document.writeln("getValue(100) = " + o.getValue(100) + "<br />");
  12:              var o2 = new myObject(3.14);
  13:              document.writeln("getValue(100) = " + o2.getValue(100) + "<br />");
  14:   
  15:          }
  16:      
  17:      </script>
  18:  </head>
  19:  <body onload="init()">
  20:   
  21:  </body>
  22:  </html>

執行結果如下:

image

 

本文使用的 JavaScript 做法是基礎的寫法,給讀者體驗 JavaScript 的物件導向化的好處以及編寫的方式,接下來會有更多的 JavaScript 物件導向化作法,如大家熟知的繼承和多型等,也會帶入在 JavaScript 上可用的 Design Pattern。

PS: 如果要以像 jQuery 或 ExtJs 等 Framework 製作物件,那麼會和本文的作法不太相同,請參閱該 Framework 的說明。