1:使用物件導向的 JavaScript
信不信由您,可以使用物件導向的方式來編寫 JavaScript。這樣做可以獲得更好的可重用代碼,組織對象以及動態地裝入對象。下面是 JavaScript 版本的購物車,其後是等效的 Java 代碼。
function Cart() {
this.items = [];
}
function Item (id,name,desc,price)) {
this.id = id;
this.name = name;
this.desc = desc;
this.price = price;
}
// Create an instance of the cart and add an item
var cart = new Cart();
cart.items.push(new Item("id-1","paper","something you write on",5));
cart.items.push(new Item("id-1","Pen", "Something you write with", 3);
var total;
while (var l; l < cart.items.length; l++) {
total = total + cart.items[l].price;
}
上面的 Cart 對象為維護內部的 Item 對象數組提供了基本支援。
購物車的等效 Java 對象表示形式如下所示。
import java.util.*;
public class Cart {
private ArrayList items = new ArrayList();
public ArrayList getItems() {
return items;
}
}
public class Item {
private String id;
private String name;
private String desc;
private double price;
public Item (String id, String name, String desc, double price) {
this.id = id;
this.name = name;
this.desc = desc;
this.price = price;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
public float getPrice() {
return price;
}
}
以上的樣本呈現了一個購物車的伺服器端對象表示形式。此對象需要由一個 JSP、Servlet 或 JSF 受管 Bean 儲存在 HttpSession 中。可以使用 AJAX 互動在購物車中添加商品或檢索目前狀態。
2:使用對象分層結構來組織 JavaScript 對象
在 JavaScript 中,可能會出現對象名稱發生衝突的情況。在 Java 中,可以使用包名稱來防止出現命名衝突。與 Java 不同的是,JavaScript 並不提供包名稱,但是您完全可以自己建立。在編寫組件時,可使用對象和對象分層結構來組織相關對象以防止出現命名衝突。下面的樣本建立了一個頂級對象 BLUEPRINTS,從某種意義上講,它相當於相關對象的名稱空間。這些對象被設定為父物件的屬性。
// create the base BLUEPRINTS object if it does not exist.
if (!BLUEPRINTS) {
var BLUEPRINTS = new Object();
}
BLUEPRINTS.Cart = function () {
this.items = [];
this.addItem = function(id) {
items.push(new Item(id);
}
function Item (id,qty) {
this.id = id;
this.qty = qty;
}
}
// create an instance of the cart and add an item
var cart = new BLUEPRINTS.Cart();
cart.addItem("id-1",5);
這種技術可以防止命名衝突,在可能發生命名衝突的地方使用這種代碼是一種很好的做法。
3:使用原型屬性來定義共用行為以及擴充項物件
原型屬性是 JavaScript 的一項語言功能。所有對象都具有這種屬性。如果在當前對象中找不到某個屬性,JavaScript 中的屬性解析功能就會查看原型屬性的值。如果定義為原型對象的對象值不包含該屬性,則會檢查其原型屬性的值。原型屬性鏈(分層結構)通常用於在 JavaScript 對象中提供繼承。下面的樣本說明了如何使用原型屬性在現有對象中添加行為。
function Cart() {
this.items = [];
}
function Item (id,name,desc,price)) {
this.id = id;
this.name = name;
this.desc = desc;
this.price = price;
}
function SmartCart() {
this.total;
}
SmartCart.prototype = new Cart();
SmartCart 擴充 Cart 對象,繼承了它的屬性並添加了一個 total 屬性。接下來,我們在 Cart 中添加一些簡便的函數,以添加商品並計算總價。雖然可以將函數直接添加到 SmartCart 對象中,但這會導致為每個 SmartCart 執行個體建立一個新函數。在 JavaScript 中,函數就是對象,因此,對於需要建立很多執行個體的對象,共用執行個體的行為可以節省資源。
下面的代碼聲明了共用的 calcualteTotal 和 addItem 函數,並將它們添加為 SmartCart 原型成員的屬性。
Cart.prototype.addItem = function(id,name,desc,price) {
this.items.push(new Item(id,name,desc,price));
}
Cart.prototype.calculateTotal = function() {
for (var l=0; l < this.items.length; l++) {
this.total = this.total + this.items[l].price;
}
return this.total;
}
// Create an instance of the cart and add an item
var cart = new SmartCart();
cart.addItem("id-1","Paper", "Something you write on", 5);
cart.addItem("id-1","Pen", "Soemthing you write with", 3);
alert("total is: " + cart.calculateTotal());
正如您所看到的那樣,對象擴充是非常簡便的。在本例中,只有一個 calcualteTotal 執行個體,addItem 函數被用於在 SmartCart 對象的所有執行個體間共用。請注意,items 的範圍仍然是 this.items,即使這些函數是從 SmartCart 對象中單獨聲明的。在執行新的原型函數時,它們將位於 SmartCart 對象的範圍中。
在可能出現很多個物件執行個體時,建議使用原型屬性來定義共用行為,因為這會減少 JavaScript 中的對象數,並且可通過使用此屬性將行為與資料完全分開。原型屬性也非常適於為對象提供預設值(此處不進行討論)。還有許多其他的功能可以通過原型屬性實現,但這已經超出了本文的討論範圍。有關詳細資料,請參見本文的“資源”部分。
4:在 JavaScript 中儲存特定於視圖的狀態並在伺服器上儲存跨頁面的狀態
在前文的樣本中,我們介紹了將購物車做為伺服器對象和作為用戶端對象的情況。在用戶端儲存狀態還是在伺服器上儲存狀態是一個難題。如果將 JavaScript 用戶端設計為單頁應用程式,則僅在用戶端上使用購物車可能比較妥當。在這種情況下,JavaScript 版本的購物車可能僅在簽出時與伺服器進行互動。
一般說來,應使用 JavaScript 對象來儲存與特定頁面有關的檢視狀態。切記,JavaScript 對象是特定於 HTML 頁面的,如果按下“重新裝入”按鈕、瀏覽器重新啟動/崩潰或者導航到另一個頁面,那麼這些對象就會丟失。
當 Java 對象的範圍為 HttpSession 時,應在伺服器上儲存跨頁面的狀態。重新整理頁面和裝入頁面時,應使用戶端和伺服器端的對象保持同步。
如果需要離線(例如,飛機旅行途中)開發 AJAX 用戶端,可以使用多種方法在用戶端上儲存狀態,但這些都不是標準的方法。Dojo 提供了 dojo.storage API,可以在 JavaScript 用戶端上儲存最多 100KB 的離線使用內容。隨著用戶端儲存標準的出現,相信將對此 API 進行相應的修改以支援該標準。如果要儲存的狀態是保密的,或者需要由多台電腦訪問該狀態,則應考慮將狀態儲存在伺服器上。
5:編寫可重用的 JavaScript
除非絕對必要,否則,不應將 JavaScript 綁定到一個特定組件上。切勿在可進行參數化的函數中對資料進行固定編碼。下面的樣本展示了一個可重用的 autocomplete JavaScript 函數。
<script type="text/javascript">
doSearch(serviceURL, srcElement, targetDiv) {
var targetElement = document.getElementById(srcElement);
var targetDivElement = document.getElementById(targetDiv);
// get completions based on serviceURL and srcElement.value
// update the contents of the targetDiv element
}
</script>
<form onsubmit="return false;">
Name: <input type="input" id="ac_1" autocomplete="false" onkeyup="doSearch('nameSearch','ac_1','div_1')">
City: <input type="input" id="ac_2" autocomplete="false" onkeyup="doSearch('citySearch','ac_2','div_2')">
<input type="button" value="Submit">
</form>
<div class="complete" id="div_1">
</div>
<div class="complete" id="div_2">
</div>
上面樣本中的 doSearch() 函數可以被重用,因為它是使用元素的字串 id、服務 URL 以及要更新的 <div> 進行參數化的。這個指令碼可以被隨後的其他頁面或應用程式使用。
6:使用物件類型作為靈活的函數參數
物件類型是使用花括弧 ({}) 定義的對象,其中包含一組用逗號分隔的索引值,與 Java 中的映射非常類似。
{key1: "stringValue", key2: 2, key3: ['blue','green','yellow']}
上面的樣本展示了一個包含字串、數字和字串數組的物件類型。正如所想像的一樣,物件類型是非常便於使用的,因為它可以被當作通用對象為函數傳遞參數。如果選擇在函數中使用更多屬性,函數簽名並不會發生變化。請考慮使用物件類型作為方法參數。
function doSearch(serviceURL, srcElement, targetDiv) {
var params = {service: serviceURL, method: "get", type: "text/xml"};
makeAJAXCall(params);
}
function makeAJAXCall(params) {
var serviceURL = params.service;
...
}
還要注意,可通過使用物件類型來傳遞匿名函數,如下面的樣本所示:
function doSearch() {
makeAJAXCall({serviceURL: "foo", method: "get", type: "text/xml", callback: function(){alert('call done');}});
}
function makeAJAXCall(params) {
var req = // getAJAX request;
req.open(params.serviceURL, params.method, true);
req.onreadystatechange = params.callback;
...
}
請不要將物件類型與使用類似文法的 JSON 相混淆。
7:將內容、CSS 和 JavaScript 完全分開
一個功能豐富的 Web 應用程式的使用者介面是由內容 (HTML/XHTML)、樣式 (CSS) 和 JavaScript 組成的。JavaScript 是至關重要的,原因是使用者操作(如按一下滑鼠)將調用它,並且它可以對內容進行處理。通過將 CSS 樣式與 JavaScript 分開,可以使代碼更易於管理和定製,並且可讀性更高。建議將 CSS 和 JavaScript 放在單獨的檔案中。
如果從基於 Java 的組件(如 JSP、Servlet 或 JSF 轉譯器)中呈現 HTML,您很可能需要在每頁中輸出樣式和 JavaScript(如下所示)。
<style>
#Styles
</style>
<script type="text/javascript">
// JavaScript logic <script>
<body>The quick brown fox...</body>
如果在每個頁面中都嵌入內容,則每次裝入頁面時,可能會產生頻寬裝入開銷。通過引用內容而不是嵌入內容(如下所示),可以對 JavaScript 和 CSS 檔案進行緩衝並在不同頁面中重用。
<link rel="stylesheet" type="text/css" href="cart.css">
<script type="text/javascript" src="cart.js">
<body>The quick brown fox...</body>
可以將連結映射到伺服器上的靜態資源,或者將其映射到動態產生資源的 JSP、Serlvet 或 JSF 組件上。如果開發的是 JSF 組件,則請考慮使用 Shale Remoting,它提供了一些基本類,這些類提供了用於編寫指令碼/CSS 連結的核心功能,並且甚至能夠訪問 JSF 組件的 JAR 檔案中的資源。
在 JavaScript 代碼中,請在動態更改元素樣式時使用 element.className 而不是 element.setAttribute("class", STYLE_CLASS),因為絕大多數瀏覽器都支援 element.className(這也是支援 IE 樣式更改的最有效方法)。
在 JavaServer Faces (JSF) 和純 Java 標記庫的標記庫定義中,不能使用屬性 "class" 來指定樣式,因為所有 Java 對象中都包含 getClass() 方法,並且無法覆蓋該方法。請改用屬性名稱 "styleClass"。
8:避免在 JavaScript 中儲存靜態內容
像在其他原始碼中一樣,在 JavaScript 中應該使用最低限度的靜態 HTML/XHTML 內容。這會使升級靜態內容的管理變得更加容易。您可能希望在不更改原始碼的情況下使組件內容具有可升級性。靜態內容可以從代碼中提取出,其方式類似於 Java 中使用 ResourceBundles 時的情況,也可以使用一個 AJAX 請求進行裝入。這一設計提供了一種建立本地化介面的方法。
下面的 XML 文檔 resources.xml 和程式碼片段展示了如何從 JavaScript 代碼中提取靜態資源。
<resources>
<resource id="foo">
<value>bar</value>
</resource>
<resource id="fooy">
<value>fooy</value>
<value>bar</value>
</resource>
</resources>
通過將 HTML 頁面映射到 window.onload 函數裝入 HTML 頁面時,將裝入 JavaScript 裝入函數。
function load() {
// load the first set of images
// get resources.xml with an AJAX request
// and give the responseXML to processResults()
}
function Resource (id, values) {
this.name = id;
this.value = values.join();
this.values = values;
}
var resources = new Object();
// parse the XML returned from an AJAX request
function processResults(responseXML) {
var resourceElements = responseXML.getElementsByTagName("resource");
for (var l=0; l < resourceElements.length; l++) {
var resourceElement = resourceElements[l];
var id = resourceElement.getAttribute("id");
var valueElements = resourceElement.getElementsByTagName("value");
var values = [];
for (var vl=0; vl < valueElements.length; vl++) {
var value = valueElements[vl].firstChild.nodeValue;
values.push(value);
}
resources[id] =new Resource(id,values);
}
alert ("foo=" + resources['fooy'].value);
}
此代碼對上面的 XML 文檔進行分析並將每個資源放入一個資來源物件中。可以使用 JSON 發送相同的資料,然而,使用 XML 可能是較好的方法,此處,如果選擇以 XML 形式發送本地化的內容,則便於提供一個方法以支援本地化的內容。
與 GUI 組件類似,當靜態內容可能來自外部資源,在靜態地設定布局尺寸時要非常小心,因為新內容可能會超過組件的邊界。
9:慎用 element.innerHTML
您可能更願意使用 element.innerHTML 而不是 DOM 樣式 element.appendChild(),因為 element.innerHTML 編程要容易得多,並且瀏覽器處理它的速度比使用 DOM API 要快得多。但一定要瞭解與這種方法有關的缺點。
如果選擇使用 element.innerHTML,請嘗試編寫產生最低限度的 HTML 的 JavaScript。應依靠 CSS 來改進表示形式。切記,應始終儘力將內容與表示形式分開。請確保在重新設定 element.innerHTML 之前,在元素的現有 innerHTML 中取消註冊事件偵聽程式,因為它會導致記憶體流失。切記,在替換內容時,element.innerHTML 內的 DOM 元素將會丟失,並且對這些元素的引用也會丟失。
請考慮使用 DOM API(如 element.appendChild())在 JavaScript 中動態建立元素。DOM API 是標準的 API,在建立元素時它們以編程方式對這些作為變數的元素進行訪問,並且更易於避免使用 element.innerHTML 時遇到的諸如記憶體流失或丟失引用等問題。這就是說,切記使用 DOM 在 IE 中建立表可能會出現問題,因為 IE 中的 API 並不遵循 DOM。有關所需的 API,請參見關於表的 MSDN 文檔。在 IE 中添加表以外的元素不會出現問題。
10:慎用關閉
關閉易於建立,但在某些情況下,可能會導致出現問題。習慣了物件導向的開發人員可能傾向於將對象的相關行為與對象綁定在一起。下面的樣本展示了一個將導致記憶體流失的關閉。
function BadCart() {
var total;
var items = [];
this.addItem = function(text) {
var cart = document.getElementById("cart");
var itemRow = document.createElement("div");
var item = document.createTextNode(text);
itemRow.appendChild(item);
cart.appendChild(item);
itemRow.onclick = function() {
alert("clicked " + text);
}
}
}
那麼,此樣本到底錯在什麼地方呢?addItem 引用的是不在 BadCart 範圍中的 DOM 節點。匿名 "onClick" 處理常式函數將一直保留對 itemRow 的引用,不允許對其進行記憶體回收,除非我們顯式地將 itemRow 設定為 null。GoodCart 中的以下代碼將會解決此問題:
function GoodCart() {
var total;
var items = [];
this.addItem = function(text) {
var cart = document.getElementById("cart");
var itemRow = document.createElement("div");
var item = document.createTextNode(text);
itemRow.appendChild(item);
cart.appendChild(item);
itemRow.onclick = function() {
alert("clicked " + text);
}
itemRow = null;
item = null;
cart = null;
}
}
對 itemRow 進行設定,將刪除設定為 itemRow.onclick 處理常式的匿名函數對它的外部參考。一種很好的做法是,進行清理並將 item 和 cart 設定為 null 以便對所有內容進行記憶體回收。另一個防止記憶體流失的解決方案是,不要使用外部函數來替代 BadCart 中的匿名函數。
如果使用關閉,切勿使用關閉的局部變數保留對瀏覽器對象(如與 DOM 相關的對象)的引用,因為這可能會導致記憶體流失。應在刪除對象的所有引用後,再對對象進行記憶體回收。有關關閉的詳細資料,請參見 Javascript 關閉。