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 关闭。