認識 JavaScript 的物件導向技術

1 comments
如果要說 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

繼續閱讀...

使用 AJAX 動態載入縣市和地區

2 comments
在 Web 應用程式中,常常都需要提供可以讓使用者登錄個人資料(如姓名、聯絡電話、通訊地址)的互動式網頁。以地址輸入欄位來說,通常還會加入縣市及鄉鎮區的連動式下拉選單(Cascading Drop-Down)供使用者選擇。在 ASP.NET 中,傳統作法是使用兩個 DropDownList 控制項,並透過 AutoPostBack 的方式擷取選取項目,再動態將資料填入 DropDownList 控制項,來達到多層連動下拉選單的效果。不過,這樣的缺點是每次變更清單選取項目時,都會造成自動向伺服器發出網頁請求,增加網路傳輸量。所幸,你可以選擇 ASP.NET AJAX Control Toolkit 的 CascadingDropDown 控制項,來做為替代方案。

ASP.NET AJAX 已內建於 ASP.NET 3.5,所以只要下載 AJAX Control Toolkit 元件即可。如果是 ASP.NET 2.0 則必須先安裝 ASP.NET 2.0 AJAX Extensions 1.0,才可以使用 ASP.NET AJAX Control Toolkit。你可以造訪 ASP.NET AJAX 官方網站取得相關下載。

ASP.NET AJAX Control Toolkit 同時也提供了完整的範例程式碼,除了可以讓你快速學習外,也可以立即引用到你的 ASP.NET 開發專案。本文程式碼亦是以 CascadingDropDown 範例為基礎修改而來。接下來,將使用 ASP.NET 3.5 來說明如何使用 CascadingDropDown 控制項,並搭配 Web 服務來動態載入縣市及鄉鎮區資料項。

建立資料來源
在這裡我們使用 XML 文件來儲存縣市及鄉鎮區的資料,如以下所示:
<?xml version="1.0" encoding="utf-8" ?>
<PostCodeService>
<county name="台北市">
<town name="中正區" value="100"/>
<town name="大同區" value="103"/>
<town name="中山區" value="104"/>
<town name="松山區" value="105"/>
<town name="大安區" value="106"/>
<town name="萬華區" value="108"/>
<town name="信義區" value="110"/>
<town name="士林區" value="111"/>
<town name="北投區" value="112"/>
<town name="內湖區" value="114"/>
<town name="南港區" value="115"/>
<town name="文山區" value="116"/>
</county>
</PostCodeService>

建立 Web Service
為了讓 AJAX 用戶端指令碼能存取 Web 服務,我們必須建立 .asmx Web 服務,並且以 ScriptServiceAttribute 屬性限定 Web 服務類別。
using System;
using System.Collections.Specialized;
using System.Web;
using System.Web.Services;
using System.Xml;
using AjaxControlToolkit;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class PostCodeService : System.Web.Services.WebService
{
private static XmlDocument _document;
private static object _lock = new object();

public static XmlDocument Document
{
get
{
lock (_lock)
{
if (_document == null)
{
_document = new XmlDocument();
_document.Load(
HttpContext.Current.Server.MapPath("~/App_Data/PostCodeService.xml"));
}
}
return _document;
}
}

public static string[] Hierarchy
{
get { return new string[] { "county", "town" }; }
}

[WebMethod]
public CascadingDropDownNameValue[] GetDropDownContents(string knownCategoryValues,
string category)
{
StringDictionary knownCategoryValuesDictionary =
CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);
return CascadingDropDown.QuerySimpleCascadingDropDownDocument(
Document, Hierarchy, knownCategoryValuesDictionary, category);
}
}

以上程式碼使用 Singleton Pattern 建立 XmlDocument 物件來載入 XML 文件,並加入 lock 陳述式以確保只會產生唯一個實例(Instance)。

