認識 JavaScript 的物件導向技術

如果要說 JavaScript全世界被誤解最深的程式語言,其實一點也不為過。長久以來,它的名稱讓許多人誤以為是 Java 的子集合。當初 JavaScript 是以作為給非程式人員的腳本語言為訴求,所以學習門檻低,很容易就能上手。正因為如此,它也讓許多人停留在粗糙、簡單的既定印象,即便到現在已經進化成一個物件導向程式語言(Object-Oriented Programming Language, OOPL)。

現在,物件導向程式設計(OOP)是已普遍被應用到許多 JavaScript 程式庫(如 AJAX Library)。如果,你要使用這些程式庫,你就有必要深入理解 JavaScript 語言的概念,才能靈活運用它來應付更複雜的 Web 應用程式。

然而 JavaScript 對 OOP 的支援方式,卻與其他以類別為基礎(Class-Based)的物件導向語言大相逕庭。本文接下來,將帶你初探 JavaScript 語言對 OOP 的支援能力。

物件
JavaScript 物件是索引鍵和值配對(Name-Value Pair)的集合。「名稱」的部分必須是字串,而「數值」可以是任何資料型別。它的資料結構跟 C# 中的 Dictionary 和 Hashtable 非常相近。

以下是建立物件的基本方法:
var rectangle = new Object();
rectangle.width = 5;
rectangle.height = 3;
rectangle.getArea = function() {
return width * height;
}
alert(rectangle.getArea()); // Displays "15"

以上範例程式碼同時呼叫 new 和建構函式,會建立新的 Object 物件。然後,才指派屬性及方法給物件變數,而所謂的方法,其實也只是參考到 Function 物件的屬性而已。這個範例顯示,JavaScript 物件的屬性不一定要事先宣告,你可以在任何時候加入額外的屬性。如果你把 JavaScript 物件它當成是在 Dictionary 物件,將指派屬性視同是加入索引鍵和值,應該就不難理解了。

你也可以使用 JavaScript 1.2 版本支援的物件實體語法(Object Literal),來宣告及初始化物件:
var rectangle = {
"width" : 5,
"height" : 3,
"getArea" : function() {
return width * height;
}
};
alert(rectangle.getArea()); // Displays "15"

每個屬性初始設定以逗號分隔的定義方式,與 C# 3.0 的物件初始設定式非常類似,唯一的差別在於這裡只接受字串索引鍵。

當要存取物件的屬性時,你可以使用熟悉的 "." (點)運算子來存取。
rectangle.width = 5;
var width = rectangle.width;

或者,使用 "[]" 運算子來取得及設定物件的屬性。
rectangle["width"] = 5;
var width = rectangle["width"];

當然,你也可以在任何時候移除物件的屬性:
delete rectangle["width"];
delete rectangle.height;

函數也是物件
JavaScript 函式其實就是包含可執行程式碼的 Function 物件。它被視為第一級物件(First-Class Object),這意味它可以被動態建立,儲存在變數、陣列和物件中,也能做為函式的「傳入參數」或「傳回結果」。
function add(x, y) {
return x + y;
}
alert(add(1, 2)); // Displays "3"

當你使用 function 關鍵字宣告函式,其實就是在定義一個 Function 物件。事實上,在執行時期會配置與函數同名的 Function 物件。以上面的範例來說,會建立一個名為 add 的函數物件。所以,你才可以在函數物件變數使用 "()" 運算子呼叫函式程式碼。

除了具名函數外,你也可以宣告匿名(Anonymous)函數,並傳回它的參照:
var add = function(x, y) {
return x + y;
};
alert(add(1, 2)); // Displays "3"

當然,你也可以在宣告的同時,直接呼叫匿名函數,並取得傳回值:
var result = (function(x, y) {
return x + y;
})(1, 2);
alert(result); // Displays "3"

另外,你也可以使用 Function 建構函式建立函數物件:
var add = new Function("x", "y", "return x + y;");
alert(add(1, 2)); // Displays "3"

