現在,物件導向程式設計(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
R大:
自從聽聞你要寫js的文章,就期盼許久,果然是佳作,期盼你的下一篇。
看著你文章成長的小T
小人物上籃
2008年8月28日 上午10:18