AJAX 用戶端網頁
<table>
<tr>
<td>縣市:</td>
<td><asp:DropDownList ID="CountyDropDownList" Width="100" runat="server" /></td>
</tr>
<tr>
<td>鄉鎮區:</td>
<td><asp:DropDownList ID="TownDropDownList" Width="100" runat="server" AutoPostBack="true"
OnSelectedIndexChanged="TownDropDownList_SelectedIndexChanged" /></td>
</tr>
<tr>
<td>郵遞區號:</td>
<td>
<asp:UpdatePanel ID="PostCodeUpdatePanel" runat="server" UpdateMode="Conditional"
RenderMode="inline">
<ContentTemplate>
<asp:TextBox ID="PostCodeTextBox" Width="100" runat="server"></asp:TextBox>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="TownDropDownList"
EventName="SelectedIndexChanged" />
</Triggers>
</asp:UpdatePanel>
</td>
</tr>
</table>
<ajaxToolkit:CascadingDropDown ID="CountyCascadingDropDown" runat="server" TargetControlID="CountyDropDownList"
Category="county" PromptText="請選擇縣市" LoadingText="載入中..."
ServicePath="PostCodeService.asmx" ServiceMethod="GetDropDownContents" />
<ajaxToolkit:CascadingDropDown ID="TownCascadingDropDown" runat="server" TargetControlID="TownDropDownList"
Category="town" PromptText="請選擇鄉鎮區" LoadingText="載入中..."
ServicePath="PostCodeService.asmx" ServiceMethod="GetDropDownContents" ParentControlID="CountyDropDownList" />


 Download source code - 430 KB for ASP.NET 3.5

繼續閱讀...

Regular Expression Examples

0 comments
對於要做複雜的文字處理的應用程式,規則運算式(Regular Expression)提供有效率、功能強大的解決方法,是不可或缺的工具。話雖如此,要熟記規則運算式的所有技巧並不容易。為方便隨查即用,在此把自己常用的規則運算式範例作個總整理。

浮點數
^([-+]?[0-9]*\.?[0-9]+)$

下列程式碼範例會驗證輸入字串是否為有效的浮點數。
bool IsValidFloat(string inputString)
{
return Regex.IsMatch(inputString, @"^([-+]?[0-9]*\.?[0-9]+)$");
}

日期格式
^((?:19|20)\d\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$

下列程式碼範例會驗證輸入字串是否為有效的日期。
bool IsValidDate(string inputString)
{
Match m = Regex.Match(inputString,
@"^((?:19|20)\d\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$");
if (m.Success)
{
int year = int.Parse(m.Groups[1].Value);
int month = int.Parse(m.Groups[2].Value);
int day = int.Parse(m.Groups[3].Value);
if (day == 31 && (month == 4 || month == 6 || month == 9 || month == 11))
{
return false;
}
else if (day >= 30 && month == 2)
{
return false;
}
else if (month == 2 && day == 29
&& !(year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)))
{
return false;
}
else
{
return true;
}
}
return false;
}

電子郵件格式
^([0-9a-z]+[-._+&])*[0-9a-z]+@([0-9a-z]+[.])+[a-z]{2,3}$

下列程式碼範例會驗證輸入字串是否為有效的電子郵件格式。
bool IsValidEmail(string inputString)
{
return Regex.IsMatch(inputString, @"^([0-9a-z]+[-._+&])*[0-9a-z]+@([0-9a-z]+[.])+[a-z]{2,3}$", RegexOptions.IgnoreCase );
}

中文字
[\u4e00-\u9fa5]

擷取 HTML 元素
<([a-z]+)\b[^>]*>(.*?)</\1>

下列程式碼範例會從輸入字串中去除 HTML 標籤。
string StripHtml(string inputString)
{
return Regex.Replace(inputString,@"</?[a-z]+\b[^>]*/?>", string.Empty, RegexOptions.IgnoreCase);
}

擷取 HTML 標籤的屬性
ATTR\s*=\s*(?:"(?<1>[^"]*)"|(?<1>\S+))

下列範例會從 HTML 格式的輸入字串中比對 img 標籤,並擷取出 src 屬性,透過替代的方式將屬性值套用到有加入 onload 屬性及指令碼的 img 標籤,以限制圖片最大寬高。
string FormatPost(string message)
{
message = Regex.Replace(message, @"<img\b*[^>]*?src\s*=\s*(?:""(?<1>[^""]*)""|'(?<1>[^\']*)')\s*/?\s*>", @"<img onload=""javascript:rmwa_img_loaded(this, 600, 200)"" src=""$1"" />", RegexOptions.IgnoreCase);
return message;
}


相關參考資源:
MSDN 規則運算式語言項目
Regular Expression Library
Regular-Expressions.info

繼續閱讀...