Function 函數比較特殊,即使沒有使用 new 關鍵字也會產生相同的結果。

在匿名函式中,如果你要在函式內部遞迴呼叫自己,可以使用 arguments.callee 的屬性。當函式被呼叫時,會自動建立包含所有傳遞參數值的陣列物件,並指派給名為 arguments 的區域變數。該物件提供了一個叫做 arguments.callee 的屬性,這個屬性會指向的目前的函式,因此可以用來做遞迴呼叫:
var fso = new ActiveXObject("Scripting.FileSystemObject");
(function(folder) {
var files = new Enumerator(folder.Files);
while(!files.atEnd()) {
document.write(fso.BuildPath(folder.path, files.item().name));
files.moveNext();
}
var subfolders = new Enumerator(folder.SubFolders);
while(!subfolders.atEnd()) {
arguments.callee(subfolders.item());
subfolders.moveNext();
}
})(fso.GetFolder("d:\\"));

以上範例,使用遞迴方式列舉根目錄下的所有子目錄及檔案。

自訂物件
通常,我們所說的物件是指是類別或結構的實體。然而,JavaScript 只有建構函式,卻沒有類別。所謂的建構函式,其實就是一般的函式。最接近類別的方式,是定義如下的建構函式:
function Shape(x, y) {
this.x = x;
this.y = y;
this.getCoordinates = function(){
return "(" + this.x + " ," + this.y + ")";
}
}
var objShape = new Shape(5, 10);
alert(objShape.getCoordinates()); // Displays "(5, 10)"

上例中的 x 、y 是屬性成員,getCoordinates() 是方法成員。在這裡第一次出現尚未提過的 "this" 關鍵字,稍後會有詳細說明。請先看這行程式碼:
var objShape = new Shape(5, 10);

當你使用 "new" 運算子呼叫建構函式時,JavaScript Engine 會執行以下動作:
  • 建立新的 Object 物件。
  • 在 Shape 函式的執行環境中,將所建立的物件參考指派給 this 值。
  • 接著,將此物件的 constructor 屬性指向 Shape 函數,以及將其內部隱含的 __proto__ 屬性指向 Shape 函數物件的 prototype 屬性。
  • 然後,執行 Shape 函式內的程式碼。
  • 最後,傳回物件參考。

請記得,除了 Function 函式外,在一般情況下,只要使用 new 運算子呼叫函式,就會傳回完全初始化的 Object 物件。

JavaScript 的程式碼都是在執行環境(Execution Context)中執行,執行環境包含變數範圍鏈(Scope Chain)的資訊以及呼叫此方法的物件參照(也就是 this 值)。而可執行程式碼又分為 Global Code 、 Function Code 及 Eval Code 三種。不同種類的程式碼,會建立不同的執行環境,而 this 值需視執行環境或呼叫者而定:
  • Global Code
    任何在函式以外(不屬於函式的一部分)的程式碼。只會產生一個 Global 執行環境。這時的 this 值就會是 Global 物件(在瀏覽器中即為 window 物件)。
  • Function Code
    宣告於函式本體的程式碼。每次的函式呼叫都會建立個別的執行環境。如果函式是透過物件參考來呼叫,那麼 this 值就會被設定為該物件參考。若是使用 new 運算子呼叫函式,則 this 值會參考到新產生的 Object 物件。如果是透過函式物件的 call() 或是 apply() 方法來呼叫,則需視傳入的第一個參數而定。
  • Eval Code
    透過 eval() 函式執行的程式碼。其 this 值與所在的執行環境的 this 值相同。

原型繼承
原型物件(Prototype)是 JavaScript 用來模擬類別階層(Class Hierarchy)的中心概念物件。每個函式物件(fucntion 型別的物件)都有一個 prototype 屬性(該物件所擁有的原型物件),它可以用來加入自訂屬性及方法。所謂的原型物件,其實也是使用 Object 建構函式所建立的物件。另外,所有物件實體內部還隱含一個名為 __proto__ 的屬性,它會指向其建構函式的 prototype 屬性。當然,建構函式的 prototype 屬性參照的原型物件也隱含有 __proto__ 屬性,指向它的建構函式的原型物件,以此類推,最後追朔到最終基底原型 Object.prototype 為止。每一個物件都會繼承一整鏈的原型,這樣的鏈結關係稱之為原型鍊(Prototype Chain)。

我們都知道 Object 的原型物件擁有以下的屬性成員:
  • constructor
  • toString()
  • toLocaleString()
  • valueOf()
  • hasOwnProperty(propertyName)
  • isPrototypeOf(objectRef)
  • isPropertyEnumerable(propertyName)

當你建立自訂物件時,將繼承 Object 物件原型的所有屬性和方法。
function MyClass() {

}
var myObject = new MyClass();
alert(myObject.toString()); // Displays "[object Object]"

上例中,我們透過自訂的物件參照呼叫 toString() 方法。事實上,這個方法是來自 Object 的原型物件。那麼 JavaScrpt 是如何解析呢?當你試圖要存取物件的屬性/方法時,沒有定義在物件中,那麼 JavaScript 就會檢查該物件的原型。如果還是沒有,就會循我們前面所提到的原型鏈往上尋找,直到 Object.prototype 為止。

現在,你已暸解 JavaScript 是如何以原型鏈來模擬類別階層關係。接下來,我們就可以利用原型物件來實做衍生類別:
function Shape(x, y) {
this.x = x;
this.y = y;
}
Shape.prototype = {
getCoordinates : function() {
return "(" + this.x + " ," + this.y + ")";
}
};
function Rectangle(x, y, width, height) {
Shape.call(this, x, y);
this.width = width;
this.height = height;
}
Rectangle.prototype = new Shape();
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
var objRect = new Rectangle(100, 200, 5, 10);
alert(objRect.getCoordinates()); // Displays "(100, 200)"
alert(objRect.getArea()); // Displays "50"

在上例中,Shape 可視為基底類別(Base Class),而 Rectangle 是衍生類別(Derived Class)。其中,繼承的關鍵在於設定 Rectangle.prototype 屬性,將 Shape 物件加入 Rectangle 的原型鏈。另外,在 Rectangle 建構函式中,透過函數物件的 call() 方法,呼叫 Shape 建構式來進行初始化。

結語
為什麼要使用 OOP ?說穿了,就是為了能夠讓程式設計更接近人類的自然思維,讓程式碼易於擴充與維護。 當你使用 OOPL 撰寫程式碼時,你就已經是在 "OOP" 了。正所謂「只在此山中,雲深不知處」,差別只在於你對物件導向是否有所體認罷了。誠如 Kenming 在「不要從程式語言學習「物件導向」!」一文中提到:

因為,物件導向是一種思維,是一種哲理,是一種典範,甚至是一種生活觀,你需要綜合相當多的知識,蘊化為 "智慧",來協助你如何應付與應對軟體的 "善變",並能提供具體的解決方案。

雖然 JavaScript 對 OOP 可提供很好的支援,但對習慣類別型程式設計的人來說,或許會有很大的落差。所幸,未來 JavaScript 2.0 版本將朝向類別型語言發展。不過,在此之前如果你希望使用 JavaScript 撰寫更具彈性的物件導向程式,可以參考 MooTools 所提供的程式庫,它提供了絕佳的類別函式,讓你可以更容易撰寫類別式的 JavaScript 程式碼。

參考資料:
使用物件導向技術來建立進階 Web 應用程式
Javascript Closures


Share/Save/Bookmark

1 comments :: 認識 JavaScript 的物件導向技術

  1. R大:

    自從聽聞你要寫js的文章,就期盼許久,果然是佳作,期盼你的下一篇。

    看著你文章成長的小T

張貼留言