国产AV88|国产乱妇无码在线观看|国产影院精品在线观看十分钟福利|免费看橹橹网站

javascript-gaojichengx有目錄u

發(fā)布時(shí)間:2024-12-25 | 雜志分類:其他
免費(fèi)制作
更多內(nèi)容

javascript-gaojichengx有目錄u

23.3 數(shù)據(jù)存儲(chǔ) 633 1 15 16 45 13 620 21 9 2311 12//設(shè)置 cookie,包括它的路徑、域、失效日期CookieUtil.set(\"name\", \"Nicholas\", \"/books/projs/\", \"www.wrox.com\", new Date(\"January 1, 2010\")); //刪除剛剛設(shè)置的 cookie CookieUtil.unset(\"name\", \"/books/projs/\", \"www.wrox.com\"); //設(shè)置安全的 cookie CookieUtil.set(\"name\", \"Nicholas\", null, ... [收起]
[展開]
javascript-gaojichengx有目錄u
粉絲: {{bookData.followerCount}}
文本內(nèi)容
第651頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 633

1

15

16

4

5

13

6

20

21

9

23

11

12

//設(shè)置 cookie,包括它的路徑、域、失效日期

CookieUtil.set(\"name\", \"Nicholas\", \"/books/projs/\", \"www.wrox.com\",

new Date(\"January 1, 2010\"));

//刪除剛剛設(shè)置的 cookie

CookieUtil.unset(\"name\", \"/books/projs/\", \"www.wrox.com\");

//設(shè)置安全的 cookie

CookieUtil.set(\"name\", \"Nicholas\", null, null, null, true);

CookieExample01.htm

這些方法通過(guò)處理解析、構(gòu)造 cookie 字符串的任務(wù)令在客戶端利用 cookie 存儲(chǔ)數(shù)據(jù)更加簡(jiǎn)單。

4. 子 cookie

為了繞開瀏覽器的單域名下的 cookie 數(shù)限制,一些開發(fā)人員使用了一種稱為子 cookie(subcookie)

的概念。子 cookie 是存放在單個(gè) cookie 中的更小段的數(shù)據(jù)。也就是使用 cookie 值來(lái)存儲(chǔ)多個(gè)名稱值對(duì)

兒。子 cookie 最常見的的格式如下所示。

name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5

子 cookie 一般也以查詢字符串的格式進(jìn)行格式化。然后這些值可以使用單個(gè) cookie 進(jìn)行存儲(chǔ)和訪

問(wèn),而非對(duì)每個(gè)名稱??值對(duì)兒使用不同的 cookie 存儲(chǔ)。最后網(wǎng)站或者 Web 應(yīng)用程序可以無(wú)需達(dá)到單域名

cookie 上限也可以存儲(chǔ)更加結(jié)構(gòu)化的數(shù)據(jù)。

為了更好地操作子 cookie,必須建立一系列新方法。子 cookie 的解析和序列化會(huì)因子 cookie 的期望

用途而略有不同并更加復(fù)雜些。例如,要獲得一個(gè)子 cookie,首先要遵循與獲得 cookie 一樣的基本步驟,

但是在解碼 cookie 值之前,需要按如下方法找出子 cookie 的信息。

var SubCookieUtil = {

get: function (name, subName){

var subCookies = this.getAll(name);

if (subCookies){

return subCookies[subName];

} else {

return null;

}

},

getAll: function(name){

var cookieName = encodeURIComponent(name) + \"=\",

cookieStart = document.cookie.indexOf(cookieName),

cookieValue = null,

cookieEnd,

subCookies,

i,

parts,

result = {};

if (cookieStart > -1){

cookieEnd = document.cookie.indexOf(\";\", cookieStart);

if (cookieEnd == -1){

cookieEnd = document.cookie.length;

}

cookieValue = document.cookie.substring(cookieStart +

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第652頁(yè)

634 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

cookieName.length, cookieEnd);

if (cookieValue.length > 0){

subCookies = cookieValue.split(\"&\");

for (i=0, len=subCookies.length; i < len; i++){

parts = subCookies[i].split(\"=\");

result[decodeURIComponent(parts[0])] =

decodeURIComponent(parts[1]);

}

return result;

}

}

return null;

},

//省略了更多代碼

};

SubCookieUtil.js

獲取子 cookie 的方法有兩個(gè):get()和 getAll()。其中 get()獲取單個(gè)子 cookie 的值,getAll()

獲取所有子 cookie 并將它們放入一個(gè)對(duì)象中返回,對(duì)象的屬性為子 cookie 的名稱,對(duì)應(yīng)值為子 cookie

對(duì)應(yīng)的值。get()方法接收兩個(gè)參數(shù):cookie 的名字和子 cookie 的名字。它其實(shí)就是調(diào)用 getAll()獲

取所有的子 cookie,然后只返回所需的那一個(gè)(如果 cookie 不存在則返回 null)。

SubCookieUtil.getAll()方法和 CookieUtil.get()在解析 cookie 值的方式上非常相似。區(qū)別

在于 cookie 的值并非立即解碼,而是先根據(jù)&字符將子 cookie 分割出來(lái)放在一個(gè)數(shù)組中,每一個(gè)子 cookie

再根據(jù)等于號(hào)分割,這樣在 parts 數(shù)組中的前一部分便是子 cookie 名,后一部分則是子 cookie 的值。

這兩個(gè)項(xiàng)目都要使用 decodeURIComponent()來(lái)解碼,然后放入 result 對(duì)象中,最后作為方法的返

回值。如果 cookie 不存在,則返回 null。

可以像下面這樣使用上述方法:

//假設(shè) document.cookie=data=name=Nicholas&book=Professional%20JavaScript

//取得全部子 cookie

var data = SubCookieUtil.getAll(\"data\");

alert(data.name); //\"Nicholas\"

alert(data.book); //\"Professional JavaScript\"

//逐個(gè)獲取子 cookie

alert(SubCookieUtil.get(\"data\", \"name\")); //\"Nicholas\"

alert(SubCookieUtil.get(\"data\", \"book\")); //\"Professional JavaScript\"

SubCookiesExample01.htm

要設(shè)置子 cookie,也有兩種方法:set()和 setAll()。以下代碼展示了它們的構(gòu)造。

var SubCookieUtil = {

set: function (name, subName, value, expires, path, domain, secure) {

var subcookies = this.getAll(name) || {};

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第653頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 635

1

15

16

4

5

13

6

20

21

9

23

11

12

subcookies[subName] = value;

this.setAll(name, subcookies, expires, path, domain, secure);

},

setAll: function(name, subcookies, expires, path, domain, secure){

var cookieText = encodeURIComponent(name) + \"=\",

subcookieParts = new Array(),

subName;

for (subName in subcookies){

if (subName.length > 0 && subcookies.hasOwnProperty(subName)){

subcookieParts.push(encodeURIComponent(subName) + \"=\" +

encodeURIComponent(subcookies[subName]));

}

}

if (cookieParts.length > 0){

cookieText += subcookieParts.join(\"&\");

if (expires instanceof Date) {

cookieText += \"; expires=\" + expires.toGMTString();

}

if (path) {

cookieText += \"; path=\" + path;

}

if (domain) {

cookieText += \"; domain=\" + domain;

}

if (secure) {

cookieText += \"; secure\";

}

} else {

cookieText += \"; expires=\" + (new Date(0)).toGMTString();

}

document.cookie = cookieText;

},

//省略了更多代碼

};

SubCookieUtil.js

這里的 set()方法接收 7 個(gè)參數(shù):cookie 名稱、子 cookie 名稱、子 cookie 值、可選的 cookie 失效

日期或時(shí)間的 Date 對(duì)象、可選的 cookie 路徑、可選的 cookie 域和可選的布爾 secure 標(biāo)志。所有的可

選參數(shù)都是作用于cookie本身而非子cookie。為了在同一個(gè)cookie中存儲(chǔ)多個(gè)子cookie,路徑、域和secure

標(biāo)志必須一致;針對(duì)整個(gè) cookie 的失效日期則可以在任何一個(gè)單獨(dú)的子 cookie 寫入的時(shí)候同時(shí)設(shè)置。在

這個(gè)方法中,第一步是獲取指定 cookie 名稱對(duì)應(yīng)的所有子 cookie。邏輯或操作符“||”用于當(dāng) getAll()

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第654頁(yè)

636 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

返回 null 時(shí)將 subcookies 設(shè)置為一個(gè)新對(duì)象。然后,在 subcookies 對(duì)象上設(shè)置好子 cookie 值并傳給

setAll()。

而 setAll()方法接收 6 個(gè)參數(shù):cookie 名稱、包含所有子 cookie 的對(duì)象以及和 set()中一樣的 4

個(gè)可選參數(shù)。這個(gè)方法使用 for-in 循環(huán)遍歷第二個(gè)參數(shù)中的屬性。為了確保確實(shí)是要保存的數(shù)據(jù),使

用了 hasOwnProperty()方法,來(lái)確保只有實(shí)例屬性被序列化到子 cookie 中。由于可能會(huì)存在屬性名

為空字符串的情況,所以在把屬性名加入結(jié)果對(duì)象之前還要檢查一下屬性名的長(zhǎng)度。將每個(gè)子 cookie

的名值對(duì)兒都存入 subcookieParts 數(shù)組中,以便稍后可以使用 join()方法以&號(hào)組合起來(lái)。剩下的

方法則和 CookieUtil.set()一樣。

可以按如下方式使用這些方法。

//假設(shè) document.cookie=data=name=Nicholas&book=Professional%20JavaScript

//設(shè)置兩個(gè) cookie

SubCookieUtil.set(\"data\", \"name\", \"Nicholas\");

SubCookieUtil.set(\"data\", \"book\", \"Professional JavaScript\");

//設(shè)置全部子 cookie 和失效日期

SubCookieUtil.setAll(\"data\", { name: \"Nicholas\", book: \"Professional JavaScript\" },

new Date(\"January 1, 2010\"));

//修改名字的值,并修改 cookie 的失效日期

SubCookieUtil.set(\"data\", \"name\", \"Michael\", new Date(\"February 1, 2010\"));

SubCookiesExample01.htm

子 cookie 的最后一組方法是用于刪除子 cookie 的。普通 cookie 可以通過(guò)將失效時(shí)間設(shè)置為過(guò)去的

時(shí)間的方法來(lái)刪除,但是子 cookie 不能這樣做。為了刪除一個(gè)子 cookie,首先必須獲取包含在某個(gè) cookie

中的所有子 cookie,然后僅刪除需要?jiǎng)h除的那個(gè)子 cookie,然后再將余下的子 cookie 的值保存為 cookie

的值。請(qǐng)看以下代碼。

var SubCookieUtil = {

//這里省略了更多代碼

unset: function (name, subName, path, domain, secure){

var subcookies = this.getAll(name);

if (subcookies){

delete subcookies[subName];

this.setAll(name, subcookies, null, path, domain, secure);

}

},

unsetAll: function(name, path, domain, secure){

this.setAll(name, null, new Date(0), path, domain, secure);

}

};

SubCookieUtil.js

這里定義的兩個(gè)方法用于兩種不同的目的。unset()方法用于刪除某個(gè) cookie 中的單個(gè)子 cookie

而不影響其他的;而 unsetAll()方法則等同于 CookieUtil.unset(),用于刪除整個(gè) cookie。和 set()

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第655頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 637

1

15

16

4

5

13

6

20

21

9

23

11

12

及 setAll()一樣,路徑、域和 secure 標(biāo)志必須和之前創(chuàng)建的 cookie 包含的內(nèi)容一致。這兩個(gè)方法可

以像下面這樣使用。

//僅刪除名為 name 的子 cookie

SubCookieUtil.unset(\"data\", \"name\");

//刪除整個(gè) cookie

SubCookieUtil.unsetAll(\"data\");

如果你擔(dān)心開發(fā)中可能會(huì)達(dá)到單域名的 cookie 上限,那么子 cookie 可是一個(gè)非常有吸引力的備選方

案。不過(guò),你需要更加密切關(guān)注 cookie 的長(zhǎng)度,以防超過(guò)單個(gè) cookie 的長(zhǎng)度限制。

5. 關(guān)于 cookie 的思考

還有一類 cookie 被稱為“HTTP 專有 cookie”。HTTP 專有 cookie 可以從瀏覽器或者服務(wù)器設(shè)置,但

是只能從服務(wù)器端讀取,因?yàn)?JavaScript 無(wú)法獲取 HTTP 專有 cookie 的值。

由于所有的 cookie 都會(huì)由瀏覽器作為請(qǐng)求頭發(fā)送,所以在 cookie 中存儲(chǔ)大量信息會(huì)影響到特定域的

請(qǐng)求性能。cookie 信息越大,完成對(duì)服務(wù)器請(qǐng)求的時(shí)間也就越長(zhǎng)。盡管瀏覽器對(duì) cookie 進(jìn)行了大小限制,

不過(guò)最好還是盡可能在 cookie 中少存儲(chǔ)信息,以避免影響性能。

cookie 的性質(zhì)和它的局限使得其并不能作為存儲(chǔ)大量信息的理想手段,所以又出現(xiàn)了其他方法。

一定不要在 cookie 中存儲(chǔ)重要和敏感的數(shù)據(jù)。cookie 數(shù)據(jù)并非存儲(chǔ)在一個(gè)安全環(huán)

境中,其中包含的任何數(shù)據(jù)都可以被他人訪問(wèn)。所以不要在 cookie 中存儲(chǔ)諸如信用卡

號(hào)或者個(gè)人地址之類的數(shù)據(jù)。

23.3.2 IE用戶數(shù)據(jù)

在 IE5.0 中,微軟通過(guò)一個(gè)自定義行為引入了持久化用戶數(shù)據(jù)的概念。用戶數(shù)據(jù)允許每個(gè)文檔最多

128KB 數(shù)據(jù),每個(gè)域名最多 1MB 數(shù)據(jù)。要使用持久化用戶數(shù)據(jù),首先必須如下所示,使用 CSS 在某個(gè)

元素上指定 userData 行為:

<div style=\"behavior:url(#default#userData)\" id=\"dataStore\"></div>

一旦該元素使用了 userData 行為,那么就可以使用 setAttribute()方法在上面保存數(shù)據(jù)了。

為了將數(shù)據(jù)提交到瀏覽器緩存中,還必須調(diào)用 save()方法并告訴它要保存到的數(shù)據(jù)空間的名字。數(shù)據(jù)

空間名字可以完全任意,僅用于區(qū)分不同的數(shù)據(jù)集。請(qǐng)看以下例子。

var dataStore = document.getElementById(\"dataStore\");

dataStore.setAttribute(\"name\", \"Nicholas\");

dataStore.setAttribute(\"book\", \"Professional JavaScript\");

dataStore.save(\"BookInfo\");

UserDataExample01.htm

在這段代碼中,<div>元素上存入了兩部分信息。在用 setAttribute()存儲(chǔ)了數(shù)據(jù)之后,調(diào)用了

save()方法,指定了數(shù)據(jù)空間的名稱為 BookInfo。下一次頁(yè)面載入之后,可以使用 load()方法指定

同樣的數(shù)據(jù)空間名稱來(lái)獲取數(shù)據(jù),如下所示。

dataStore.load(\"BookInfo\");

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第656頁(yè)

638 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

alert(dataStore.getAttribute(\"name\")); //\"Nicholas\"

alert(dataStore.getAttribute(\"book\")); //\"Professional JavaScript\"

UserDataExample01.htm

對(duì) load()的調(diào)用獲取了 BookInfo 數(shù)據(jù)空間中的所有信息,并且使數(shù)據(jù)可以通過(guò)元素訪問(wèn);只有

到載入確切完成之后數(shù)據(jù)方能使用。如果 getAttribute()調(diào)用了不存在的名稱或者是尚未載入的名

程,則返回 null。

你可以通過(guò) removeAttribute()方法明確指定要?jiǎng)h除某元素?cái)?shù)據(jù),只要指定屬性名稱。刪除之后,

必須像下面這樣再次調(diào)用 save()來(lái)提交更改。

dataStore.removeAttribute(\"name\");

dataStore.removeAttribute(\"book\");

dataStore.save(\"BookInfo\");

UserDataExample01.htm

這段代碼刪除了兩個(gè)數(shù)據(jù)屬性,然后將更改保存到緩存中。

對(duì) IE 用戶數(shù)據(jù)的訪問(wèn)限制和對(duì) cookie 的限制類似。要訪問(wèn)某個(gè)數(shù)據(jù)空間,腳本運(yùn)行的頁(yè)面必須來(lái)

自同一個(gè)域名,在同一個(gè)路徑下,并使用與進(jìn)行存儲(chǔ)的腳本同樣的協(xié)議。和 cookie 不同的是,你無(wú)法將

用戶數(shù)據(jù)訪問(wèn)限制擴(kuò)展到更多的客戶。還有一點(diǎn)不同,用戶數(shù)據(jù)默認(rèn)是可以跨越會(huì)話持久存在的,同時(shí)

也不會(huì)過(guò)期;數(shù)據(jù)需要通過(guò) removeAttribute()方法專門進(jìn)行刪除以釋放空間。

和 cookie 一樣,IE 用戶數(shù)據(jù)并非安全的,所以不能存放敏感信息。

23.3.3 Web存儲(chǔ)機(jī)制

Web Storage 最早是在 Web 超文本應(yīng)用技術(shù)工作組(WHAT-WG)的 Web 應(yīng)用 1.0 規(guī)范中描述的。

這個(gè)規(guī)范的最初的工作最終成為了 HTML5 的一部分。Web Storage 的目的是克服由 cookie 帶來(lái)的一些限

制,當(dāng)數(shù)據(jù)需要被嚴(yán)格控制在客戶端上時(shí),無(wú)須持續(xù)地將數(shù)據(jù)發(fā)回服務(wù)器。Web Storage 的兩個(gè)主要目

標(biāo)是:

? 提供一種在 cookie 之外存儲(chǔ)會(huì)話數(shù)據(jù)的途徑;

? 提供一種存儲(chǔ)大量可以跨會(huì)話存在的數(shù)據(jù)的機(jī)制。

最初的 Web Storage 規(guī)范包含了兩種對(duì)象的定義:sessionStorage 和 globalStorage。這兩個(gè)

對(duì)象在支持的瀏覽器中都是以 windows 對(duì)象屬性的形式存在的,支持這兩個(gè)屬性的瀏覽器包括 IE8+、

Firefox 3.5+、Chrome 4+和 Opera 10.5+。

Firefox 2 和 3 基于早期規(guī)范的內(nèi)容部分實(shí)現(xiàn)了 Web Storage,當(dāng)時(shí)只實(shí)現(xiàn)了

globalStorage,沒有實(shí)現(xiàn) localStorage。

1. Storage 類型

Storage 類型提供最大的存儲(chǔ)空間(因?yàn)g覽器而異)來(lái)存儲(chǔ)名值對(duì)兒。Storage 的實(shí)例與其他對(duì)

象類似,有如下方法。

? clear(): 刪除所有值;Firefox 中沒有實(shí)現(xiàn) 。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第657頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 639

1

15

16

4

5

13

6

20

21

9

23

11

12

? getItem(name):根據(jù)指定的名字 name 獲取對(duì)應(yīng)的值。

? key(index):獲得 index 位置處的值的名字。

? removeItem(name):刪除由 name 指定的名值對(duì)兒。

? setItem(name, value):為指定的 name 設(shè)置一個(gè)對(duì)應(yīng)的值。

其中,getItem()、removeItem()和 setItem()方法可以直接調(diào)用,也可通過(guò) Storage 對(duì)象間

接調(diào)用。因?yàn)槊總€(gè)項(xiàng)目都是作為屬性存儲(chǔ)在該對(duì)象上的,所以可以通過(guò)點(diǎn)語(yǔ)法或者方括號(hào)語(yǔ)法訪問(wèn)屬性

來(lái)讀取值,設(shè)置也一樣,或者通過(guò) delete 操作符進(jìn)行刪除。不過(guò),我們還建議讀者使用方法而不是屬

性來(lái)訪問(wèn)數(shù)據(jù),以免某個(gè)鍵會(huì)意外重寫該對(duì)象上已經(jīng)存在的成員。

還可以使用 length 屬性來(lái)判斷有多少名值對(duì)兒存放在 Storage 對(duì)象中。但無(wú)法判斷對(duì)象中所有

數(shù)據(jù)的大小,不過(guò) IE8 提供了一個(gè) remainingSpace 屬性,用于獲取還可以使用的存儲(chǔ)空間的字節(jié)數(shù)。

Storage 類型只能存儲(chǔ)字符串。非字符串的數(shù)據(jù)在存儲(chǔ)之前會(huì)被轉(zhuǎn)換成字符串。

2. sessionStorage 對(duì)象

sessionStorage 對(duì)象存儲(chǔ)特定于某個(gè)會(huì)話的數(shù)據(jù),也就是該數(shù)據(jù)只保持到瀏覽器關(guān)閉。這個(gè)對(duì)象

就像會(huì)話 cookie,也會(huì)在瀏覽器關(guān)閉后消失。存儲(chǔ)在 sessionStorage 中的數(shù)據(jù)可以跨越頁(yè)面刷新而

存在,同時(shí)如果瀏覽器支持,瀏覽器崩潰并重啟之后依然可用(Firefox 和 WebKit 都支持,IE 則不行)。

因?yàn)?seesionStorage 對(duì)象綁定于某個(gè)服務(wù)器會(huì)話,所以當(dāng)文件在本地運(yùn)行的時(shí)候是不可用的。存

儲(chǔ)在 sessionStorage 中的數(shù)據(jù)只能由最初給對(duì)象存儲(chǔ)數(shù)據(jù)的頁(yè)面訪問(wèn)到,所以對(duì)多頁(yè)面應(yīng)用有限制。

由于 sessionStorage 對(duì)象其實(shí)是 Storage 的一個(gè)實(shí)例,所以可以使用 setItem()或者直接設(shè)

置新的屬性來(lái)存儲(chǔ)數(shù)據(jù)。下面是這兩種方法的例子。

//使用方法存儲(chǔ)數(shù)據(jù)

sessionStorage.setItem(\"name\", \"Nicholas\");

//使用屬性存儲(chǔ)數(shù)據(jù)

sessionStorage.book = \"Professional JavaScript\";

SessionStorageExample01.htm

不同瀏覽器寫入數(shù)據(jù)方面略有不同。Firefox 和 WebKit 實(shí)現(xiàn)了同步寫入,所以添加到存儲(chǔ)空間中的

數(shù)據(jù)是立刻被提交的。而 IE 的實(shí)現(xiàn)則是異步寫入數(shù)據(jù),所以在設(shè)置數(shù)據(jù)和將數(shù)據(jù)實(shí)際寫入磁盤之間可

能有一些延遲。對(duì)于少量數(shù)據(jù)而言,這個(gè)差異是可以忽略的。對(duì)于大量數(shù)據(jù),你會(huì)發(fā)現(xiàn) IE 要比其他瀏

覽器更快地恢復(fù)執(zhí)行,因?yàn)樗鼤?huì)跳過(guò)實(shí)際的磁盤寫入過(guò)程。

在 IE8 中可以強(qiáng)制把數(shù)據(jù)寫入磁盤:在設(shè)置新數(shù)據(jù)之前使用 begin()方法,并且在所有設(shè)置完成之

后調(diào)用 commit()方法??匆韵吕?。

//只適用于 IE8

sessionStorage.begin();

sessionStorage.name = \"Nicholas\";

sessionStorage.book = \"Professional JavaScript\";

sessionStorage.commit();

這段代碼確保了 name 和 book 的值在調(diào)用 commit()之后立刻被寫入磁盤。調(diào)用 begin()是為了

確保在這段代碼執(zhí)行的時(shí)候不會(huì)發(fā)生其他磁盤寫入操作。對(duì)于少量數(shù)據(jù)而言,這個(gè)過(guò)程不是必需的;不

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第658頁(yè)

640 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

過(guò),對(duì)于大量數(shù)據(jù)(如文檔之類的)可能就要考慮這種事務(wù)形式的方法了。

sessionStorage 中有數(shù)據(jù)時(shí),可以使用 getItem()或者通過(guò)直接訪問(wèn)屬性名來(lái)獲取數(shù)據(jù)。兩種

方法的例子如下。

//使用方法讀取數(shù)據(jù)

var name = sessionStorage.getItem(\"name\");

//使用屬性讀取數(shù)據(jù)

var book = sessionStorage.book;

SessionStorageExample01.htm

還可以通過(guò)結(jié)合 length 屬性和 key()方法來(lái)迭代 sessionStorage 中的值,如下所示。

for (var i=0, len = sessionStorage.length; i < len; i++){

var key = sessionStorage.key(i);

var value = sessionStorage.getItem(key);

alert(key + \"=\" + value);

}

SessionStorageExample01.htm

它是這樣遍歷 sessionStorage 中的名值對(duì)兒的:首先通過(guò) key()方法獲取指定位置上的名字,

然后再通過(guò) getItem()找出對(duì)應(yīng)該名字的值。

還可以使用 for-in 循環(huán)來(lái)迭代 sessionStorage 中的值:

for (var key in sessionStorage){

var value = sessionStorage.getItem(key);

alert(key + \"=\" + value);

}

每次經(jīng)過(guò)循環(huán)的時(shí)候,key 被設(shè)置為 sessionStorage 中下一個(gè)名字,此時(shí)不會(huì)返回任何內(nèi)置方

法或 length 屬性。

要從 sessionStorage 中刪除數(shù)據(jù),可以使用 delete 操作符刪除對(duì)象屬性,也可調(diào)用

removeItem()方法。以下是這些方法的例子。

//使用 delete 刪除一個(gè)值——在 WebKit 中無(wú)效

delete sessionStorage.name;

//使用方法刪除一個(gè)值

sessionStorage.removeItem(\"book\");

SessionStorageExample01.htm

在撰寫本書時(shí),delete 操作符在 WebKit 中無(wú)法刪除數(shù)據(jù),removeItem()則可以在各種支持的瀏

覽器中正確運(yùn)行。

sessionStorage 對(duì)象應(yīng)該主要用于僅針對(duì)會(huì)話的小段數(shù)據(jù)的存儲(chǔ)。如果需要跨越會(huì)話存儲(chǔ)數(shù)據(jù),

那么 globalStorage 或者 localStorage 更為合適。

3. globalStorage 對(duì)象

Firefox 2 中實(shí)現(xiàn)了 globalStorage 對(duì)象。作為最初的 Web Storage 規(guī)范的一部分,這個(gè)對(duì)象的目

的是跨越會(huì)話存儲(chǔ)數(shù)據(jù),但有特定的訪問(wèn)限制。要使用 globalStorage,首先要指定哪些域可以訪問(wèn)

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第659頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 641

1

15

16

4

5

13

6

20

21

9

23

11

12

該數(shù)據(jù)??梢酝ㄟ^(guò)方括號(hào)標(biāo)記使用屬性來(lái)實(shí)現(xiàn),如以下例子所示。

//保存數(shù)據(jù)

globalStorage[\"wrox.com\"].name = \"Nicholas\";

//獲取數(shù)據(jù)

var name = globalStorage[\"wrox.com\"].name;

GlobalStorageExample01.htm

在這里,訪問(wèn)的是針對(duì)域名 wrox.com 的存儲(chǔ)空間。globalStorage 對(duì)象不是 Storage 的實(shí)例,

而具體的 globalStorage[\"wrox.com\"]才是。這個(gè)存儲(chǔ)空間對(duì)于 wrox.com 及其所有子域都是可以

訪問(wèn)的??梢韵裣旅孢@樣指定子域名。

//保存數(shù)據(jù)

globalStorage[\"www.wrox.com\"].name = \"Nicholas\";

//獲取數(shù)據(jù)

var name = globalStorage[\"www.wrox.com\"].name;

GlobalStorageExample01.htm

這里所指定的存儲(chǔ)空間只能由來(lái)自 www.wrox.com 的頁(yè)面訪問(wèn),其他子域名都不行。

某些瀏覽器允許更加寬泛的訪問(wèn)限制,比如只根據(jù)頂級(jí)域名進(jìn)行限制或者允許全局訪問(wèn),如下面例

子所示。

//存儲(chǔ)數(shù)據(jù),任何人都可以訪問(wèn)——不要這樣做!

globalStorage[\"\"].name = \"Nicholas\";

//存儲(chǔ)數(shù)據(jù),可以讓任何以.net 結(jié)尾的域名訪問(wèn)——不要這樣做!

globalStorage[\"net\"].name = \"Nicholas\";

雖然這些也支持,但是還是要避免使用這種可寬泛訪問(wèn)的數(shù)據(jù)存儲(chǔ),以防止出現(xiàn)潛在的安全問(wèn)題。

考慮到安全問(wèn)題,這些功能在未來(lái)可能會(huì)被刪除或者是被更嚴(yán)格地限制,所以不應(yīng)依賴于這類功能。當(dāng)

使用 globalStorage 的時(shí)候一定要指定一個(gè)域名。

對(duì) globalStorage 空間的訪問(wèn),是依據(jù)發(fā)起請(qǐng)求的頁(yè)面的域名、協(xié)議和端口來(lái)限制的。例如,如

果使用 HTTPS 協(xié)議在 wrox.com 中存儲(chǔ)了數(shù)據(jù),那么通過(guò) HTTP 訪問(wèn)的 wrox.com 的頁(yè)面就不能訪問(wèn)

該數(shù)據(jù)。同樣,通過(guò) 80 端口訪問(wèn)的頁(yè)面則無(wú)法與同一個(gè)域同樣協(xié)議但通過(guò) 8080 端口訪問(wèn)的頁(yè)面共享數(shù)

據(jù)。這類似于 Ajax 請(qǐng)求的同源策略。

globalStorage 的每個(gè)屬性都是 Storage 的實(shí)例。因此,可以像如下代碼中這樣使用。

globalStorage[\"www.wrox.com\"].name = \"Nicholas\";

globalStorage[\"www.wrox.com\"].book = \"Professional JavaScript\";

globalStorage[\"www.wrox.com\"].removeItem(\"name\");

var book = globalStorage[\"www.wrox.com\"].getItem(\"book\");

GlobalStorageExample01.htm

如果你事先不能確定域名,那么使用 location.host 作為屬性名比較安全。例如:

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第660頁(yè)

642 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

globalStorage[location.host].name = \"Nicholas\";

var book = globalStorage[location.host].getItem(\"book\");

GlobalStorageExample01.htm

如果不使用 removeItem() 或 者 delete 刪除,或者用戶未清除瀏覽器緩存,存儲(chǔ)在

globalStorage 屬性中的數(shù)據(jù)會(huì)一直保留在磁盤上。這讓 globalStorage 非常適合在客戶端存儲(chǔ)文

檔或者長(zhǎng)期保存用戶偏好設(shè)置。

4. localStorage 對(duì)象

localStorage 對(duì)象在修訂過(guò)的 HTML 5 規(guī)范中作為持久保存客戶端數(shù)據(jù)的方案取代了

globalStorage。與 globalStorage 不同,不能給 localStorage 指定任何訪問(wèn)規(guī)則;規(guī)則事先就

設(shè)定好了。要訪問(wèn)同一個(gè) localStorage 對(duì)象,頁(yè)面必須來(lái)自同一個(gè)域名(子域名無(wú)效),使用同一種

協(xié)議,在同一個(gè)端口上。這相當(dāng)于 globalStorage[location.host]。

由于 localStorage 是 Storage 的實(shí)例,所以可以像使用 sessionStorage 一樣來(lái)使用它。下

面是一些例子。

//使用方法存儲(chǔ)數(shù)據(jù)

localStorage.setItem(\"name\", \"Nicholas\");

//使用屬性存儲(chǔ)數(shù)據(jù)

localStorage.book = \"Professional JavaScript\";

//使用方法讀取數(shù)據(jù)

var name = localStorage.getItem(\"name\");

//使用屬性讀取數(shù)據(jù)

var book = localStorage.book;

LocalStorageExample01.htm

存儲(chǔ)在 localStorage 中的數(shù)據(jù)和存儲(chǔ)在 globalStorage 中的數(shù)據(jù)一樣,都遵循相同的規(guī)則:

數(shù)據(jù)保留到通過(guò) JavaScript 刪除或者是用戶清除瀏覽器緩存。

為了兼容只支持 globalStorage 的瀏覽器,可以使用以下函數(shù)。

function getLocalStorage(){

if (typeof localStorage == \"object\"){

return localStorage;

} else if (typeof globalStorage == \"object\"){

return globalStorage[location.host];

} else {

throw new Error(\"Local storage not available.\");

}

}

GlobalAndLocalStorageExample01.htm

然后,像下面這樣調(diào)用一次這個(gè)函數(shù),就可以正常地讀寫數(shù)據(jù)了。

var storage = getLocalStorage();

GlobalAndLocalStorageExample01.htm

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第661頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 643

1

15

16

4

5

13

6

20

21

9

23

11

12

在確定了使用哪個(gè) Storage 對(duì)象之后,就能在所有支持 Web Storage 的瀏覽器中使用相同的存取規(guī)

則操作數(shù)據(jù)了。

5. storage 事件

對(duì) Storage 對(duì)象進(jìn)行任何修改,都會(huì)在文檔上觸發(fā) storage 事件。當(dāng)通過(guò)屬性或 setItem()方

法保存數(shù)據(jù),使用 delete 操作符或 removeItem()刪除數(shù)據(jù),或者調(diào)用 clear()方法時(shí),都會(huì)發(fā)生該

事件。這個(gè)事件的 event 對(duì)象有以下屬性。

? domain:發(fā)生變化的存儲(chǔ)空間的域名。

? key:設(shè)置或者刪除的鍵名。

? newValue:如果是設(shè)置值,則是新值;如果是刪除鍵,則是 null。

? oldValue:鍵被更改之前的值。

在這四個(gè)屬性中,IE8 和 Firefox 只實(shí)現(xiàn)了 domain 屬性。在撰寫本書的時(shí)候,WebKit 尚不支持

storage 事件:

以下代碼展示了如何偵聽 storage 事件:

EventUtil.addHandler(document, \"storage\", function(event){

alert(\"Storage changed for \" + event.domain);

});

StorageEventExample01.htm

無(wú)論對(duì) sessionStorage、globalStorage 還是 localStorage 進(jìn)行操作,都會(huì)觸發(fā) storage

事件,但不作區(qū)分。

6. 限制

與其他客戶端數(shù)據(jù)存儲(chǔ)方案類似,Web Storage 同樣也有限制。這些限制因?yàn)g覽器而異。一般來(lái)說(shuō),

對(duì)存儲(chǔ)空間大小的限制都是以每個(gè)來(lái)源(協(xié)議、域和端口)為單位的。換句話說(shuō),每個(gè)來(lái)源都有固定大

小的空間用于保存自己的數(shù)據(jù)??紤]到這個(gè)限制,就要注意分析和控制每個(gè)來(lái)源中有多少頁(yè)面需要保存

數(shù)據(jù)。

對(duì)于 localStorage 而言,大多數(shù)桌面瀏覽器會(huì)設(shè)置每個(gè)來(lái)源 5MB 的限制。Chrome 和 Safari 對(duì)每

個(gè)來(lái)源的限制是 2.5MB。而 iOS 版 Safari 和 Android 版 WebKit 的限制也是 2.5MB。

對(duì) sessionStorage 的限制也是因?yàn)g覽器而異。有的瀏覽器對(duì) sessionStorage 的大小沒有限制,

但 Chrome、Safari、iOS 版 Safari 和 Android 版 WebKit 都有限制,也都是 2.5MB。IE8+和 Opera 對(duì)

sessionStorage 的限制是 5MB。

有關(guān) Web Storage 的限制,請(qǐng)參考 http://dev-test.nemikor.com/web-storage/support-test/。

23.3.4 IndexedDB

Indexed Database API,或者簡(jiǎn)稱為 IndexedDB,是在瀏覽器中保存結(jié)構(gòu)化數(shù)據(jù)的一種數(shù)據(jù)庫(kù)。

IndexedDB 是為了替代目前已被廢棄的 Web SQL Database API(因?yàn)橐褟U棄,所以本書未介紹)而出現(xiàn)

的。IndexedDB 的思想是創(chuàng)建一套 API,方便保存和讀取 JavaScript 對(duì)象,同時(shí)還支持查詢及搜索。

IndexedDB 設(shè)計(jì)的操作完全是異步進(jìn)行的。因此,大多數(shù)操作會(huì)以請(qǐng)求方式進(jìn)行,但這些操作會(huì)在

后期執(zhí)行,然后如果成功則返回結(jié)果,如果失敗則返回錯(cuò)誤。差不多每一次 IndexedDB 操作,都需要你

注冊(cè) onerror 或 onsuccess 事件處理程序,以確保適當(dāng)?shù)靥幚斫Y(jié)果。

在得到完整支持的情況下,IndexedDB 將是一個(gè)作為 API 宿主的全局對(duì)象。由于 API 仍然可能有

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第662頁(yè)

644 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

變化,瀏覽器也都使用提供商前綴,因此這個(gè)對(duì)象在 IE10 中叫 msIndexedDB,在 Firefox 4 中叫

mozIndexedDB,在 Chrome 中叫 webkitIndexedDB。為了清楚起見,本節(jié)示例中將使用 IndexedDB,

而實(shí)際上每個(gè)示例前面都應(yīng)該加上下面這行代碼:

var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB ||

window.webkitIndexedDB;

IndexedDBExample01.htm

1. 數(shù)據(jù)庫(kù)

IndexedDB 就是一個(gè)數(shù)據(jù)庫(kù),與 MySQL 或 Web SQL Database 等這些你以前可能用過(guò)的數(shù)據(jù)庫(kù)類似。

IndexedDB 最大的特色是使用對(duì)象保存數(shù)據(jù),而不是使用表來(lái)保存數(shù)據(jù)。一個(gè) IndexedDB 數(shù)據(jù)庫(kù),就是

一組位于相同命名空間下的對(duì)象的集合。

使用 IndexedDB 的第一步是打開它,即把要打開的數(shù)據(jù)庫(kù)名傳給 indexDB.open()。如果傳入的

數(shù)據(jù)庫(kù)已經(jīng)存在,就會(huì)發(fā)送一個(gè)打開它的請(qǐng)求;如果傳入的數(shù)據(jù)庫(kù)還不存在,就會(huì)發(fā)送一個(gè)創(chuàng)建并打開

它的請(qǐng)求。總之,調(diào)用indexDB.open()會(huì)返回一個(gè)IDBRequest 對(duì)象,在這個(gè)對(duì)象上可以添加onerror

和 onsuccess 事件處理程序。先來(lái)看一個(gè)例子。

var request, database;

request = indexedDB.open(\"admin\");

request.onerror = function(event){

alert(\"Something bad happened while trying to open: \" +

event.target.errorCode);

};

request.onsuccess = function(event){

database = event.target.result;

};

IndexedDBExample01.htm

在這兩個(gè)事件處理程序中,event.target 都指向 request 對(duì)象,因此它們可以互換使用。如果響

應(yīng)的是 onsuccess 事件處理程序,那么 event.target.result 中將有一個(gè)數(shù)據(jù)庫(kù)實(shí)例對(duì)象(IDBDatabase),這個(gè)對(duì)象會(huì)保存在 database 變量中。如果發(fā)生了錯(cuò)誤,那 event.target.errorCode 中將

保存一個(gè)錯(cuò)誤碼,表示問(wèn)題的性質(zhì)。以下就是可能的錯(cuò)誤碼(這個(gè)錯(cuò)誤碼適合所有操作)。

? IDBDatabaseException.UNKNOWN_ERR(1):意外錯(cuò)誤,無(wú)法歸類。

? IDBDatabaseException.NON_TRANSIENT_ERR(2):操作不合法。

? IDBDatabaseException.NOT_FOUND_ERR(3):未發(fā)現(xiàn)要操作的數(shù)據(jù)庫(kù)。

? IDBDatabaseException.CONSTRAINT_ERR(4):違反了數(shù)據(jù)庫(kù)約束。

? IDBDatabaseException.DATA_ERR(5):提供給事務(wù)的數(shù)據(jù)不能滿足要求。

? IDBDatabaseException.NOT_ALLOWED_ERR(6):操作不合法。

? IDBDatabaseException.TRANSACTION_INACTIVE_ERR(7):試圖重用已完成的事務(wù)。

? IDBDatabaseException.ABORT_ERR(8):請(qǐng)求中斷,未成功。

? IDBDatabaseException.READ_ONLY_ERR(9):試圖在只讀模式下寫入或修改數(shù)據(jù)。

? IDBDatabaseException.TIMEOUT_ERR(10):在有效時(shí)間內(nèi)未完成操作。

? IDBDatabaseException.QUOTA_ERR(11):磁盤空間不足。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第663頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 645

1

15

16

4

5

13

6

20

21

9

23

11

12

默認(rèn)情況下,IndexedDB 數(shù)據(jù)庫(kù)是沒有版本號(hào)的,最好一開始就為數(shù)據(jù)庫(kù)指定一個(gè)版本號(hào)。為此,

可以調(diào)用 setVersion()方法,傳入以字符串形式表示的版本號(hào)。同樣,調(diào)用這個(gè)方法也會(huì)返回一個(gè)請(qǐng)

求對(duì)象,需要你再指定事件處理程序。

if (database.version != \"1.0\"){

request = database.setVersion(\"1.0\");

request.onerror = function(event){

alert(\"Something bad happened while trying to set version: \" +

event.target.errorCode);

};

request.onsuccess = function(event){

alert(\"Database initialization complete. Database name: \" + database.name +

\", Version: \" + database.version);

};

} else {

alert(\"Database already initialized. Database name: \" + database.name +

\", Version: \" + database.version);

}

IndexedDBExample01.htm

這個(gè)例子嘗試把數(shù)據(jù)庫(kù)的版本號(hào)設(shè)置為 1.0。第一行先檢測(cè) version 屬性,看是否已經(jīng)為數(shù)據(jù)庫(kù)設(shè)

置了相應(yīng)的版本號(hào)。如果沒有,就調(diào)用 setVersion()創(chuàng)建修改版本的請(qǐng)求。如果請(qǐng)求成功,顯示一條

消息,表示版本修改成功。(在真實(shí)的項(xiàng)目開發(fā)中,你應(yīng)該在這里建立對(duì)象存儲(chǔ)空間。詳細(xì)內(nèi)容請(qǐng)看下

一節(jié)。)

如果數(shù)據(jù)庫(kù)的版本號(hào)已經(jīng)被設(shè)置為 1.0,則顯示一條消息,說(shuō)明數(shù)據(jù)庫(kù)已經(jīng)初始化過(guò)了??傊?,通

過(guò)這種模式,就能知道你想使用的數(shù)據(jù)庫(kù)是否已經(jīng)設(shè)置了適當(dāng)?shù)膶?duì)象存儲(chǔ)空間。在整個(gè) Web 應(yīng)用中,

隨著對(duì)數(shù)據(jù)庫(kù)結(jié)構(gòu)的更新和修改,可能會(huì)產(chǎn)生很多個(gè)不同版本的數(shù)據(jù)庫(kù)。

2. 對(duì)象存儲(chǔ)空間

在建立了與數(shù)據(jù)庫(kù)的連接之后,下一步就是使用對(duì)象存儲(chǔ)空間①。如果數(shù)據(jù)庫(kù)的版本與你傳入的版

本不匹配,那可能就需要?jiǎng)?chuàng)建一個(gè)新的對(duì)象存儲(chǔ)空間。在創(chuàng)建對(duì)象存儲(chǔ)空間之前,必須要想清楚你想要

保存什么數(shù)據(jù)類型。

假設(shè)你要保存的用戶記錄由用戶名、密碼等組成,那么保存一條記錄的對(duì)象應(yīng)該類似如下所示:

var user = {

username: \"007\",

firstName: \"James\",

lastName: \"Bond\",

password: \"foo\"

};

有了這個(gè)對(duì)象,很容易想到 username 屬性可以作為這個(gè)對(duì)象存儲(chǔ)空間的鍵。這個(gè) username 必須

全局唯一,而且大多數(shù)時(shí)候都要通過(guò)這個(gè)鍵來(lái)訪問(wèn)數(shù)據(jù)。這一點(diǎn)非常重要,因?yàn)樵趧?chuàng)建對(duì)象存儲(chǔ)空間時(shí),

必須指定這么一個(gè)鍵。以下是就是為保存上述用戶記錄而創(chuàng)建對(duì)象存儲(chǔ)空間的示例。

var store = db.createObjectStore(\"users\", { keyPath: \"username\" });

IndexedDBExample02.htm

——————————

① 有關(guān)系數(shù)據(jù)庫(kù)經(jīng)驗(yàn)的讀者,可以把這里的對(duì)象存儲(chǔ)空間(object storge)想象成表,而把其中保存的對(duì)象想象成表中

的記錄。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第664頁(yè)

646 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

其中第二個(gè)參數(shù)中的 keyPath 屬性,就是空間中將要保存的對(duì)象的一個(gè)屬性,而這個(gè)屬性將作為

存儲(chǔ)空間的鍵來(lái)使用。

好,現(xiàn)在有了一個(gè)對(duì)存儲(chǔ)空間的引用。接下來(lái)可以使用 add()或 put()方法來(lái)向其中添加數(shù)據(jù)。這

兩個(gè)方法都接收一個(gè)參數(shù),即要保存的對(duì)象,然后這個(gè)對(duì)象就會(huì)被保存到存儲(chǔ)空間中。這兩個(gè)方法的區(qū)

別在空間中已經(jīng)包含鍵值相同的對(duì)象時(shí)會(huì)體現(xiàn)出來(lái)。在這種情況下,add()會(huì)返回錯(cuò)誤,而 put()則會(huì)

重寫原有對(duì)象。簡(jiǎn)單地說(shuō),可以把 add()想象成插入新值,把 put()想象成更新原有的值。在初始化對(duì)

象存儲(chǔ)空間時(shí),可以使用類似下面這樣的代碼。

//users 中保存著一批用戶對(duì)象

var i=0,

len = users.length;

while(i < len){

store.add(users[i++]);

}

IndexedDBExample02.htm

每次調(diào)用 add()或 put()都會(huì)創(chuàng)建一個(gè)新的針對(duì)這個(gè)對(duì)象存儲(chǔ)空間的更新請(qǐng)求。如果想驗(yàn)證請(qǐng)求是

否成功完成,可以把返回的請(qǐng)求對(duì)象保存在一個(gè)變量中,然后再指定 onerror 或 onsuccess 事件處理

程序。

//users 中保存著一批用戶對(duì)象

var i=0,

request,

requests = [],

len = users.length;

while(i < len){

request = store.add(users[i++]);

request.onerror = function(){

//處理錯(cuò)誤

};

request.onsuccess = function(){

//處理成功

};

requests.push(request);

}

創(chuàng)建了對(duì)象存儲(chǔ)空間并向其中添加了數(shù)據(jù)之后,就該查詢數(shù)據(jù)了。

3. 事務(wù)

跨過(guò)創(chuàng)建對(duì)象存儲(chǔ)空間這一步之后,接下來(lái)的所有操作都是通過(guò)事務(wù)來(lái)完成的。在數(shù)據(jù)庫(kù)對(duì)象上調(diào)

用 transaction()方法可以創(chuàng)建事務(wù)。任何時(shí)候,只要想讀取或修改數(shù)據(jù),都要通過(guò)事務(wù)來(lái)組織所有

操作。在最簡(jiǎn)單的情況下,可以像下面這樣創(chuàng)建事務(wù)①。

var transaction = db.transaction();

如果沒有參數(shù),就只能通過(guò)事務(wù)來(lái)讀取數(shù)據(jù)庫(kù)中保存的對(duì)象。最常見的方式是傳入要訪問(wèn)的一或多

個(gè)對(duì)象存儲(chǔ)空間。

——————————

① 以下示例代碼中的 db 即前面示例代碼中的 database,正文中提到的“數(shù)據(jù)庫(kù)對(duì)象”也是指它。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第665頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 647

1

15

16

4

5

13

6

20

21

9

23

11

12

var transaction = db.transaction(\"users\");

這樣就能保證只加載 users 存儲(chǔ)空間中的數(shù)據(jù),以便通過(guò)事務(wù)進(jìn)行訪問(wèn)。如果要訪問(wèn)多個(gè)對(duì)象存

儲(chǔ)空間,也可以在第一個(gè)參數(shù)的位置上傳入字符串?dāng)?shù)組。

var transaction = db.transaction([\"users\", \"anotherStore\"]);

如前所述,這些事務(wù)都是以只讀方式訪問(wèn)數(shù)據(jù)。要修改訪問(wèn)方式,必須在創(chuàng)建事務(wù)時(shí)傳入第二個(gè)參

數(shù),這個(gè)參數(shù)表示訪問(wèn)模式,用 IDBTransaction 接口定義的如下常量表示:READ_ONLY(0)表示只

讀,READ_WRITE(1)表示讀寫,VERSION_CHANGE(2)表示改變。IE10+和 Firefox 4+實(shí)現(xiàn)的是

IDBTransaction,但在 Chrome 中則叫 webkitIDBTransaction,所以使用下面的代碼可以統(tǒng)一接口:

var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;

IndexedDBExample03.htm

有了這行代碼,就可以更方便地為 transaction()指定第二個(gè)參數(shù)了。

var transaction = db.transaction(\"users\", IDBTransaction.READ_WRITE);

IndexedDBExample03.htm

這個(gè)事務(wù)能夠讀寫 users 存儲(chǔ)空間。

取得了事務(wù)的索引后,使用 objectStore()方法并傳入存儲(chǔ)空間的名稱,就可以訪問(wèn)特定的存儲(chǔ)

空間。然后,可以像以前一樣使用 add()和 put()方法,使用 get()可以取得值,使用 delete()可以

刪除對(duì)象,而使用 clear()則可以刪除所有對(duì)象。get()和 delete()方法都接收一個(gè)對(duì)象鍵作為參數(shù),

而所有這 5 個(gè)方法都會(huì)返回一個(gè)新的請(qǐng)求對(duì)象。例如:

var request = db.transaction(\"users\").objectStore(\"users\").get(\"007\");

request.onerror = function(event){

alert(\"Did not get the object!\");

};

request.onsuccess = function(event){

var result = event.target.result;

alert(result.firstName); //\"James\"

};

IndexedDBExample02.htm

因?yàn)橐粋€(gè)事務(wù)可以完成任何多個(gè)請(qǐng)求,所以事務(wù)對(duì)象本身也有事件處理程序:onerror 和

oncomplete。這兩個(gè)事件可以提供事務(wù)級(jí)的狀態(tài)信息。

transaction.onerror = function(event){

//整個(gè)事務(wù)都被取消了

};

transaction.oncomplete = function(event){

//整個(gè)事務(wù)都成功完成了

};

注意,通過(guò) oncomplete 事件的事件對(duì)象(event)訪問(wèn)不到 get()請(qǐng)求返回的任何數(shù)據(jù)。必須在

相應(yīng)請(qǐng)求的 onsuccess 事件處理程序中才能訪問(wèn)到數(shù)據(jù)。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第666頁(yè)

648 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

4. 使用游標(biāo)查詢

使用事務(wù)可以直接通過(guò)已知的鍵檢索單個(gè)對(duì)象。而在需要檢索多個(gè)對(duì)象的情況下,則需要在事務(wù)內(nèi)

部創(chuàng)建游標(biāo)。游標(biāo)就是一指向結(jié)果集的指針。與傳統(tǒng)數(shù)據(jù)庫(kù)查詢不同,游標(biāo)并不提前收集結(jié)果。游標(biāo)指

針會(huì)先指向結(jié)果中的第一項(xiàng),在接到查找下一項(xiàng)的指令時(shí),才會(huì)指向下一項(xiàng)。

在對(duì)象存儲(chǔ)空間上調(diào)用 openCursor()方法可以創(chuàng)建游標(biāo)。與 IndexedDB 中的其他操作一樣,

openCursor()方法返回的是一個(gè)請(qǐng)求對(duì)象,因此必須為該對(duì)象指定 onsuccess 和 onerror 事件處理

程序。例如:

var store = db.transaction(\"users\").objectStore(\"users\"),

request = store.openCursor();

request.onsuccess = function(event){

//處理成功

};

request.onerror = function(event){

//處理失敗

};

IndexedDBExample04.htm

在 onsuccess 事件處理程序執(zhí)行時(shí),可以通過(guò) event.target.result 取得存儲(chǔ)空間中的下一個(gè)

對(duì)象。在結(jié)果集中有下一項(xiàng)時(shí),這個(gè)屬性中保存一個(gè) IDBCursor 的實(shí)例,在沒有下一項(xiàng)時(shí),這個(gè)屬性

的值為 null。IDBCursor 的實(shí)例有以下幾個(gè)屬性。

? direction:數(shù)值,表示游標(biāo)移動(dòng)的方向。默認(rèn)值為 IDBCursor.NEXT(0),表示下一項(xiàng)。

IDBCursor.NEXT_NO_DUPLICATE(1)表示下一個(gè)不重復(fù)的項(xiàng),DBCursor.PREV(2)表示前

一項(xiàng),而 IDBCursor.PREV_NO_DUPLICATE 表示前一個(gè)不重復(fù)的項(xiàng)。

? key:對(duì)象的鍵。

? value:實(shí)際的對(duì)象。

? primaryKey:游標(biāo)使用的鍵。可能是對(duì)象鍵,也可能是索引鍵(稍后討論索引鍵)。

要檢索某一個(gè)結(jié)果的信息,可以像下面這樣:

request.onsuccess = function(event){

var cursor = event.target.result;

if (cursor){ //必須要檢查

console.log(\"Key: \" + cursor.key + \", Value: \" +

JSON.stringify(cursor.value));

}

};

請(qǐng)記住,這個(gè)例子中的 cursor.value 是一個(gè)對(duì)象,這也是為什么在顯示它之前先將它轉(zhuǎn)換成 JSON

字符串的原因。

使用游標(biāo)可以更新個(gè)別的記錄。調(diào)用 update()方法可以用指定的對(duì)象更新當(dāng)前游標(biāo)的 value。與

其他操作一樣,調(diào)用 update()方法也會(huì)創(chuàng)建一個(gè)新請(qǐng)求,因此如果你想知道結(jié)果,就要為它指定

onsuccess 和 onerror 事件處理程序。

request.onsuccess = function(event){

var cursor = event.target.result,

value,

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第667頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 649

1

15

16

4

5

13

6

20

21

9

23

11

12

updateRequest;

if (cursor){ //必須要檢查

if (cursor.key == \"foo\"){

value = cursor.value; //取得當(dāng)前的值

value.password = \"magic!\"; //更新密碼

updateRequest = cursor.update(value); //請(qǐng)求保存更新

updateRequest.onsuccess = function(){

//處理成功

};

updateReqeust.onerror = function(){

//處理失敗

};

}

}

};

此時(shí),如果調(diào)用 delete()方法,就會(huì)刪除相應(yīng)的記錄。與 update()一樣,調(diào)用 delete()也返

回一個(gè)請(qǐng)求。

request.onsuccess = function(event){

var cursor = event.target.result,

value,

deleteRequest;

if (cursor){ //必須要檢查

if (cursor.key == \"foo\"){

deleteRequest = cursor.delete(); //請(qǐng)求刪除當(dāng)前項(xiàng)

deleteRequest.onsuccess = function(){

//處理成功

};

deleteRequest.onerror = function(){

//處理失敗

};

}

}

};

如果當(dāng)前事務(wù)沒有修改對(duì)象存儲(chǔ)空間的權(quán)限,update()和 delete()會(huì)拋出錯(cuò)誤。

默認(rèn)情況下,每個(gè)游標(biāo)只發(fā)起一次請(qǐng)求。要想發(fā)起另一次請(qǐng)求,必須調(diào)用下面的一個(gè)方法。

? continue(key):移動(dòng)到結(jié)果集中的下一項(xiàng)。參數(shù) key 是可選的,不指定這個(gè)參數(shù),游標(biāo)移動(dòng)

到下一項(xiàng);指定這個(gè)參數(shù),游標(biāo)會(huì)移動(dòng)到指定鍵的位置。

? advance(count):向前移動(dòng) count 指定的項(xiàng)數(shù)。

這兩個(gè)方法都會(huì)導(dǎo)致游標(biāo)使用相同的請(qǐng)求,因此相同的 onsuccess 和 onerror 事件處理程序也會(huì)

得到重用。例如,下面的例子遍歷了對(duì)象存儲(chǔ)空間中的所有項(xiàng)。

request.onsuccess = function(event){

var cursor = event.target.result;

if (cursor){ //必須要檢查

console.log(\"Key: \" + cursor.key + \", Value: \" +

JSON.stringify(cursor.value));

cursor.continue(); //移動(dòng)到下一項(xiàng)

} else {

console.log(\"Done!\");

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第668頁(yè)

650 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

}

};

調(diào)用 continue()會(huì)觸發(fā)另一次請(qǐng)求,進(jìn)而再次調(diào)用 onsuccess 事件處理程序。在沒有更多項(xiàng)可

以迭代時(shí),將最后一次調(diào)用 onsuccess 事件處理程序,此時(shí) event.target.result 的值為 null。

5. 鍵范圍

使用游標(biāo)總讓人覺得不那么理想,因?yàn)橥ㄟ^(guò)游標(biāo)查找數(shù)據(jù)的方式太有限了。鍵范圍(key range)為

使用游標(biāo)增添了一些靈活性。鍵范圍由 IDBKeyRange 的實(shí)例表示。支持標(biāo)準(zhǔn) IDBKeyRange 類型的瀏

覽器有 IE10+和 Firefox 4+,Chrome 中的名字叫 webkitIDBKeyRange。與使用 IndexedDB 中的其他類

型一樣,你最好先聲明一個(gè)本地的類型,同時(shí)要考慮到不同瀏覽器中的差異。

var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

有四種定義鍵范圍的方式。第一種是使用 only()方法,傳入你想要取得的對(duì)象的鍵。

var onlyRange = IDBKeyRange.only(\"007\");

這個(gè)范圍可以保證只取得鍵為\"007\"的對(duì)象。使用這個(gè)范圍創(chuàng)建的游標(biāo)與直接訪問(wèn)存儲(chǔ)空間并調(diào)用

get(\"007\")差不多。

第二種定義鍵范圍的方式是指定結(jié)果集的下界。下界表示游標(biāo)開始的位置。例如,以下鍵范圍可以

保證游標(biāo)從鍵為\"007\"的對(duì)象開始,然后繼續(xù)向前移動(dòng),直至最后一個(gè)對(duì)象。

//從鍵為\"007\"的對(duì)象開始,然后可以移動(dòng)到最后

var lowerRange = IDBKeyRange.lowerBound(\"007\");

如果你想忽略鍵為\"007\"的對(duì)象,從它的下一個(gè)對(duì)象開始,那么可以傳入第二個(gè)參數(shù) true:

//從鍵為\"007\"的對(duì)象的下一個(gè)對(duì)象開始,然后可以移動(dòng)到最后

var lowerRange = IDBKeyRange.lowerBound(\"007\", true);

第三種定義鍵范圍的方式是指定結(jié)果集的上界,也就是指定游標(biāo)不能超越哪個(gè)鍵。指定上界使用

upperRange()方法。下面這個(gè)鍵范圍可以保證游標(biāo)從頭開始,到取得鍵為\"ace\"的對(duì)象終止。

//從頭開始,到鍵為\"ace\"的對(duì)象為止

var upperRange = IDBKeyRange.upperBound(\"ace\");

如果你不想包含鍵為指定值的對(duì)象,同樣,傳入第二個(gè)參數(shù) true:

//從頭開始,到鍵為\"ace\"的對(duì)象的上一個(gè)對(duì)象為止

var upperRange = IDBKeyRange.upperBound(\"ace\", true);

第四種定義鍵范圍的方式——沒錯(cuò),就是同時(shí)指定上、下界,使用 bound()方法。這個(gè)方法可以接

收 4 個(gè)參數(shù):表示下界的鍵、表示上界的鍵、可選的表示是否跳過(guò)下界的布爾值和可選的表示是否跳過(guò)

上界的布爾值。以下是幾個(gè)例子。

//從鍵為\"007\"的對(duì)象開始,到鍵為\"ace\"的對(duì)象為止

var boundRange = IDBKeyRange.bound(\"007\", \"ace\");

//從鍵為\"007\"的對(duì)象的下一個(gè)對(duì)象開始,到鍵為\"ace\"的對(duì)象為止

var boundRange = IDBKeyRange.bound(\"007\", \"ace\", true);

//從鍵為\"007\"的對(duì)象的下一個(gè)對(duì)象開始,到鍵為\"ace\"的對(duì)象的上一個(gè)對(duì)象為止

var boundRange = IDBKeyRange.bound(\"007\", \"ace\", true, true);

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第669頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 651

1

15

16

4

5

13

6

20

21

9

23

11

12

//從鍵為\"007\"的對(duì)象開始,到鍵為\"ace\"的對(duì)象的上一個(gè)對(duì)象為止

var boundRange = IDBKeyRange.bound(\"007\", \"ace\", false, true);

無(wú)論如何,在定義鍵范圍之后,把它傳給 openCursor()方法,就能得到一個(gè)符合相應(yīng)約束條件的

游標(biāo)。

var store = db.transaction(\"users\").objectStore(\"users\"),

range = IDBKeyRange.bound(\"007\", \"ace\");

request = store.openCursor(range);

request.onsuccess = function(event){

var cursor = event.target.result;

if (cursor){ //必須要檢查

console.log(\"Key: \" + cursor.key + \", Value: \" +

JSON.stringify(cursor.value));

cursor.continue(); //移動(dòng)到下一項(xiàng)

} else {

console.log(\"Done!\");

}

};

這個(gè)例子輸出的對(duì)象的鍵為\"007\"到\"ace\",比上一節(jié)最后那個(gè)例子輸出的值少一些。

6. 設(shè)定游標(biāo)方向

實(shí)際上,openCursor()可以接收兩個(gè)參數(shù)。第一個(gè)參數(shù)就是剛剛看到的 IDBKeyRange 的實(shí)例,

第二個(gè)是表示方向的數(shù)值常量。作為第二個(gè)參數(shù)的常量是前面講查詢時(shí)介紹的 IDBCursor 中的常量。

Fire fox4 +和 Chrome 的實(shí)現(xiàn)又有不同,因此第一步還是在本地消除差異:

var IDBCursor = window.IDBCursor || window.webkitIDBCursor;

正常情況下,游標(biāo)都是從存儲(chǔ)空間的第一項(xiàng)開始,調(diào)用 continue()或 advance()前進(jìn)到最后一

項(xiàng)。游標(biāo)的默認(rèn)方向值是 IDBCursor.NEXT。如果對(duì)象存儲(chǔ)空間中有重復(fù)的項(xiàng),而你想讓游標(biāo)跳過(guò)那些

重復(fù)的項(xiàng),可以為 openCursor 傳入 IDBCursor.NEXT_NO_DUPLICATE 作為第二個(gè)參數(shù):

var store = db.transaction(\"users\").objectStore(\"users\"),

request = store.openCursor(null, IDBCursor.NEXT_NO_DUPLICATE);

注意,openCursor()的第一個(gè)參數(shù)是 null,表示使用默認(rèn)的鍵范圍,即包含所有對(duì)象。這個(gè)游

標(biāo)可以從存儲(chǔ)空間中的第一個(gè)對(duì)象開始,逐個(gè)迭代到最后一個(gè)對(duì)象——但會(huì)跳過(guò)重復(fù)的對(duì)象。

當(dāng)然,也可以創(chuàng)建一個(gè)游標(biāo),讓它在對(duì)象存儲(chǔ)空間中向后移動(dòng),即從最后一個(gè)對(duì)象開始,逐個(gè)迭

代,直至第一個(gè)對(duì)象。此時(shí),要傳入的常量是 IDBCursor.PREV 和 IDBCursor.PREV_NO_DUPLICATE。

例如:

var store = db.transaction(\"users\").objectStore(\"users\"),

request = store.openCursor(null, IDBCursor.PREV);

IndexedDBExample05.htm

使用 IDBCursor.PREV 或 IDBCursor.PREV_NO_DUPLICATE 打開游標(biāo)時(shí),每次調(diào)用 continue()

或 advance(),都會(huì)在存儲(chǔ)空間中向后而不是向前移動(dòng)游標(biāo)。

7. 索引

對(duì)于某些數(shù)據(jù),可能需要為一個(gè)對(duì)象存儲(chǔ)空間指定多個(gè)鍵。比如,若要通過(guò)用戶 ID 和用戶名兩種

方式來(lái)保存用戶資料,就需要通過(guò)這兩個(gè)鍵來(lái)存取記錄。為此,可以考慮將用戶 ID 作為主鍵,然后為

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第670頁(yè)

652 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

用戶名創(chuàng)建索引。

要?jiǎng)?chuàng)建索引,首先引用對(duì)象存儲(chǔ)空間,然后調(diào)用 createIndex()方法,如下所示。

var store = db.transaction(\"users\").objectStore(\"users\"),

index = store.createIndex(\"username\", \"username\", { unique: false});

createIndex()的第一個(gè)參數(shù)是索引的名字,第二個(gè)參數(shù)是索引的屬性的名字,第三個(gè)參數(shù)是一

個(gè)包含 unique 屬性的選項(xiàng)(options)對(duì)象。這個(gè)選項(xiàng)通常都必須指定,因?yàn)樗硎炬I在所有記錄中

是否唯一。因?yàn)?username 有可能重復(fù),所以這個(gè)索引不是唯一的。

createIndex()的返回值是 IDBIndex 的實(shí)例。在對(duì)象存儲(chǔ)空間上調(diào)用 index()方法也能返回同

一個(gè)實(shí)例。例如,要使用一個(gè)已經(jīng)存在的名為\"username\"的索引,可以像下面這樣取得該索引。

var store = db.transaction(\"users\").objectStore(\"users\"),

index = store.index(\"username\");

索引其實(shí)與對(duì)象存儲(chǔ)空間很相似。在索引上調(diào)用 openCursor()方法也可以創(chuàng)建新的游標(biāo),除了將

來(lái)會(huì)把索引鍵而非主鍵保存在 event.result.key 屬性中之外,這個(gè)游標(biāo)與在對(duì)象存儲(chǔ)空間上調(diào)用

openCursor()返回的游標(biāo)完全一樣。來(lái)看下面的例子。

var store = db.transaction(\"users\").objectStore(\"users\"),

index = store.index(\"username\"),

request = index.openCursor();

request.onsuccess = function(event){

//處理成功

};

在索引上也能創(chuàng)建一個(gè)特殊的只返回每條記錄主鍵的游標(biāo),那就要調(diào)用 openKeyCursor()方法。

這個(gè)方法接收的參數(shù)與 openCursor()相同。而最大的不同在于,這種情況下 event.result.key 中

仍然保存著索引鍵,而 event.result.value 中保存的則是主鍵,而不再是整個(gè)對(duì)象。

var store = db.transaction(\"users\").objectStore(\"users\"),

index = store.index(\"username\"),

request = index.openKeyCursor();

request.onsuccess = function(event){

//處理成功

// event.result.key 中保存索引鍵,而 event.result.value 中保存主鍵

};

同樣,使用 get()方法能夠從索引中取得一個(gè)對(duì)象,只要傳入相應(yīng)的索引鍵即可;當(dāng)然,這個(gè)方法

也將返回一個(gè)請(qǐng)求。

var store = db.transaction(\"users\").objectStore(\"users\"),

index = store.index(\"username\"),

request = index.get(\"007\");

request.onsuccess = function(event){

//處理成功

};

request.onerror = function(event){

//處理失敗

};

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第671頁(yè)

23.3 數(shù)據(jù)存儲(chǔ) 653

1

15

16

4

5

13

6

20

21

9

23

11

12

要根據(jù)給定的索引鍵取得主鍵,可以使用 getKey()方法。這個(gè)方法也會(huì)創(chuàng)建一個(gè)新的請(qǐng)求,但

event.result.value 等于主鍵的值,而不是包含整個(gè)對(duì)象。

var store = db.transaction(\"users\").objectStore(\"users\"),

index = store.index(\"username\"),

request = index.getKey(\"007\");

request.onsuccess = function(event){

//處理成功

//event.result.key 中保存索引鍵,而 event.result.value 中保存主鍵

};

在這個(gè)例子的 onsuccess 事件處理程序中,event.result.value 中保存的是用戶 ID。

任何時(shí)候,通過(guò) IDBIndex 對(duì)象的下列屬性都可以取得有關(guān)索引的相關(guān)信息。

? name:索引的名字。

? keyPath:傳入 createIndex()中的屬性路徑。

? objectStore:索引的對(duì)象存儲(chǔ)空間。

? unique:表示索引鍵是否唯一的布爾值。

另外,通過(guò)對(duì)象存儲(chǔ)對(duì)象的 indexName 屬性可以訪問(wèn)到為該空間建立的所有索引。通過(guò)以下代碼

就可以知道根據(jù)存儲(chǔ)的對(duì)象建立了哪些索引。

var store = db.transaction(\"users\").objectStore(\"users\"),

indexNames = store.indexNames,

index,

i = 0,

len = indexNames.length;

while(i < len){

index = store.index(indexNames[i++]);

console.log(\"Index name: \" + index.name + \", KeyPath: \" + index.keyPath +

\", Unique: \" + index.unique);

}

以上代碼遍歷了每個(gè)索引,在控制臺(tái)中輸出了它們的信息。

在對(duì)象存儲(chǔ)空間上調(diào)用 deleteIndex()方法并傳入索引的名字可以刪除索引。

var store = db.transaction(\"users\").objectStore(\"users\");

store.deleteIndex(\"username\");

因?yàn)閯h除索引不會(huì)影響對(duì)象存儲(chǔ)空間中的數(shù)據(jù),所以這個(gè)操作沒有任何回調(diào)函數(shù)。

8. 并發(fā)問(wèn)題

雖然網(wǎng)頁(yè)中的 IndexedDB 提供的是異步 API,但仍然存在并發(fā)操作的問(wèn)題。如果瀏覽器的兩個(gè)不同

的標(biāo)簽頁(yè)打開了同一個(gè)頁(yè)面,那么一個(gè)頁(yè)面試圖更新另一個(gè)頁(yè)面尚未準(zhǔn)備就緒的數(shù)據(jù)庫(kù)的問(wèn)題就有可能

發(fā)生。把數(shù)據(jù)庫(kù)設(shè)置為新版本有可能導(dǎo)致這個(gè)問(wèn)題。因此,只有當(dāng)瀏覽器中僅有一個(gè)標(biāo)簽頁(yè)使用數(shù)據(jù)庫(kù)

的情況下,調(diào)用 setVersion()才能完成操作。

剛打開數(shù)據(jù)庫(kù)時(shí),要記著指定 onversionchange 事件處理程序。當(dāng)同一個(gè)來(lái)源的另一個(gè)標(biāo)簽頁(yè)調(diào)

用 setVersion()時(shí),就會(huì)執(zhí)行這個(gè)回調(diào)函數(shù)。處理這個(gè)事件的最佳方式是立即關(guān)閉數(shù)據(jù)庫(kù),從而保證

版本更新順利完成。例如:

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第672頁(yè)

654 第 23 章 離線應(yīng)用與客戶端存儲(chǔ)

var request, database;

request = indexedDB.open(\"admin\");

request.onsuccess = function(event){

database = event.target.result;

database.onversionchange = function(){

database.close();

};

};

每次成功打開數(shù)據(jù)庫(kù),都應(yīng)該指定 onversionchange 事件處理程序。

調(diào)用 setVersion()時(shí),指定請(qǐng)求的 onblocked 事件處理程序也很重要。在你想要更新數(shù)據(jù)庫(kù)的

版本但另一個(gè)標(biāo)簽頁(yè)已經(jīng)打開數(shù)據(jù)庫(kù)的情況下,就會(huì)觸發(fā)這個(gè)事件處理程序。此時(shí),最好先通知用戶關(guān)

閉其他標(biāo)簽頁(yè),然后再重新調(diào)用 setVersion()。例如:

var request = database.setVersion(\"2.0\");

request.onblocked = function(){

alert(\"Please close all other tabs and try again.\");

};

request.onsuccess = function(){

//處理成功,繼續(xù)

};

請(qǐng)記住,其他標(biāo)簽頁(yè)中的 onversionchange 事件處理程序也會(huì)執(zhí)行。

通過(guò)指定這些事件處理程序,就能確保你的 Web 應(yīng)用妥善地處理好 IndexedDB 的并發(fā)問(wèn)題。

9. 限制

對(duì) IndexedDB 的限制很多都與對(duì) Web Storage 的類似。首先,IndexedDB 數(shù)據(jù)庫(kù)只能由同源(相同

協(xié)議、域名和端口)頁(yè)面操作,因此不能跨域共享信息。換句話說(shuō),www.wrox.com 與 p2p.wrox.com

的數(shù)據(jù)庫(kù)是完全獨(dú)立的。

其次,每個(gè)來(lái)源的數(shù)據(jù)庫(kù)占用的磁盤空間也有限制。Firefox 4+目前的上限是每個(gè)源 50MB,而

Chrome 的限制是 5MB。移動(dòng)設(shè)備上的 Firefox 最多允許保存 5MB,如果超過(guò)了這個(gè)配額,將會(huì)請(qǐng)求

用戶的許可。

Firefox 還有另外一個(gè)限制,即不允許本地文件訪問(wèn) IndexedDB。Chrome 沒有這個(gè)限制。如果你在

本地運(yùn)行本書的示例,請(qǐng)使用 Chrome。

23.4 小結(jié)

離線 Web 應(yīng)用和客戶端存儲(chǔ)數(shù)據(jù)的能力對(duì)未來(lái)的 Web 應(yīng)用越來(lái)越重要。瀏覽器已經(jīng)能夠檢測(cè)到用

戶是否離線,并觸發(fā) JavaScript 事件以便應(yīng)用做出處理??梢灾付ㄔ趹?yīng)用緩存中保存哪些文件以便離線

時(shí)使用。對(duì)于應(yīng)用緩存的狀態(tài)及變化,也有相應(yīng)的 JavaScript API 可以調(diào)用檢測(cè)。

本書還討論了客戶端存儲(chǔ)的以下幾方面內(nèi)容。

? 以前,這種存儲(chǔ)只能使用 cookie 完成,cookie 是一小塊可以客戶端設(shè)置也可以在服務(wù)器端設(shè)置

的信息,每次發(fā)起請(qǐng)求時(shí)都會(huì)傳送它。

? 在 JavaScript 中通過(guò) document.cookie 可以訪問(wèn) cookie。

? cookie 的限制使其可以存儲(chǔ)少量數(shù)據(jù),然而對(duì)于大量數(shù)據(jù)效率很低。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第673頁(yè)

23.4 小結(jié) 655

1

15

16

4

5

13

6

20

21

9

23

11

12

IE 發(fā)明了一種叫做用戶數(shù)據(jù)的行為,可以應(yīng)用到頁(yè)面的某個(gè)元素上,它有以下特點(diǎn)。

? 一旦應(yīng)用后,該元素便可以從一個(gè)命名數(shù)據(jù)空間中載入數(shù)據(jù),然后可以通過(guò) getAttribute()、

setAttribute()和 removeAttribute()方法訪問(wèn)。

? 數(shù)據(jù)必須明確使用 save()方法保存到命名數(shù)據(jù)空間中,以便能在會(huì)話之間持久化數(shù)據(jù)。

Web Storage 定義了兩種用于存儲(chǔ)數(shù)據(jù)的對(duì)象:sessionStorage 和 localStorage。前者嚴(yán)格用

于在一個(gè)瀏覽器會(huì)話中存儲(chǔ)數(shù)據(jù),因?yàn)閿?shù)據(jù)在瀏覽器關(guān)閉后會(huì)立即刪除;后者用于跨會(huì)話持久化數(shù)據(jù)并

遵循跨域安全策略。

IndexedDB 是一種類似 SQL 數(shù)據(jù)庫(kù)的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)機(jī)制。但它的數(shù)據(jù)不是保存在表中,而是保存

在對(duì)象存儲(chǔ)空間中。創(chuàng)建對(duì)象存儲(chǔ)空間時(shí),需要定義一個(gè)鍵,然后就可以添加數(shù)據(jù)。可以使用游標(biāo)在對(duì)

象存儲(chǔ)空間中查詢特定的對(duì)象。而索引則是為了提高查詢速度而基于特定的屬性創(chuàng)建的。

有了以上這些選擇,就可以在客戶端機(jī)器上使用 JavaScript 存儲(chǔ)大量數(shù)據(jù)了。但你必須小心,不要

在客戶端存儲(chǔ)敏感數(shù)據(jù),因?yàn)閿?shù)據(jù)緩存不會(huì)加密。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第674頁(yè)

656 第 24 章 最佳實(shí)踐

最 佳 實(shí) 踐

本章內(nèi)容

? 可維護(hù)的代碼

? 保證代碼性能

? 部署代碼

從 2000 以來(lái),Web 開發(fā)方面的種種規(guī)范、條例正在高速發(fā)展。Web 開發(fā)過(guò)去曾是荒蕪地帶,

里面東西還都湊合,而現(xiàn)在已經(jīng)演化成了完整的研究規(guī)范,并建立了種種最佳實(shí)踐。隨著簡(jiǎn)

單的網(wǎng)站成長(zhǎng)為更加復(fù)雜的 Web 應(yīng)用,同時(shí) Web 愛好者成為了有收入的專業(yè)人士,Web 開發(fā)的世界充

滿了各種關(guān)于最新技術(shù)和開發(fā)方法的信息。尤其是 JavaScript,它從大量的研究和推斷中獲益。JavaScript

的最佳實(shí)踐分成若干類,并在開發(fā)過(guò)程的不同點(diǎn)上進(jìn)行處理。

24.1 可維護(hù)性

在早期的網(wǎng)站中,JavaScript 主要是用于小特效或者是表單驗(yàn)證。而今天的 Web 應(yīng)用則會(huì)有成千上

萬(wàn)行 JavaScript 代碼,執(zhí)行各種復(fù)雜的過(guò)程。這種演化讓開發(fā)者必須得考慮到可維護(hù)性。除了秉承較傳

統(tǒng)理念的軟件工程師外,還要雇傭 JavaScript 開發(fā)人員為公司創(chuàng)造價(jià)值,而他們并非僅僅按時(shí)交付產(chǎn)品,

同時(shí)還要開發(fā)智力成果在之后不斷地增加價(jià)值。

編寫可維護(hù)的代碼很重要,因?yàn)榇蟛糠珠_發(fā)人員都花費(fèi)大量時(shí)間維護(hù)他人代碼。很難從頭開始開發(fā)

新代碼的,很多情況下是以他人的工作成果為基礎(chǔ)的。確保自己代碼的可維護(hù)性,以便其他開發(fā)人員在

此基礎(chǔ)上更好的開展工作。

注意可維護(hù)的代碼的概念并不是 JavaScript 特有的。這里的很多概念都可以廣泛

應(yīng)用于各種編程語(yǔ)言,當(dāng)然也有某些特定于 JavaScript 的概念。

24.1.1 什么是可維護(hù)的代碼

可維護(hù)的代碼有一些特征。一般來(lái)說(shuō),如果說(shuō)代碼是可維護(hù)的,它需要遵循以下特點(diǎn)。

? 可理解性——其他人可以接手代碼并理解它的意圖和一般途徑,而無(wú)需原開發(fā)人員的完整解釋。

? 直觀性——代碼中的東西一看就能明白,不管其操作過(guò)程多么復(fù)雜。

? 可適應(yīng)性——代碼以一種數(shù)據(jù)上的變化不要求完全重寫的方法撰寫。

? 可擴(kuò)展性——在代碼架構(gòu)上已考慮到在未來(lái)允許對(duì)核心功能進(jìn)行擴(kuò)展。

第 24 章

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第675頁(yè)

24.1 可維護(hù)性 657

14

2

3

17

18

13

19

7

8

22

10

24

12

? 可調(diào)試性——當(dāng)有地方出錯(cuò)時(shí),代碼可以給予你足夠的信息來(lái)盡可能直接地確定問(wèn)題所在。

對(duì)于專業(yè)人士而言,能寫出可維護(hù)的 JavaScript 代碼是非常重要的技能。這正是周末改改網(wǎng)站的愛

好者和真正理解自己作品的開發(fā)人員之間的區(qū)別。

24.1.2 代碼約定

一種讓代碼變得可維護(hù)的簡(jiǎn)單途徑是形成一套 JavaScript 代碼的書寫約定。絕大多數(shù)語(yǔ)言都開發(fā)出

了各自的代碼約定,只要在網(wǎng)上一搜就能找到大量相關(guān)文檔。專業(yè)的組織為開發(fā)人員制定了詳盡的代碼

約定試圖讓代碼對(duì)任何人都可維護(hù)。杰出的開放源代碼項(xiàng)目有著嚴(yán)格的代碼約定要求,這讓社區(qū)中的任

何人都可以輕松地理解代碼是如何組織的。

由于 JavaScript 的可適應(yīng)性,代碼約定對(duì)它也很重要。由于和大多數(shù)面向?qū)ο笳Z(yǔ)言不同,JavaScript

并不強(qiáng)制開發(fā)人員將所有東西都定義為對(duì)象。語(yǔ)言可以支持各種編程風(fēng)格,從傳統(tǒng)面向?qū)ο笫降铰暶魇?/p>

到函數(shù)式。只要快速瀏覽一下一些開源 JavaScript 庫(kù),就能發(fā)現(xiàn)好幾種創(chuàng)建對(duì)象、定義方法和管理環(huán)境

的途徑。

以下小節(jié)將討論代碼約定的概論。對(duì)這些主題的解說(shuō)非常重要,雖然可能的解說(shuō)方式會(huì)有區(qū)別,這

取決于個(gè)人需求。

1. 可讀性

要讓代碼可維護(hù),首先它必須可讀。可讀性與代碼作為文本文件的格式化方式有關(guān)??勺x性的大部

分內(nèi)容都是和代碼的縮進(jìn)相關(guān)的。當(dāng)所有人都使用一樣的縮進(jìn)方式時(shí),整個(gè)項(xiàng)目中的代碼都會(huì)更加易于

閱讀。通常會(huì)使用若干空格而非制表符來(lái)進(jìn)行縮進(jìn),這是因?yàn)橹票矸诓煌奈谋揪庉嬈髦酗@示效果不

同。一種不錯(cuò)的、很常見的縮進(jìn)大小為 4 個(gè)空格,當(dāng)然你也可以使用其他數(shù)量。

可讀性的另一方面是注釋。在大多數(shù)編程語(yǔ)言中,對(duì)每個(gè)方法的注釋都視為一個(gè)可行的實(shí)踐。因?yàn)?/p>

JavaScript 可以在代碼的任何地方創(chuàng)建函數(shù),所以這點(diǎn)常常被忽略了。然而正因如此,在 JavaScript 中為

每個(gè)函數(shù)編寫文檔就更加重要了。一般而言,有如下一些地方需要進(jìn)行注釋。

? 函數(shù)和方法——每個(gè)函數(shù)或方法都應(yīng)該包含一個(gè)注釋,描述其目的和用于完成任務(wù)所可能使用

的算法。陳述事先的假設(shè)也非常重要,如參數(shù)代表什么,函數(shù)是否有返回值(因?yàn)檫@不能從函

數(shù)定義中推斷出來(lái))。

? 大段代碼——用于完成單個(gè)任務(wù)的多行代碼應(yīng)該在前面放一個(gè)描述任務(wù)的注釋。

? 復(fù)雜的算法——如果使用了一種獨(dú)特的方式解決某個(gè)問(wèn)題,則要在注釋中解釋你是如何做的。

這不僅僅可以幫助其他瀏覽你代碼的人,也能在下次你自己查閱代碼的時(shí)候幫助理解。

? Hack——因?yàn)榇嬖跒g覽器差異,JavaScript 代碼一般會(huì)包含一些 hack。不要假設(shè)其他人在看代

碼的時(shí)候能夠理解 hack 所要應(yīng)付的瀏覽器問(wèn)題。如果因?yàn)槟撤N瀏覽器無(wú)法使用普通的方法,

所以你需要用一些不同的方法,那么請(qǐng)將這些信息放在注釋中。這樣可以減少出現(xiàn)這種情況的

可能性:有人偶然看到你的 hack,然后“修正”了它,最后重新引入了你本來(lái)修正了的錯(cuò)誤。

縮進(jìn)和注釋可以帶來(lái)更可讀的代碼,在未來(lái)則更容易維護(hù)。

2. 變量和函數(shù)命名

適當(dāng)給變量和函數(shù)起名字對(duì)于增加代碼可理解性和可維護(hù)性是非常重要的。由于很多 JavaScript 開

發(fā)人員最初都只是業(yè)余愛好者,所以有一種使用無(wú)意義名字的傾向,諸如給變量起\"foo\"、\"bar\"等名

字,給函數(shù)起\"doSomething\"這樣的名字。專業(yè) JavaScript 開發(fā)人員必須克服這些惡習(xí)以創(chuàng)建可維護(hù)的

代碼。命名的一般規(guī)則如下所示。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第676頁(yè)

658 第 24 章 最佳實(shí)踐

? 變量名應(yīng)為名詞如 car 或 person。

? 函數(shù)名應(yīng)該以動(dòng)詞開始,如 getName()。返回布爾類型值的函數(shù)一般以 is 開頭,如

isEnable()。

? 變量和函數(shù)都應(yīng)使用合乎邏輯的名字,不要擔(dān)心長(zhǎng)度。長(zhǎng)度問(wèn)題可以通過(guò)后處理和壓縮(本章

后面會(huì)講到)來(lái)緩解。

必須避免出現(xiàn)無(wú)法表示所包含的數(shù)據(jù)類型的無(wú)用變量名。有了合適的命名,代碼閱讀起來(lái)就像講述

故事一樣,更容易理解。

3. 變量類型透明

由于在 JavaScript 中變量是松散類型的,很容易就忘記變量所應(yīng)包含的數(shù)據(jù)類型。合適的命名方式

可以一定程度上緩解這個(gè)問(wèn)題,但放到所有的情況下看,還不夠。有三種表示變量數(shù)據(jù)類型的方式。

第一種方式是初始化。當(dāng)定義了一個(gè)變量后,它應(yīng)該被初始化為一個(gè)值,來(lái)暗示它將來(lái)應(yīng)該如何應(yīng)

用。例如,將來(lái)保存布爾類型值的變量應(yīng)該初始化為 true 或者 false,將來(lái)保存數(shù)字的變量就應(yīng)該初

始化為一個(gè)數(shù)字,如以下例子所示:

//通過(guò)初始化指定變量類型

var found = false; //布爾型

var count = -1; //數(shù)字

var name = \"\"; //字符串

var person = null; //對(duì)象

初始化為一個(gè)特定的數(shù)據(jù)類型可以很好的指明變量的類型。但缺點(diǎn)是它無(wú)法用于函數(shù)聲明中的函數(shù)

參數(shù)。

第二種方法是使用匈牙利標(biāo)記法來(lái)指定變量類型。匈牙利標(biāo)記法在變量名之前加上一個(gè)或多個(gè)字符

來(lái)表示數(shù)據(jù)類型。這個(gè)標(biāo)記法在腳本語(yǔ)言中很流行,曾經(jīng)很長(zhǎng)時(shí)間也是 JavaScript 所推崇的方式。

JavaScript 中最傳統(tǒng)的匈牙利標(biāo)記法是用單個(gè)字符表示基本類型:\"o\"代表對(duì)象,\"s\"代表字符串,\"i\"

代表整數(shù),\"f\"代表浮點(diǎn)數(shù),\"b\"代表布爾型。如下所示:

//用于指定數(shù)據(jù)類型的匈牙利標(biāo)記法

var bFound; //布爾型

var iCount; //整數(shù)

var sName; //字符串

var oPerson; //對(duì)象

JavaScript 中用匈牙利標(biāo)記法的好處是函數(shù)參數(shù)一樣可以使用。但它的缺點(diǎn)是讓代碼某種程度上難

以閱讀,阻礙了沒有用它時(shí)代碼的直觀性和句子式的特質(zhì)。因此,匈牙利標(biāo)記法失去了一些開發(fā)者的

寵愛。

最后一種指定變量類型的方式是使用類型注釋。類型注釋放在變量名右邊,但是在初始化前面。這

種方式是在變量旁邊放一段指定類型的注釋,如下所示:

//用于指定類型的類型注釋

var found /*:Boolean*/ = false;

var count /*:int*/ = 10;

var name /*:String*/ = \"Nicholas\";

var person /*:Object*/ = null;

類型注釋維持了代碼的整體可讀性,同時(shí)注入了類型信息。類型注釋的缺點(diǎn)是你不能用多行注釋一

次注釋大塊的代碼,因?yàn)轭愋妥⑨屢彩嵌嘈凶⑨專瑑烧邥?huì)沖突,如下例所示所示:

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第677頁(yè)

24.1 可維護(hù)性 659

14

2

3

17

18

13

19

7

8

22

10

24

12

//以下代碼不能正確運(yùn)行

/*

var found /*:Boolean*/ = false;

var count /*:int*/ = 10;

var name /*:String*/ = \"Nicholas\";

var person /*:Object*/ = null;

*/

這里,試圖通過(guò)多行注釋注釋所有變量。類型注釋與其相沖突,因?yàn)榈谝淮纬霈F(xiàn)的 /* (第二行)

匹配了第一次出現(xiàn)的*/(第 3 行),這會(huì)造成一個(gè)語(yǔ)法錯(cuò)誤。如果你想注釋掉這些使用類型注釋的代碼

行,最好在每一行上使用單行注釋(很多編輯器可以幫你完成)。

這就是最常見的三種指定變量數(shù)據(jù)類型的方法。每種都有各自的優(yōu)勢(shì)和劣勢(shì),要自己在使用之前進(jìn)

行評(píng)估。最重要的是要確定哪種最適合你的項(xiàng)目并一致使用。

24.1.3 松散耦合

只要應(yīng)用的某個(gè)部分過(guò)分依賴于另一部分,代碼就是耦合過(guò)緊,難于維護(hù)。典型的問(wèn)題如:對(duì)象直

接引用另一個(gè)對(duì)象,并且當(dāng)修改其中一個(gè)的同時(shí)需要修改另外一個(gè)。緊密耦合的軟件難于維護(hù)并且需要

經(jīng)常重寫。

因?yàn)?Web 應(yīng)用所涉及的技術(shù),有多種情況會(huì)使它變得耦合過(guò)緊。必須小心這些情況,并盡可能維

護(hù)弱耦合的代碼。

1. 解耦 HTML/JavaScript

一種最常見的耦合類型是 HTML/JavaScript 耦合。在 Web 上,HTML 和 JavaScript 各自代表了解決

方案中的不同層次:HTML 是數(shù)據(jù),JavaScript 是行為。因?yàn)樗鼈兲焐托枰换?,所以有多種不同的

方法將這兩個(gè)技術(shù)關(guān)聯(lián)起來(lái)。但是,有一些方法會(huì)將 HTML 和 JavaScript 過(guò)于緊密地耦合在一起。

直接寫在 HTML 中的 JavaScript,使用包含內(nèi)聯(lián)代碼的<script>元素或者是使用 HTML 屬性來(lái)分

配事件處理程序,都是過(guò)于緊密的耦合。請(qǐng)看以下代碼。

<!-- 使用了 <script> 的緊密耦合的 HTML/JavaScript -->

<script type=\"text/javascript\">

document.write(\"Hello world!\");

</script>

<!-- 使用事件處理程序?qū)傩灾档木o密耦合的 HTML/JavaScript -->

<input type=\"button\" value=\"Click Me\" onclick=\"doSomething()\" />

雖然這些從技術(shù)上來(lái)說(shuō)都是正確的,但是實(shí)踐中,它們將表示數(shù)據(jù)的 HTML 和定義行為的 JavaScript

緊密耦合在了一起。理想情況是,HTML 和 JavaScript 應(yīng)該完全分離,并通過(guò)外部文件和使用 DOM 附

加行為來(lái)包含 JavaScript。

當(dāng) HTML 和 JavaScript 過(guò)于緊密的耦合在一起時(shí),出現(xiàn) JavaScript 錯(cuò)誤時(shí)就要先判斷錯(cuò)誤是出現(xiàn)在

HTML 部分還是在 JavaScript 文件中。它還會(huì)引入和代碼是否可用的相關(guān)新問(wèn)題。在這個(gè)例子中,可能

在 doSomething()函數(shù)可用之前,就已經(jīng)按下了按鈕,引發(fā)了一個(gè) JavaScript 錯(cuò)誤。因?yàn)槿魏螌?duì)按鈕

行為的更改要同時(shí)觸及 HTML 和 JavaScript,因此影響了可維護(hù)性。而這些更改本該只在 JavaScript 中

進(jìn)行。

HTML 和 JavaScript 的緊密耦合也可以在相反的關(guān)系上成立:JavaScript 包含了 HTML。這通常會(huì)出

現(xiàn)在使用 innerHTML 來(lái)插入一段 HTML 文本到頁(yè)面上這種情況中,如下面的例子所示:

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第678頁(yè)

660 第 24 章 最佳實(shí)踐

//將 HTML 緊密耦合到 JavaScript

function insertMessage(msg){

var container = document.getElementById(\"container\");

container.innerHTML = \"<div class=\\\"msg\\\"><p class=\\\"post\\\">\" + msg + \"</p>\" +

\"<p><em>Latest message above.</em></p></div>\";

}

一般來(lái)說(shuō),你應(yīng)該避免在 JavaScript 中創(chuàng)建大量 HTML。再一次重申要保持層次的分離,這樣可以

很容易的確定錯(cuò)誤來(lái)源。當(dāng)使用上面這個(gè)例子的時(shí)候,有一個(gè)頁(yè)面布局的問(wèn)題,可能和動(dòng)態(tài)創(chuàng)建的 HTML

沒有被正確格式化有關(guān)。不過(guò),要定位這個(gè)錯(cuò)誤可能非常困難,因?yàn)槟憧赡芤话阆瓤错?yè)面的源代碼來(lái)查

找那段煩人的 HTML,但是卻沒能找到,因?yàn)樗莿?dòng)態(tài)生成的。對(duì)數(shù)據(jù)或者布局的更改也會(huì)要求更改

JavaScript,這也表明了這兩個(gè)層次過(guò)于緊密地耦合了。

HTML 呈現(xiàn)應(yīng)該盡可能與 JavaScript 保持分離。當(dāng) JavaScript 用于插入數(shù)據(jù)時(shí),盡量不要直接插入

標(biāo)記。一般可以在頁(yè)面中直接包含并隱藏標(biāo)記,然后等到整個(gè)頁(yè)面渲染好之后,就可以用 JavaScript 顯

示該標(biāo)記,而非生成它。另一種方法是進(jìn)行 Ajax 請(qǐng)求并獲取更多要顯示的 HTML,這個(gè)方法可以讓同

樣的渲染層(PHP、JSP、Ruby 等等)來(lái)輸出標(biāo)記,而不是直接嵌在 JavaScript 中。

將 HTML 和 JavaScript 解耦可以在調(diào)試過(guò)程中節(jié)省時(shí)間,更加容易確定錯(cuò)誤的來(lái)源,也減輕維護(hù)的

難度:更改行為只需要在 JavaScript 文件中進(jìn)行,而更改標(biāo)記則只要在渲染文件中。

2. 解耦 CSS/JavaScript

另一個(gè) Web 層則是 CSS,它主要負(fù)責(zé)頁(yè)面的顯示。JavaScript 和 CSS 也是非常緊密相關(guān)的:他們都

是 HTML 之上的層次,因此常常一起使用。但是,和 HTML 與 JavaScript 的情況一樣,CSS 和 JavaScript

也可能會(huì)過(guò)于緊密地耦合在一起。最常見的緊密耦合的例子是使用 JavaScript 來(lái)更改某些樣式,如下所示:

//CSS 對(duì) JavaScript 的緊密耦合

element.style.color = \"red\";

element.style.backgroundColor = \"blue\";

由于 CSS 負(fù)責(zé)頁(yè)面的顯示,當(dāng)顯示出現(xiàn)任何問(wèn)題時(shí)都應(yīng)該只是查看 CSS 文件來(lái)解決。然而,當(dāng)使

用了 JavaScript 來(lái)更改某些樣式的時(shí)候,比如顏色,就出現(xiàn)了第二個(gè)可能已更改和必須檢查的地方。結(jié)

果是 JavaScript 也在某種程度上負(fù)責(zé)了頁(yè)面的顯示,并與 CSS 緊密耦合了。如果未來(lái)需要更改樣式表,

CSS 和 JavaScript 文件可能都需要修改。這就給開發(fā)人員造成了維護(hù)上的噩夢(mèng)。所以在這兩個(gè)層次之間

必須有清晰的劃分。

現(xiàn)代 Web 應(yīng)用常常要使用 JavaScript 來(lái)更改樣式,所以雖然不可能完全將 CSS 和 JavaScript 解耦,

但是還是能讓耦合更松散的。這是通過(guò)動(dòng)態(tài)更改樣式類而非特定樣式來(lái)實(shí)現(xiàn)的,如下例所示:

//CSS 對(duì) JavaScript 的松散耦合

element.className = \"edit\";

通過(guò)只修改某個(gè)元素的 CSS 類,就可以讓大部分樣式信息嚴(yán)格保留在 CSS 中。JavaScript 可以更改

樣式類,但并不會(huì)直接影響到元素的樣式。只要應(yīng)用了正確的類,那么任何顯示問(wèn)題都可以直接追溯到

CSS 而非 JavaScript。

第二類緊密耦合僅會(huì)在 IE 中出現(xiàn)(但運(yùn)行于標(biāo)準(zhǔn)模式下的 IE8 不會(huì)出現(xiàn)),它可以在 CSS 中通過(guò)表

達(dá)式嵌入 JavaScript,如下例所示:

/* JavaScript 對(duì) CSS 的緊密耦合 */

div {

width: expression(document.body.offsetWidth - 10 + \"px\");

}

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第679頁(yè)

24.1 可維護(hù)性 661

14

2

3

17

18

13

19

7

8

22

10

24

12

通常要避免使用表達(dá)式,因?yàn)樗鼈儾荒芸鐬g覽器兼容,還因?yàn)樗鼈兯氲?JavaScript 和 CSS 之間

的緊密耦合。如果使用了表達(dá)式,那么可能會(huì)在 CSS 中出現(xiàn) JavaScript 錯(cuò)誤。由于 CSS 表達(dá)式而追蹤過(guò)

JavaScript 錯(cuò)誤的開發(fā)人員,會(huì)告訴你在他們決定看一下 CSS 之前花了多長(zhǎng)時(shí)間來(lái)查找錯(cuò)誤。

再次提醒,好的層次劃分是非常重要的。顯示問(wèn)題的唯一來(lái)源應(yīng)該是 CSS,行為問(wèn)題的唯一來(lái)源應(yīng)

該是 JavaScript。在這些層次之間保持松散耦合可以讓你的整個(gè)應(yīng)用更加易于維護(hù)。

3. 解耦應(yīng)用邏輯/事件處理程序

每個(gè) Web 應(yīng)用一般都有相當(dāng)多的事件處理程序,監(jiān)聽著無(wú)數(shù)不同的事件。然而,很少有能仔細(xì)得

將應(yīng)用邏輯從事件處理程序中分離的。請(qǐng)看以下例子:

function handleKeyPress(event){

event = EventUtil.getEvent(event);

if (event.keyCode == 13){

var target = EventUtil.getTarget(event);

var value = 5 * parseInt(target.value);

if (value > 10){

document.getElementById(\"error-msg\").style.display = \"block\";

}

}

}

這個(gè)事件處理程序除了包含了應(yīng)用邏輯,還進(jìn)行了事件的處理。這種方式的問(wèn)題有其雙重性。首先,

除了通過(guò)事件之外就再?zèng)]有方法執(zhí)行應(yīng)用邏輯,這讓調(diào)試變得困難。如果沒有發(fā)生預(yù)想的結(jié)果怎么辦?

是不是表示事件處理程序沒有被調(diào)用還是指應(yīng)用邏輯失?。科浯?,如果一個(gè)后續(xù)的事件引發(fā)同樣的應(yīng)用

邏輯,那就必須復(fù)制功能代碼或者將代碼抽取到一個(gè)單獨(dú)的函數(shù)中。無(wú)論何種方式,都要作比實(shí)際所需

更多的改動(dòng)。

較好的方法是將應(yīng)用邏輯和事件處理程序相分離,這樣兩者分別處理各自的東西。一個(gè)事件處理程

序應(yīng)該從事件對(duì)象中提取相關(guān)信息,并將這些信息傳送到處理應(yīng)用邏輯的某個(gè)方法中。例如,前面的代

碼可以被重寫為:

function validateValue(value){

value = 5 * parseInt(value);

if (value > 10){

document.getElementById(\"error-msg\").style.display = \"block\";

}

}

function handleKeyPress(event){

event = EventUtil.getEvent(event);

if (event.keyCode == 13){

var target = EventUtil.getTarget(event);

validateValue(target.value);

}

}

改動(dòng)過(guò)的代碼合理將應(yīng)用邏輯從事件處理程序中分離了出來(lái)。handleKeyPress() 函數(shù)確認(rèn)是按

下了 Enter 鍵(event.keyCode 為 13),取得了事件的目標(biāo)并將 value 屬性傳遞給 validateValue()

函數(shù),這個(gè)函數(shù)包含了應(yīng)用邏輯。注意 validateValue()中沒有任何東西會(huì)依賴于任何事件處理程序

邏輯,它只是接收一個(gè)值,并根據(jù)該值進(jìn)行其他處理。

從事件處理程序中分離應(yīng)用邏輯有幾個(gè)好處。首先,可以讓你更容易更改觸發(fā)特定過(guò)程的事件。如

果最開始由鼠標(biāo)點(diǎn)擊事件觸發(fā)過(guò)程,但現(xiàn)在按鍵也要進(jìn)行同樣處理,這種更改就很容易。其次,可以在

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第680頁(yè)

662 第 24 章 最佳實(shí)踐

不附加到事件的情況下測(cè)試代碼,使其更易創(chuàng)建單元測(cè)試或者是自動(dòng)化應(yīng)用流程。

以下是要牢記的應(yīng)用和業(yè)務(wù)邏輯之間松散耦合的幾條原則:

? 勿將 event 對(duì)象傳給其他方法;只傳來(lái)自 event 對(duì)象中所需的數(shù)據(jù);

? 任何可以在應(yīng)用層面的動(dòng)作都應(yīng)該可以在不執(zhí)行任何事件處理程序的情況下進(jìn)行;

? 任何事件處理程序都應(yīng)該處理事件,然后將處理轉(zhuǎn)交給應(yīng)用邏輯。

牢記這幾條可以在任何代碼中都獲得極大的可維護(hù)性的改進(jìn),并且為進(jìn)一步的測(cè)試和開發(fā)制造了很

多可能。

24.1.4 編程實(shí)踐

書寫可維護(hù)的 JavaScript 并不僅僅是關(guān)于如何格式化代碼;它還關(guān)系到代碼做什么的問(wèn)題。在企業(yè)

環(huán)境中創(chuàng)建的 Web 應(yīng)用往往同時(shí)由大量人員一同創(chuàng)作。這種情況下的目標(biāo)是確保每個(gè)人所使用的瀏覽

器環(huán)境都有一致和不變的規(guī)則。因此,最好堅(jiān)持以下一些編程實(shí)踐。

1. 尊重對(duì)象所有權(quán)

JavaScript 的動(dòng)態(tài)性質(zhì)使得幾乎任何東西在任何時(shí)間都可以修改。有人說(shuō)在 JavaScript 沒有什么神圣

的東西,因?yàn)闊o(wú)法將某些東西標(biāo)記為最終或恒定狀態(tài)。這種狀況在 ECMAScript 5 中通過(guò)引入防篡改對(duì)

象(第 22 章討論過(guò))得以改變;不過(guò),默認(rèn)情況下所有對(duì)象都是可以修改的。在其他語(yǔ)言中,當(dāng)沒有

實(shí)際的源代碼的時(shí)候,對(duì)象和類是不可變的。JavaScript 可以在任何時(shí)候修改任意對(duì)象,這樣就可以以

不可預(yù)計(jì)的方式覆寫默認(rèn)的行為。因?yàn)檫@門語(yǔ)言沒有強(qiáng)行的限制,所以對(duì)于開發(fā)者來(lái)說(shuō),這是很重要的,

也是必要的。

也許在企業(yè)環(huán)境中最重要的編程實(shí)踐就是尊重對(duì)象所有權(quán),它的意思是你不能修改不屬于你的對(duì)

象。簡(jiǎn)單地說(shuō),如果你不負(fù)責(zé)創(chuàng)建或維護(hù)某個(gè)對(duì)象、它的對(duì)象或者它的方法,那么你就不能對(duì)它們進(jìn)行

修改。更具體地說(shuō):

? 不要為實(shí)例或原型添加屬性;

? 不要為實(shí)例或原型添加方法;

? 不要重定義已存在的方法。

問(wèn)題在于開發(fā)人員會(huì)假設(shè)瀏覽器環(huán)境按照某個(gè)特定方式運(yùn)行,而對(duì)于多個(gè)人都用到的對(duì)象進(jìn)行改動(dòng)

就會(huì)產(chǎn)生錯(cuò)誤。如果某人期望叫做 stopEvent()的函數(shù)能取消某個(gè)事件的默認(rèn)行為,但是你對(duì)其進(jìn)行

了更改,然后它完成了本來(lái)的任務(wù),后來(lái)還追加了另外的事件處理程序,那肯定會(huì)出現(xiàn)問(wèn)題了。其他開

發(fā)人員會(huì)認(rèn)為函數(shù)還是按照原來(lái)的方式執(zhí)行,所以他們的用法會(huì)出錯(cuò)并有可能造成危害,因?yàn)樗麄儾⒉?/p>

知道有副作用。

這些規(guī)則不僅僅適用于自定義類型和對(duì)象,對(duì)于諸如 Object、String、document、window 等

原生類型和對(duì)象也適用。此處潛在的問(wèn)題可能更加危險(xiǎn),因?yàn)闉g覽器提供者可能會(huì)在不做宣布或者是不

可預(yù)期的情況下更改這些對(duì)象。

著名的 Prototype JavaScript 庫(kù)就出現(xiàn)過(guò)這種例子:它為 document 對(duì)象實(shí)現(xiàn)了 getElementsByClassName()方法,返回一個(gè) Array 的實(shí)例并增加了一個(gè) each()方法。John Resig 在他的博客上敘

述了產(chǎn)生這個(gè)問(wèn)題的一系列事件。他在帖子(http://ejohn.org/blog/getelementsbyclassname-pre-prototype-16/)

中說(shuō),他發(fā)現(xiàn)當(dāng)瀏覽器開始內(nèi)部實(shí)現(xiàn) getElementsByClassName()的時(shí)候就出現(xiàn)問(wèn)題了,這個(gè)方法并

不返回一個(gè) Array 而是返回一個(gè)并不包含 each()方法的 NodeList。使用 Prototype 庫(kù)的開發(fā)人員習(xí)慣

于寫這樣的代碼:

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第681頁(yè)

24.1 可維護(hù)性 663

14

2

3

17

18

13

19

7

8

22

10

24

12

document.getElementsByClassName(\"selected\").each(Element.hide);

雖然在沒有原生實(shí)現(xiàn) getElementsByClassName()的瀏覽器中可以正常運(yùn)行,但對(duì)于支持的了瀏

覽器就會(huì)產(chǎn)生錯(cuò)誤,因?yàn)榉祷氐闹挡煌?。你不能預(yù)測(cè)瀏覽器提供者在未來(lái)會(huì)怎樣更改原生對(duì)象,所以不

管用任何方式修改他們,都可能會(huì)導(dǎo)致將來(lái)你的實(shí)現(xiàn)和他們的實(shí)現(xiàn)之間的沖突。

所以,最佳的方法便是永遠(yuǎn)不修改不是由你所有的對(duì)象。所謂擁有對(duì)象,就是說(shuō)這個(gè)對(duì)象是你創(chuàng)建

的,比如你自己創(chuàng)建的自定義類型或?qū)ο笞置媪?。?Array、document 這些顯然不是你的,它們?cè)谀?/p>

的代碼執(zhí)行前就存在了。你依然可以通過(guò)以下方式為對(duì)象創(chuàng)建新的功能:

? 創(chuàng)建包含所需功能的新對(duì)象,并用它與相關(guān)對(duì)象進(jìn)行交互;

? 創(chuàng)建自定義類型,繼承需要進(jìn)行修改的類型。然后可以為自定義類型添加額外功能。

現(xiàn)在很多 JavaScript 庫(kù)都贊同并遵守這條開發(fā)原理,這樣即使瀏覽器頻繁更改,庫(kù)本身也能繼續(xù)成

長(zhǎng)和適應(yīng)。

2. 避免全局量

與尊重對(duì)象所有權(quán)密切相關(guān)的是盡可能避免全局變量和函數(shù)。這也關(guān)系到創(chuàng)建一個(gè)腳本執(zhí)行的一致

的和可維護(hù)的環(huán)境。最多創(chuàng)建一個(gè)全局變量,讓其他對(duì)象和函數(shù)存在其中。請(qǐng)看以下例子:

//兩個(gè)全局量——避免?。?/p>

var name = \"Nicholas\";

function sayName(){

alert(name);

}

這段代碼包含了兩個(gè)全局量:變量 name 和函數(shù) sayName()。其實(shí)可以創(chuàng)建一個(gè)包含兩者的對(duì)象,

如下例所示:

//一個(gè)全局量——推薦

var MyApplication = {

name: \"Nicholas\",

sayName: function(){

alert(this.name);

}

};

這段重寫的代碼引入了一個(gè)單一的全局對(duì)象 MyApplication,name 和 sayName()都附加到其上。

這樣做消除了一些存在于前一段代碼中的一些問(wèn)題。首先,變量 name 覆蓋了 window.name 屬性,可

能會(huì)與其他功能產(chǎn)生沖突;其次,它有助消除功能作用域之間的混淆。調(diào)用 MyApplication.sayName()

在邏輯上暗示了代碼的任何問(wèn)題都可以通過(guò)檢查定義 MyApplication 的代碼來(lái)確定。

單一的全局量的延伸便是命名空間的概念,由 YUI(Yahoo! User Interface)庫(kù)普及。命名空間包括

創(chuàng)建一個(gè)用于放置功能的對(duì)象。在 YUI 的 2.x 版本中,有若干用于追加功能的命名空間。比如:

? YAHOO.util.Dom —— 處理 DOM 的方法;

? YAHOO.util.Event —— 與事件交互的方法;

? YAHOO.lang —— 用于底層語(yǔ)言特性的方法。

對(duì)于 YUI,單一的全局對(duì)象 YAHOO 作為一個(gè)容器,其中定義了其他對(duì)象。用這種方式將功能組合

在一起的對(duì)象,叫做命名空間。整個(gè) YUI 庫(kù)便是構(gòu)建在這個(gè)概念上的,讓它能夠在同一個(gè)頁(yè)面上與其他

的 JavaScript 庫(kù)共存。

命名空間很重要的一部分是確定每個(gè)人都同意使用的全局對(duì)象的名字,并且盡可能唯一,讓其他人

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第682頁(yè)

664 第 24 章 最佳實(shí)踐

不太可能也使用這個(gè)名字。在大多數(shù)情況下,可以是開發(fā)代碼的公司的名字,例如 YAHOO 或者 Wrox。

你可以如下例所示開始創(chuàng)建命名空間來(lái)組合功能。

//創(chuàng)建全局對(duì)象

var Wrox = {};

//為 Professional JavaScript 創(chuàng)建命名空間

Wrox.ProJS = {};

//將書中用到的對(duì)象附加上去

Wrox.ProJS.EventUtil = { ... };

Wrox.ProJS.CookieUtil = { ... };

在這個(gè)例子中,Wrox是全局量,其他命名空間在此之上創(chuàng)建。如果本書所有代碼都放在Wrox.ProJS

命名空間,那么其他作者也應(yīng)把自己的代碼添加到 Wrox 對(duì)象中。只要所有人都遵循這個(gè)規(guī)則,那么就

不用擔(dān)心其他人也創(chuàng)建叫做 EventUtil 或者 CookieUtil 的對(duì)象,因?yàn)樗鼤?huì)存在于不同的命名空間中。

請(qǐng)看以下例子:

//為 Professional Ajax 創(chuàng)建命名空間

Wrox.ProAjax = {};

//附加該書中所使用的其他對(duì)象

Wrox.ProAjax.EventUtil = { ... };

Wrox.ProAjax.CookieUtil = { ... };

//ProJS 還可以繼續(xù)分別訪問(wèn)

Wrox.ProJS.EventUtil.addHandler( ... );

//以及 ProAjax

Wrox.ProAjax.EventUtil.addHandler( ... );

雖然命名空間會(huì)需要多寫一些代碼,但是對(duì)于可維護(hù)的目的而言是值得的。命名空間有助于確保代

碼可以在同一個(gè)頁(yè)面上與其他代碼以無(wú)害的方式一起工作。

3.避免與 null 進(jìn)行比較

由于 JavaScript 不做任何自動(dòng)的類型檢查,所有它就成了開發(fā)人員的責(zé)任。因此,在 JavaScript 代碼

中其實(shí)很少進(jìn)行類型檢測(cè)。最常見的類型檢測(cè)就是查看某個(gè)值是否為 null。但是,直接將值與 null

比較是使用過(guò)度的,并且常常由于不充分的類型檢查導(dǎo)致錯(cuò)誤??匆韵吕樱?/p>

function sortArray(values){

if (values != null){ //避免!

values.sort(comparator);

}

}

該函數(shù)的目的是根據(jù)給定的比較子對(duì)一個(gè)數(shù)組進(jìn)行排序。為了函數(shù)能正確執(zhí)行,values 參數(shù)必需

是數(shù)組,但這里的 if 語(yǔ)句僅僅檢查該 values 是否為 null。還有其他的值可以通過(guò) if 語(yǔ)句,包括字

符串、數(shù)字,它們會(huì)導(dǎo)致函數(shù)拋出錯(cuò)誤。

現(xiàn)實(shí)中,與 null 比較很少適合情況而被使用。必須按照所期望的對(duì)值進(jìn)行檢查,而非按照不被期

望的那些。例如,在前面的范例中,values 參數(shù)應(yīng)該是一個(gè)數(shù)組,那么就要檢查它是不是一個(gè)數(shù)組,

而不是檢查它是否非 null。函數(shù)按照下面的方式修改會(huì)更加合適:

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第683頁(yè)

24.1 可維護(hù)性 665

14

2

3

17

18

13

19

7

8

22

10

24

12

function sortArray(values){

if (values instanceof Array){ //推薦

values.sort(comparator);

}

}

該函數(shù)的這個(gè)版本可以阻止所有非法值,而且完全用不著 null。

這種驗(yàn)證數(shù)組的技術(shù)在多框架的網(wǎng)頁(yè)中不一定正確工作,因?yàn)槊總€(gè)框架都有其自

己的全局對(duì)象,因此,也有自己的 Array 構(gòu)造函數(shù)。如果你是從一個(gè)框架將數(shù)組傳

送到另一個(gè)框架,那么就要另外檢查是否存在 sort()方法。

如果看到了與 null 比較的代碼,嘗試使用以下技術(shù)替換:

? 如果值應(yīng)為一個(gè)引用類型,使用 instanceof 操作符檢查其構(gòu)造函數(shù);

? 如果值應(yīng)為一個(gè)基本類型,使用 typeof 檢查其類型;

? 如果是希望對(duì)象包含某個(gè)特定的方法名,則使用 typeof 操作符確保指定名字的方法存在于對(duì)

象上。

代碼中的 null 比較越少,就越容易確定代碼的目的,并消除不必要的錯(cuò)誤。

4. 使用常量

盡管 JavaScript 沒有常量的正式概念,但它還是很有用的。這種將數(shù)據(jù)從應(yīng)用邏輯分離出來(lái)的思想,

可以在不冒引入錯(cuò)誤的風(fēng)險(xiǎn)的同時(shí),就改變數(shù)據(jù)。請(qǐng)看以下例子:

function validate(value){

if (!value){

alert(\"Invalid value!\");

location.href = \"/errors/invalid.php\";

}

}

在這個(gè)函數(shù)中有兩段數(shù)據(jù):要顯示給用戶的信息以及 URL。顯示在用戶界面上的字符串應(yīng)該以允許

進(jìn)行語(yǔ)言國(guó)際化的方式抽取出來(lái)。URL 也應(yīng)被抽取出來(lái),因?yàn)樗鼈冇须S著應(yīng)用成長(zhǎng)而改變的傾向。基本

上,有著可能由于這樣那樣原因會(huì)變化的這些數(shù)據(jù),那么都會(huì)需要找到函數(shù)并在其中修改代碼 。而每次

修改應(yīng)用邏輯的代碼,都可能會(huì)引入錯(cuò)誤??梢酝ㄟ^(guò)將數(shù)據(jù)抽取出來(lái)變成單獨(dú)定義的常量的方式,將應(yīng)

用邏輯與數(shù)據(jù)修改隔離開來(lái)。請(qǐng)看以下例子:

var Constants = {

INVALID_VALUE_MSG: \"Invalid value!\",

INVALID_VALUE_URL: \"/errors/invalid.php\"

};

function validate(value){

if (!value){

alert(Constants.INVALID_VALUE_MSG);

location.href = Constants.INVALID_VALUE_URL;

}

}

在這段重寫過(guò)的代碼中,消息和 URL 都被定義于 Constants 對(duì)象中,然后函數(shù)引用這些值。這些

設(shè)置允許數(shù)據(jù)在無(wú)須接觸使用它的函數(shù)的情況下進(jìn)行變更。Constants 對(duì)象甚至可以完全在單獨(dú)的文

24.1 可維護(hù)性

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第684頁(yè)

666 第 24 章 最佳實(shí)踐

件中進(jìn)行定義,同時(shí)該文件可以由包含正確值的其他過(guò)程根據(jù)國(guó)際化設(shè)置來(lái)生成。

關(guān)鍵在于將數(shù)據(jù)和使用它的邏輯進(jìn)行分離。要注意的值的類型如下所示。

? 重復(fù)值——任何在多處用到的值都應(yīng)抽取為一個(gè)常量。這就限制了當(dāng)一個(gè)值變了而另一個(gè)沒變

的時(shí)候會(huì)造成的錯(cuò)誤。這也包含了 CSS 類名。

? 用戶界面字符串 —— 任何用于顯示給用戶的字符串,都應(yīng)被抽取出來(lái)以方便國(guó)際化。

? URLs ——在 Web 應(yīng)用中,資源位置很容易變更,所以推薦用一個(gè)公共地方存放所有的 URL。

? 任意可能會(huì)更改的值 —— 每當(dāng)你在用到字面量值的時(shí)候,你都要問(wèn)一下自己這個(gè)值在未來(lái)是不

是會(huì)變化。如果答案是“是”,那么這個(gè)值就應(yīng)該被提取出來(lái)作為一個(gè)常量。

對(duì)于企業(yè)級(jí)的 JavaScript 開發(fā)而言,使用常量是非常重要的技巧,因?yàn)樗茏尨a更容易維護(hù),并

且在數(shù)據(jù)更改的同時(shí)保護(hù)代碼。

24.2 性能

自從 JavaScript 誕生以來(lái),用這門語(yǔ)言編寫網(wǎng)頁(yè)的開發(fā)人員有了極大的增長(zhǎng)。與此同時(shí),JavaScript

代碼的執(zhí)行效率也越來(lái)越受到關(guān)注。因?yàn)?JavaScript 最初是一個(gè)解釋型語(yǔ)言,執(zhí)行速度要比編譯型語(yǔ)言

慢得多。Chrome 是第一款內(nèi)置優(yōu)化引擎,將 JavaScript 編譯成本地代碼的瀏覽器。此后,主流瀏覽器紛

紛效仿,陸續(xù)實(shí)現(xiàn)了 JavaScript 的編譯執(zhí)行。

即使到了編譯執(zhí)行 JavaScript 的新階段,仍然會(huì)存在低效率的代碼。不過(guò),還是有一些方式可以改

進(jìn)代碼的整體性能的。

24.2.1 注意作用域

第 4 章討論了 JavaScript 中“作用域”的概念以及作用域鏈?zhǔn)侨绾芜\(yùn)作的。隨著作用域鏈中的作用

域數(shù)量的增加,訪問(wèn)當(dāng)前作用域以外的變量的時(shí)間也在增加。訪問(wèn)全局變量總是要比訪問(wèn)局部變量慢,

因?yàn)樾枰闅v作用域鏈。只要能減少花費(fèi)在作用域鏈上的時(shí)間,就能增加腳本的整體性能。

1.避免全局查找

可能優(yōu)化腳本性能最重要的就是注意全局查找。使用全局變量和函數(shù)肯定要比局部的開銷更大,因

為要涉及作用域鏈上的查找。請(qǐng)看以下函數(shù):

function updateUI(){

var imgs = document.getElementsByTagName(\"img\");

for (var i=0, len=imgs.length; i < len; i++){

imgs[i].title = document.title + \" image \" + i;

}

var msg = document.getElementById(\"msg\");

msg.innerHTML = \"Update complete.\";

}

該函數(shù)可能看上去完全正常,但是它包含了三個(gè)對(duì)于全局 document 對(duì)象的引用。如果在頁(yè)面上有

多個(gè)圖片,那么 for 循環(huán)中的 document 引用就會(huì)被執(zhí)行多次甚至上百次,每次都會(huì)要進(jìn)行作用域鏈

查找。通過(guò)創(chuàng)建一個(gè)指向 document 對(duì)象的局部變量,就可以通過(guò)限制一次全局查找來(lái)改進(jìn)這個(gè)函數(shù)的

性能:

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第685頁(yè)

24.2 性能 667

14

2

3

17

18

13

19

7

8

22

10

24

12

function updateUI(){

var doc = document;

var imgs = doc.getElementsByTagName(\"img\");

for (var i=0, len=imgs.length; i < len; i++){

imgs[i].title = doc.title + \" image \" + i;

}

var msg = doc.getElementById(\"msg\");

msg.innerHTML = \"Update complete.\";

}

這里,首先將 document 對(duì)象存在本地的 doc 變量中;然后在余下的代碼中替換原來(lái)的 document。

與原來(lái)的的版本相比,現(xiàn)在的函數(shù)只有一次全局查找,肯定更快。

將在一個(gè)函數(shù)中會(huì)用到多次的全局對(duì)象存儲(chǔ)為局部變量總是沒錯(cuò)的。

2. 避免 with 語(yǔ)句

在性能非常重要的地方必須避免使用 with 語(yǔ)句。和函數(shù)類似,with 語(yǔ)句會(huì)創(chuàng)建自己的作用域,

因此會(huì)增加其中執(zhí)行的代碼的作用域鏈的長(zhǎng)度。由于額外的作用域鏈查找,在 with 語(yǔ)句中執(zhí)行的代碼

肯定會(huì)比外面執(zhí)行的代碼要慢。

必須使用 with 語(yǔ)句的情況很少,因?yàn)樗饕糜谙~外的字符。在大多數(shù)情況下,可以用局部

變量完成相同的事情而不引入新的作用域。下面是一個(gè)例子:

function updateBody(){

with(document.body){

alert(tagName);

innerHTML = \"Hello world!\";

}

}

這段代碼中的 with 語(yǔ)句讓 document.body 變得更容易使用。其實(shí)可以使用局部變量達(dá)到相同的

效果,如下所示:

function updateBody(){

var body = document.body

alert(body.tagName);

body.innerHTML = \"Hello world!\";

}

雖然代碼稍微長(zhǎng)了點(diǎn),但是閱讀起來(lái)比 with 語(yǔ)句版本更好,它確保讓你知道 tagName 和

innerHTML 是屬于哪個(gè)對(duì)象的。同時(shí),這段代碼通過(guò)將 document.body 存儲(chǔ)在局部變量中省去了額

外的全局查找。

24.2.2 選擇正確方法

和其他語(yǔ)言一樣,性能問(wèn)題的一部分是和用于解決問(wèn)題的算法或者方法有關(guān)的。老練的開發(fā)人員根

據(jù)經(jīng)驗(yàn)可以得知哪種方法可能獲得更好的性能。很多應(yīng)用在其他編程語(yǔ)言中的技術(shù)和方法也可以在

JavaScript 中使用。

1. 避免不必要的屬性查找

在計(jì)算機(jī)科學(xué)中,算法的復(fù)雜度是使用 O 符號(hào)來(lái)表示的。最簡(jiǎn)單、最快捷的算法是常數(shù)值即 O(1)。

之后,算法變得越來(lái)越復(fù)雜并花更長(zhǎng)時(shí)間執(zhí)行。下面的表格列出了 JavaScript 中常見的算法類型。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第686頁(yè)

668 第 24 章 最佳實(shí)踐

標(biāo) 記 名 稱 描 述

O(1) 常數(shù) 不管有多少值,執(zhí)行的時(shí)間都是恒定的。一般表示簡(jiǎn)單值和存儲(chǔ)在變量中的值

O(log n) 對(duì)數(shù) 總的執(zhí)行時(shí)間和值的數(shù)量相關(guān),但是要完成算法并不一定要獲取每個(gè)值。例如:二分查找

O(n) 線性 總執(zhí)行時(shí)間和值的數(shù)量直接相關(guān)。例如:遍歷某個(gè)數(shù)組中的所有元素

O(n2

) 平方 總執(zhí)行時(shí)間和值的數(shù)量有關(guān),每個(gè)值至少要獲取n次。例如:插入排序

常數(shù)值,即 O(1),指代字面值和存儲(chǔ)在變量中的值。符號(hào) O(1)表示無(wú)論有多少個(gè)值,需要獲取常

量值的時(shí)間都一樣。獲取常量值是非常高效的過(guò)程。請(qǐng)看下面代碼:

var value = 5;

var sum = 10 + value;

alert(sum);

該代碼進(jìn)行了四次常量值查找:數(shù)字 5,變量 value,數(shù)字 10 和變量 sum。這段代碼的整體復(fù)雜

度被認(rèn)為是 O(1)。

在 JavaScript 中訪問(wèn)數(shù)組元素也是一個(gè) O(1)操作,和簡(jiǎn)單的變量查找效率一樣。所以以下代碼和前

面的例子效率一樣:

var values = [5, 10];

var sum = values[0] + values[1];

alert(sum);

使用變量和數(shù)組要比訪問(wèn)對(duì)象上的屬性更有效率,后者是一個(gè) O(n)操作。對(duì)象上的任何屬性查找都

要比訪問(wèn)變量或者數(shù)組花費(fèi)更長(zhǎng)時(shí)間,因?yàn)楸仨氃谠玩溨袑?duì)擁有該名稱的屬性進(jìn)行一次搜索。簡(jiǎn)而言

之,屬性查找越多,執(zhí)行時(shí)間就越長(zhǎng)。請(qǐng)看以下內(nèi)容:

var values = { first: 5, second: 10};

var sum = values.first + values.second;

alert(sum);

這段代碼使用兩次屬性查找來(lái)計(jì)算 sum 的值。進(jìn)行一兩次屬性查找并不會(huì)導(dǎo)致顯著的性能問(wèn)題,但

是進(jìn)行成百上千次則肯定會(huì)減慢執(zhí)行速度。

注意獲取單個(gè)值的多重屬性查找。例如,請(qǐng)看以下代碼:

var query = window.location.href.substring(window.location.href.indexOf(\"?\"));

在這段代碼中,有 6 次屬性查找:window.location.href.substring()有 3 次,window.

location.href.indexOf()又有 3 次。只要數(shù)一數(shù)代碼中的點(diǎn)的數(shù)量,就可以確定屬性查找的次數(shù)了。

這段代碼由于兩次用到了 window.location.href,同樣的查找進(jìn)行了兩次,因此效率特別不好。

一旦多次用到對(duì)象屬性,應(yīng)該將其存儲(chǔ)在局部變量中。第一次訪問(wèn)該值會(huì)是 O(n),然而后續(xù)的訪問(wèn)

都會(huì)是 O(1),就會(huì)節(jié)省很多。例如,之前的代碼可以如下重寫:

var url = window.location.href;

var query = url.substring(url.indexOf(\"?\"));

這個(gè)版本的代碼只有 4 次屬性查找,相對(duì)于原始版本節(jié)省了 33%。在更大的腳本中進(jìn)行這種優(yōu)化,

傾向于獲得更多改進(jìn)。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第687頁(yè)

24.2 性能 669

14

2

3

17

18

13

19

7

8

22

10

24

12

一般來(lái)講,只要能減少算法的復(fù)雜度,就要盡可能減少。盡可能多地使用局部變量將屬性查找替換

為值查找。進(jìn)一步講,如果即可以用數(shù)字化的數(shù)組位置進(jìn)行訪問(wèn),也可以使用命名屬性(諸如 NodeList

對(duì)象),那么使用數(shù)字位置。

2. 優(yōu)化循環(huán)

循環(huán)是編程中最常見的結(jié)構(gòu),在 JavaScript 程序中同樣隨處可見。優(yōu)化循環(huán)是性能優(yōu)化過(guò)程中很重

要的一個(gè)部分,由于它們會(huì)反復(fù)運(yùn)行同一段代碼,從而自動(dòng)地增加執(zhí)行時(shí)間。在其他語(yǔ)言中對(duì)于循環(huán)優(yōu)

化有大量研究,這些技術(shù)也可以應(yīng)用于 JavaScript。一個(gè)循環(huán)的基本優(yōu)化步驟如下所示。

(1) 減值迭代——大多數(shù)循環(huán)使用一個(gè)從 0 開始、增加到某個(gè)特定值的迭代器。在很多情況下,從

最大值開始,在循環(huán)中不斷減值的迭代器更加高效。

(2) 簡(jiǎn)化終止條件——由于每次循環(huán)過(guò)程都會(huì)計(jì)算終止條件,所以必須保證它盡可能快。也就是說(shuō)

避免屬性查找或其他 O(n)的操作。

(3) 簡(jiǎn)化循環(huán)體——循環(huán)體是執(zhí)行最多的,所以要確保其被最大限度地優(yōu)化。確保沒有某些可以被

很容易移出循環(huán)的密集計(jì)算。

(4) 使用后測(cè)試循環(huán)——最常用 for 循環(huán)和 while 循環(huán)都是前測(cè)試循環(huán)。而如 do-while 這種后測(cè)

試循環(huán),可以避免最初終止條件的計(jì)算,因此運(yùn)行更快。

用一個(gè)例子來(lái)描述這種改動(dòng)。以下是一個(gè)基本的 for 循環(huán):

for (var i=0; i < values.length; i++){

process(values[i]);

}

這段代碼中變量 i 從 0 遞增到 values 數(shù)組中的元素總數(shù)。假設(shè)值的處理順序無(wú)關(guān)緊要,那么循環(huán)

可以改為 i 減值,如下所示:

for (var i=values.length -1; i >= 0; i--){

process(values[i]);

}

這里,變量 i 每次循環(huán)之后都會(huì)減 1。在這個(gè)過(guò)程中,將終止條件從 value.length 的 O(n)調(diào)用

簡(jiǎn)化成了 0 的 O(1)調(diào)用。由于循環(huán)體只有一個(gè)語(yǔ)句,無(wú)法進(jìn)一步優(yōu)化。不過(guò)循環(huán)還能改成后測(cè)試循環(huán),

如下:

var i=values.length -1;

if (i > -1){

do {

process(values[i]);

}while(--i >= 0);

}

此處主要的優(yōu)化是將終止條件和自減操作符組合成了單個(gè)語(yǔ)句。這時(shí),任何進(jìn)一步的優(yōu)化只能在

process()函數(shù)中進(jìn)行了,因?yàn)檠h(huán)部分已經(jīng)優(yōu)化完全了。

記住使用“后測(cè)試”循環(huán)時(shí)必須確保要處理的值至少有一個(gè)??諗?shù)組會(huì)導(dǎo)致多余的一次循環(huán)而“前

測(cè)試”循環(huán)則可以避免。

3. 展開循環(huán)

當(dāng)循環(huán)的次數(shù)是確定的,消除循環(huán)并使用多次函數(shù)調(diào)用往往更快 。請(qǐng)看一下前面的例子。如果數(shù)組

的長(zhǎng)度總是一樣的,對(duì)每個(gè)元素都調(diào)用 process()可能更優(yōu),如以下代碼所示:

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第688頁(yè)

670 第 24 章 最佳實(shí)踐

//消除循環(huán)

process(values[0]);

process(values[1]);

process(values[2]);

這個(gè)例子假設(shè) values 數(shù)組里面只有 3 個(gè)元素,直接對(duì)每個(gè)元素調(diào)用 process()。這樣展開循環(huán)

可以消除建立循環(huán)和處理終止條件的額外開銷,使代碼運(yùn)行得更快。

如果循環(huán)中的迭代次數(shù)不能事先確定,那可以考慮使用一種叫做 Duff 裝置的技術(shù)。這個(gè)技術(shù)是以

其創(chuàng)建者 Tom Duff 命名的,他最早在 C 語(yǔ)言中使用這項(xiàng)技術(shù)。正是 Jeff Greenberg 用 JavaScript 實(shí)現(xiàn)了

Duff 裝置。Duff 裝置的基本概念是通過(guò)計(jì)算迭代的次數(shù)是否為 8 的倍數(shù)將一個(gè)循環(huán)展開為一系列語(yǔ)句。

請(qǐng)看以下代碼:

//credit: Jeff Greenberg for JS implementation of Duff’s Device

//假設(shè) values.length > 0

var iterations = Math.ceil(values.length / 8);

var startAt = values.length % 8;

var i = 0;

do {

switch(startAt){

case 0: process(values[i++]);

case 7: process(values[i++]);

case 6: process(values[i++]);

case 5: process(values[i++]);

case 4: process(values[i++]);

case 3: process(values[i++]);

case 2: process(values[i++]);

case 1: process(values[i++]);

}

startAt = 0;

} while (--iterations > 0);

Duff 裝置的實(shí)現(xiàn)是通過(guò)將 values 數(shù)組中元素個(gè)數(shù)除以 8 來(lái)計(jì)算出循環(huán)需要進(jìn)行多少次迭代的。然

后使用取整的上限函數(shù)確保結(jié)果是整數(shù)。如果完全根據(jù)除 8 來(lái)進(jìn)行迭代,可能會(huì)有一些不能被處理到的

元素,這個(gè)數(shù)量保存在 startAt 變量中。首次執(zhí)行該循環(huán)時(shí),會(huì)檢查 StartAt 變量看有需要多少額外

調(diào)用。例如,如果數(shù)組中有 10 個(gè)值,startAt 則等于 2,那么最開始的時(shí)候 process()則只會(huì)被調(diào)用

2 次。在接下來(lái)的循環(huán)中,startAt 被重置為 0,這樣之后的每次循環(huán)都會(huì)調(diào)用 8 次 process()。展開

循環(huán)可以提升大數(shù)據(jù)集的處理速度。

由 Andrew B. King 所著的 Speed Up Your Site(New Riders,2003)提出了一個(gè)更快的 Duff 裝置技術(shù),

將 do-while 循環(huán)分成 2 個(gè)單獨(dú)的循環(huán)。以下是例子:

//credit: Speed Up Your Site (New Riders, 2003)

var iterations = Math.floor(values.length / 8);

var leftover = values.length % 8;

var i = 0;

if (leftover > 0){

do {

process(values[i++]);

} while (--leftover > 0);

}

do {

process(values[i++]);

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第689頁(yè)

24.2 性能 671

14

2

3

17

18

13

19

7

8

22

10

24

12

process(values[i++]);

process(values[i++]);

process(values[i++]);

process(values[i++]);

process(values[i++]);

process(values[i++]);

process(values[i++]);

} while (--iterations > 0);

在這個(gè)實(shí)現(xiàn)中,剩余的計(jì)算部分不會(huì)在實(shí)際循環(huán)中處理,而是在一個(gè)初始化循環(huán)中進(jìn)行除以 8 的操

作。當(dāng)處理掉了額外的元素,繼續(xù)執(zhí)行每次調(diào)用 8 次 process()的主循環(huán)。這個(gè)方法幾乎比原始的 Duff

裝置實(shí)現(xiàn)快上 40%。

針對(duì)大數(shù)據(jù)集使用展開循環(huán)可以節(jié)省很多時(shí)間,但對(duì)于小數(shù)據(jù)集,額外的開銷則可能得不償失。它

是要花更多的代碼來(lái)完成同樣的任務(wù),如果處理的不是大數(shù)據(jù)集,一般來(lái)說(shuō)并不值得。

4. 避免雙重解釋

當(dāng) JavaScript 代碼想解析 JavaScript 的時(shí)候就會(huì)存在雙重解釋懲罰。當(dāng)使用 eval()函數(shù)或者是

Function 構(gòu)造函數(shù)以及使用 setTimeout()傳一個(gè)字符串參數(shù)時(shí)都會(huì)發(fā)生這種情況。下面有一些例子:

//某些代碼求值——避免!!

eval(\"alert('Hello world!')\");

//創(chuàng)建新函數(shù)——避免!!

var sayHi = new Function(\"alert('Hello world!')\");

//設(shè)置超時(shí)——避免!!

setTimeout(\"alert('Hello world!')\", 500);

在以上這些例子中,都要解析包含了 JavaScript 代碼的字符串。這個(gè)操作是不能在初始的解析過(guò)程

中完成的,因?yàn)榇a是包含在字符串中的,也就是說(shuō)在 JavaScript 代碼運(yùn)行的同時(shí)必須新啟動(dòng)一個(gè)解

析器來(lái)解析新的代碼。實(shí)例化一個(gè)新的解析器有不容忽視的開銷,所以這種代碼要比直接解析慢得多。

對(duì)于這幾個(gè)例子都有另外的辦法。只有極少的情況下 eval()是絕對(duì)必須的,所以盡可能避免使用。

在這個(gè)例子中,代碼其實(shí)可以直接內(nèi)嵌在原代碼中。對(duì)于 Function 構(gòu)造函數(shù),完全可以直接寫成一般

的函數(shù),調(diào)用 setTimeout()可以傳入函數(shù)作為第一個(gè)參數(shù)。以下是一些例子:

//已修正

alert('Hello world!');

//創(chuàng)建新函數(shù)——已修正

var sayHi = function(){

alert('Hello world!');

};

//設(shè)置一個(gè)超時(shí)——已修正

setTimeout(function(){

alert('Hello world!');

}, 500);

如果要提高代碼性能,盡可能避免出現(xiàn)需要按照 JavaScript 解釋的字符串。

5. 性能的其他注意事項(xiàng)

當(dāng)評(píng)估腳本性能的時(shí)候,還有其他一些可以考慮的東西。下面并非主要的問(wèn)題,不過(guò)如果使用得當(dāng)

也會(huì)有相當(dāng)大的提升。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第690頁(yè)

672 第 24 章 最佳實(shí)踐

? 原生方法較快——只要有可能,使用原生方法而不是自己用 JavaScript 重寫一個(gè)。原生方法是用

諸如 C/C++之類的編譯型語(yǔ)言寫出來(lái)的,所以要比 JavaScript 的快很多很多。JavaScript 中最容

易被忘記的就是可以在 Math 對(duì)象中找到的復(fù)雜的數(shù)學(xué)運(yùn)算;這些方法要比任何用 JavaScript 寫

的同樣方法如正弦、余弦快的多。

? Switch 語(yǔ)句較快 —— 如果有一系列復(fù)雜的 if-else 語(yǔ)句,可以轉(zhuǎn)換成單個(gè) switch 語(yǔ)句則可

以得到更快的代碼。還可以通過(guò)將 case 語(yǔ)句按照最可能的到最不可能的順序進(jìn)行組織,來(lái)進(jìn)一

步優(yōu)化 switch 語(yǔ)句。

? 位運(yùn)算符較快 —— 當(dāng)進(jìn)行數(shù)學(xué)運(yùn)算的時(shí)候,位運(yùn)算操作要比任何布爾運(yùn)算或者算數(shù)運(yùn)算快。選

擇性地用位運(yùn)算替換算數(shù)運(yùn)算可以極大提升復(fù)雜計(jì)算的性能。諸如取模,邏輯與和邏輯或都可

以考慮用位運(yùn)算來(lái)替換。

24.2.3 最小化語(yǔ)句數(shù)

JavaScript 代碼中的語(yǔ)句數(shù)量也影響所執(zhí)行的操作的速度。完成多個(gè)操作的單個(gè)語(yǔ)句要比完成單個(gè)

操作的多個(gè)語(yǔ)句快。所以,就要找出可以組合在一起的語(yǔ)句,以減少腳本整體的執(zhí)行時(shí)間。這里有幾個(gè)

可以參考的模式。

1. 多個(gè)變量聲明

有個(gè)地方很多開發(fā)人員都容易創(chuàng)建很多語(yǔ)句,那就是多個(gè)變量的聲明。很容易看到代碼中由多個(gè)

var 語(yǔ)句來(lái)聲明多個(gè)變量,如下所示:

//4 個(gè)語(yǔ)句——很浪費(fèi)

var count = 5;

var color = \"blue\";

var values = [1,2,3];

var now = new Date();

在強(qiáng)類型語(yǔ)言中,不同的數(shù)據(jù)類型的變量必須在不同的語(yǔ)句中聲明。然而,在 JavaScript 中所有的

變量都可以使用單個(gè) var 語(yǔ)句來(lái)聲明。前面的代碼可以如下重寫:

//一個(gè)語(yǔ)句

var count = 5,

color = \"blue\",

values = [1,2,3],

now = new Date();

此處,變量聲明只用了一個(gè) var 語(yǔ)句,之間由逗號(hào)隔開。在大多數(shù)情況下這種優(yōu)化都非常容易做,

并且要比單個(gè)變量分別聲明快很多。

2. 插入迭代值

當(dāng)使用迭代值(也就是在不同的位置進(jìn)行增加或減少的值)的時(shí)候,盡可能合并語(yǔ)句。請(qǐng)看以下

代碼:

var name = values[i];

i++;

前面這 2 句語(yǔ)句各只有一個(gè)目的:第一個(gè)從 values 數(shù)組中獲取值,然后存儲(chǔ)在 name 中;第二個(gè)

給變量 i 增加 1。這兩句可以通過(guò)迭代值插入第一個(gè)語(yǔ)句組合成一個(gè)語(yǔ)句,如下所示:

var name = values[i++];

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第691頁(yè)

24.2 性能 673

14

2

3

17

18

13

19

7

8

22

10

24

12

這一個(gè)語(yǔ)句可以完成和前面兩個(gè)語(yǔ)句一樣的事情。因?yàn)樽栽霾僮鞣呛缶Y操作符,i 的值只有在語(yǔ)

句其他部分結(jié)束之后才會(huì)增加。一旦出現(xiàn)類似情況,都要嘗試將迭代值插入到最后使用它的語(yǔ)句中去。

3. 使用數(shù)組和對(duì)象字面量

本書中,你可能看過(guò)兩種創(chuàng)建數(shù)組和對(duì)象的方法:使用構(gòu)造函數(shù)或者是使用字面量。使用構(gòu)造函數(shù)

總是要用到更多的語(yǔ)句來(lái)插入元素或者定義屬性,而字面量可以將這些操作在一個(gè)語(yǔ)句中完成。請(qǐng)看以

下例子:

//用 4 個(gè)語(yǔ)句創(chuàng)建和初始化數(shù)組——浪費(fèi)

var values = new Array();

values[0] = 123;

values[1] = 456;

values[2] = 789;

//用 4 個(gè)語(yǔ)句創(chuàng)建和初始化對(duì)象——浪費(fèi)

var person = new Object();

person.name = \"Nicholas\";

person.age = 29;

person.sayName = function(){

alert(this.name);

};

這段代碼中,只創(chuàng)建和初始化了一個(gè)數(shù)組和一個(gè)對(duì)象。各用了 4 個(gè)語(yǔ)句:一個(gè)調(diào)用構(gòu)造函數(shù),其他

3 個(gè)分配數(shù)據(jù)。其實(shí)可以很容易地轉(zhuǎn)換成使用字面量的形式,如下所示:

//只用一條語(yǔ)句創(chuàng)建和初始化數(shù)組

var values = [123, 456, 789];

//只用一條語(yǔ)句創(chuàng)建和初始化對(duì)象

var person = {

name : \"Nicholas\",

age : 29,

sayName : function(){

alert(this.name);

}

};

重寫后的代碼只包含兩條語(yǔ)句,一條創(chuàng)建和初始化數(shù)組,另一條創(chuàng)建和初始化對(duì)象。之前用了八條

語(yǔ)句的東西現(xiàn)在只用了兩條,減少了 75%的語(yǔ)句量。在包含成千上萬(wàn)行 JavaScript 的代碼庫(kù)中,這些優(yōu)

化的價(jià)值更大。

只要有可能,盡量使用數(shù)組和對(duì)象的字面量表達(dá)方式來(lái)消除不必要的語(yǔ)句。

在 IE6 和更早版本中使用字面量有微小的性能懲罰。不過(guò)這些問(wèn)題在 IE7 中已經(jīng)

解決。

24.2.4 優(yōu)化DOM交互

在 JavaScript 各個(gè)方面中,DOM 毫無(wú)疑問(wèn)是最慢的一部分。DOM 操作與交互要消耗大量時(shí)間,

因?yàn)樗鼈兺枰匦落秩菊麄€(gè)頁(yè)面或者某一部分。進(jìn)一步說(shuō),看似細(xì)微的操作也可能要花很久來(lái)執(zhí)

行,因?yàn)?DOM 要處理非常多的信息。理解如何優(yōu)化與 DOM 的交互可以極大得提高腳本完成的速度。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第692頁(yè)

674 第 24 章 最佳實(shí)踐

1. 最小化現(xiàn)場(chǎng)更新

一旦你需要訪問(wèn)的 DOM 部分是已經(jīng)顯示的頁(yè)面的一部分,那么你就是在進(jìn)行一個(gè)現(xiàn)場(chǎng)更新。之所

以叫現(xiàn)場(chǎng)更新,是因?yàn)樾枰⒓矗ìF(xiàn)場(chǎng))對(duì)頁(yè)面對(duì)用戶的顯示進(jìn)行更新。每一個(gè)更改,不管是插入單個(gè)

字符,還是移除整個(gè)片段,都有一個(gè)性能懲罰,因?yàn)闉g覽器要重新計(jì)算無(wú)數(shù)尺寸以進(jìn)行更新?,F(xiàn)場(chǎng)更新

進(jìn)行得越多,代碼完成執(zhí)行所花的時(shí)間就越長(zhǎng);完成一個(gè)操作所需的現(xiàn)場(chǎng)更新越少,代碼就越快。請(qǐng)看

以下例子:

var list = document.getElementById(\"myList\"),

item,

i;

for (i=0; i < 10; i++) {

item = document.createElement(\"li\");

list.appendChild(item);

item.appendChild(document.createTextNode(\"Item \" + i));

}

這段代碼為列表添加了 10 個(gè)項(xiàng)目。添加每個(gè)項(xiàng)目時(shí),都有 2 個(gè)現(xiàn)場(chǎng)更新:一個(gè)添加<li>元素,另

一個(gè)給它添加文本節(jié)點(diǎn)。這樣添加 10 個(gè)項(xiàng)目,這個(gè)操作總共要完成 20 個(gè)現(xiàn)場(chǎng)更新。

要修正這個(gè)性能瓶頸,需要減少現(xiàn)場(chǎng)更新的數(shù)量。一般有 2 種方法。第一種是將列表從頁(yè)面上移除,

最后進(jìn)行更新,最后再將列表插回到同樣的位置。這個(gè)方法不是非常理想,因?yàn)樵诿看雾?yè)面更新的時(shí)候

它會(huì)不必要的閃爍。第二個(gè)方法是使用文檔片段來(lái)構(gòu)建 DOM 結(jié)構(gòu),接著將其添加到 List 元素中。這

個(gè)方式避免了現(xiàn)場(chǎng)更新和頁(yè)面閃爍問(wèn)題。請(qǐng)看下面內(nèi)容:

var list = document.getElementById(\"myList\"),

fragment = document.createDocumentFragment(),

item,

i;

for (i=0; i < 10; i++) {

item = document.createElement(\"li\");

fragment.appendChild(item);

item.appendChild(document.createTextNode(\"Item \" + i));

}

list.appendChild(fragment);

在這個(gè)例子中只有一次現(xiàn)場(chǎng)更新,它發(fā)生在所有項(xiàng)目都創(chuàng)建好之后。文檔片段用作一個(gè)臨時(shí)的占位

符,放置新創(chuàng)建的項(xiàng)目。然后使用 appendChild()將所有項(xiàng)目添加到列表中。記住,當(dāng)給 appendChild()

傳入文檔片段時(shí),只有片段中的子節(jié)點(diǎn)被添加到目標(biāo),片段本身不會(huì)被添加的。

一旦需要更新 DOM,請(qǐng)考慮使用文檔片段來(lái)構(gòu)建 DOM 結(jié)構(gòu),然后再將其添加到現(xiàn)存的文檔中。

2. 使用 innerHTML

有兩種在頁(yè)面上創(chuàng)建 DOM 節(jié)點(diǎn)的方法:使用諸如 createElement()和 appendChild()之類的

DOM 方法,以及使用 innerHTML。對(duì)于小的 DOM 更改而言,兩種方法效率都差不多。然而,對(duì)于大

的 DOM 更改,使用 innerHTML 要比使用標(biāo)準(zhǔn) DOM 方法創(chuàng)建同樣的 DOM 結(jié)構(gòu)快得多。

當(dāng)把 innerHTML 設(shè)置為某個(gè)值時(shí),后臺(tái)會(huì)創(chuàng)建一個(gè) HTML 解析器,然后使用內(nèi)部的 DOM 調(diào)用來(lái)

創(chuàng)建 DOM 結(jié)構(gòu),而非基于 JavaScript 的 DOM 調(diào)用。由于內(nèi)部方法是編譯好的而非解釋執(zhí)行的,所以執(zhí)

行快得多。前面的例子還可以用 innerHTML 改寫如下:

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第693頁(yè)

24.2 性能 675

14

2

3

17

18

13

19

7

8

22

10

24

12

var list = document.getElementById(\"myList\"),

html = \"\",

i;

for (i=0; i < 10; i++) {

html += \"<li>Item \" + i + \"</li>\";

}

list.innerHTML = html;

這段代碼構(gòu)建了一個(gè) HTML 字符串,然后將其指定到 list.innerHTML,便創(chuàng)建了需要的 DOM 結(jié)

構(gòu)。雖然字符串連接上總是有點(diǎn)性能損失,但這種方式還是要比進(jìn)行多個(gè) DOM 操作更快。

使用 innerHTML 的關(guān)鍵在于(和其他 DOM 操作一樣)最小化調(diào)用它的次數(shù)。例如,下面的代碼

在這個(gè)操作中用到 innerHTML 的次數(shù)太多了:

var list = document.getElementById(\"myList\"),

i;

for (i=0; i < 10; i++) {

list.innerHTML += \"<li>Item \" + i + \"</li>\"; //避免!!!

}

這段代碼的問(wèn)題在于每次循環(huán)都要調(diào)用 innerHTML,這是極其低效的。調(diào)用 innerHTML 實(shí)際上就

是一次現(xiàn)場(chǎng)更新,所以也要如此對(duì)待。構(gòu)建好一個(gè)字符串然后一次性調(diào)用 innerHTML 要比調(diào)用

innerHTML 多次快得多。

3. 使用事件代理

大多數(shù) Web 應(yīng)用在用戶交互上大量用到事件處理程序。頁(yè)面上的事件處理程序的數(shù)量和頁(yè)面響應(yīng)

用戶交互的速度之間有個(gè)負(fù)相關(guān)。為了減輕這種懲罰,最好使用事件代理。

事件代理,如第 13 章中所討論的那樣,用到了事件冒泡。任何可以冒泡的事件都不僅僅可以在事

件目標(biāo)上進(jìn)行處理,目標(biāo)的任何祖先節(jié)點(diǎn)上也能處理。使用這個(gè)知識(shí),就可以將事件處理程序附加到更

高層的地方負(fù)責(zé)多個(gè)目標(biāo)的事件處理。如果可能,在文檔級(jí)別附加事件處理程序,這樣可以處理整個(gè)頁(yè)

面的事件。

4. 注意 HTMLCollection

HTMLCollection 對(duì)象的陷阱已經(jīng)在本書中討論過(guò)了,因?yàn)樗鼈儗?duì)于 Web 應(yīng)用的性能而言是巨大

的損害。記住,任何時(shí)候要訪問(wèn) HTMLCollection,不管它是一個(gè)屬性還是一個(gè)方法,都是在文檔上進(jìn)

行一個(gè)查詢,這個(gè)查詢開銷很昂貴。最小化訪問(wèn) HTMLCollection 的次數(shù)可以極大地改進(jìn)腳本的性能。

也許優(yōu)化 HTMLCollection 訪問(wèn)最重要的地方就是循環(huán)了。前面提到過(guò)將長(zhǎng)度計(jì)算移入 for 循環(huán)

的初始化部分?,F(xiàn)在看一下這個(gè)例子:

var images = document.getElementsByTagName(\"img\"),

i, len;

for (i=0, len=images.length; i < len; i++){

//處理

}

這里的關(guān)鍵在于長(zhǎng)度 length 存入了 len 變量,而不是每次都去訪問(wèn) HTMLCollection 的 length

屬性。當(dāng)在循環(huán)中使用 HTMLCollection 的時(shí)候,下一步應(yīng)該是獲取要使用的項(xiàng)目的引用,如下所示,

以便避免在循環(huán)體內(nèi)多次調(diào)用 HTMLCollection。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第694頁(yè)

676 第 24 章 最佳實(shí)踐

var images = document.getElementsByTagName(\"img\"),

image,

i, len;

for (i=0, len=images.length; i < len; i++){

image = images[i];

//處理

}

這段代碼添加了 image 變量,保存了當(dāng)前的圖像。這之后,在循環(huán)內(nèi)就沒有理由再訪問(wèn) images 的

HTMLCollection 了 。

編寫 JavaScript 的時(shí)候,一定要知道何時(shí)返回 HTMLCollection 對(duì)象,這樣你就可以最小化對(duì)他們

的訪問(wèn)。發(fā)生以下情況時(shí)會(huì)返回 HTMLCollection 對(duì)象:

? 進(jìn)行了對(duì) getElementsByTagName() 的調(diào)用;

? 獲取了元素的 childNodes 屬性;

? 獲取了元素的 attributes 屬性;

? 訪問(wèn)了特殊的集合,如 document.forms、document.images 等。

要了解當(dāng)使用 HTMLCollection 對(duì)象時(shí),合理使用會(huì)極大提升代碼執(zhí)行速度。

24.3 部署

也許所有 JavaScript 解決方案最重要的部分,便是最后部署到運(yùn)營(yíng)中的網(wǎng)站或者是 Web 應(yīng)用的過(guò)程。

在這之前可能你已經(jīng)做了相當(dāng)多的工作,為普通的使用進(jìn)行架構(gòu)并優(yōu)化一個(gè)解決方案?,F(xiàn)在是時(shí)候從開

發(fā)環(huán)境中走出來(lái)并進(jìn)入 Web 階段了,在此將會(huì)和真正的用戶交互。然而,在這之前還有一系列需要解

決的問(wèn)題。

24.3.1 構(gòu)建過(guò)程

完備 JavaScript 代碼可以用于部署的一件很重要的事情,就是給它開發(fā)某些類型的構(gòu)建過(guò)程。軟件

開發(fā)的典型模式是寫代碼—編譯—測(cè)試,即首先書寫好代碼,將其編譯通過(guò),然后運(yùn)行并確保其正常工作。

由于 JavaScript 并非一個(gè)編譯型語(yǔ)言,模式變成了寫代碼—測(cè)試,這里你寫的代碼就是你要在瀏覽器中測(cè)

試的代碼。這個(gè)方法的問(wèn)題在于它不是最優(yōu)的,你寫的代碼不應(yīng)該原封不動(dòng)地放入瀏覽器中,理由如下

所示。

? 知識(shí)產(chǎn)權(quán)問(wèn)題 —— 如果把帶有完整注釋的代碼放到線上,那別人就更容易知道你的意圖,對(duì)它

再利用,并且可能找到安全漏洞。

? 文件大小 —— 書寫代碼要保證容易閱讀,才能更好地維護(hù),但是這對(duì)于性能是不利的。瀏覽器

并不能從額外的空白字符或者是冗長(zhǎng)的函數(shù)名和變量名中獲得什么好處。

? 代碼組織 —— 組織代碼要考慮到可維護(hù)性并不一定是傳送給瀏覽器的最好方式。

基于這些原因,最好給 JavaScript 文件定義一個(gè)構(gòu)建過(guò)程。

構(gòu)建過(guò)程始于在源控制中定義用于存儲(chǔ)文件的邏輯結(jié)構(gòu)。最好避免使用一個(gè)文件存放所有的

JavaScript,遵循以下面向?qū)ο笳Z(yǔ)言中的典型模式:將每個(gè)對(duì)象或自定義類型分別放入其單獨(dú)的文件中。

這樣可以確保每個(gè)文件包含最少量的代碼,使其在不引入錯(cuò)誤的情況下更容易修改。另外,在使用像

CVS 或 Subversion 這類并發(fā)源控制系統(tǒng)的時(shí)候,這樣做也減少了在合并操作中產(chǎn)生沖突的風(fēng)險(xiǎn)。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第695頁(yè)

24.3 部署 677

14

2

3

17

18

13

19

7

8

22

10

24

12

記住將代碼分離成多個(gè)文件只是為了提高可維護(hù)性,并非為了部署。要進(jìn)行部署的時(shí)候,需要將這

些源代碼合并為一個(gè)或幾個(gè)歸并文件。推薦 Web 應(yīng)用中盡可能使用最少的 JavaScript 文件,是因?yàn)?HTTP

請(qǐng)求是 Web 中的主要性能瓶頸之一。記住通過(guò)<script>標(biāo)記引用 JavaScript 文件是一個(gè)阻塞操作,當(dāng)

代碼下載并運(yùn)行的時(shí)候會(huì)停止其他所有的下載。因此,盡量從邏輯上將 JavaScript 代碼分組成部署文件。

一旦組織好文件和目錄結(jié)構(gòu),并確定哪些要出現(xiàn)在部署文件中,就可以創(chuàng)建構(gòu)建系統(tǒng)了。Ant 構(gòu)建

工具(http://ant.apache.org)是為了自動(dòng)化 Java 構(gòu)建過(guò)程而誕生的,不過(guò)因?yàn)槠湟子眯院蛻?yīng)用廣泛,而

在 Web 應(yīng)用開發(fā)人員中也頗流行,諸如 Julien Lecomte 的軟件工程師,已經(jīng)寫了教程指導(dǎo)如何使用 Ant

進(jìn)行 JavaScript 和 CSS 的構(gòu)建自動(dòng)化(Lecomte 的文章在 www.julienlecomte.net/blog/2007/09/16/ )。

Ant 由于其簡(jiǎn)便的文件處理能力而非常適合 JavaScript 編譯系統(tǒng)。例如,可以很方便地獲得目錄中

的所有文件的列表,然后將其合并為一個(gè)文件,如下所示:

<project name=\"JavaScript Project\" default=\"js.concatenate\">

<!-- 輸出的目錄 -->

<property name=\"build.dir\" value=\"./js\" />

<!-- 包含源文件的目錄 -->

<property name=\"src.dir\" value=\"./dev/src\" />

<!-- 合并所有 JS 文件的目標(biāo) -->

<!-- Credit: Julien Lecomte, http://www.julienlecomte.net/blog/2007/09/16/ -->

<target name=\"js.concatenate\">

<concat destfile=\"${build.dir}/output.js\">

<filelist dir=\"${src.dir}/js\" files=\"a.js, b.js\"/>

<fileset dir=\"${src.dir}/js\" includes=\"*.js\" excludes=\"a.js, b.js\"/>

</concat>

</target>

</project>

SampleAntDir/build.xml

該 build.xml 文件定義了兩個(gè)屬性:輸出最終文件的構(gòu)建目錄,以及 JavaScript 源文件所在的源目錄。

目標(biāo) js.concatenate 使用了<concat>元素來(lái)指定需要進(jìn)行合并的文件的列表以及結(jié)果文件所要輸

出的位置。<filelist>元素用于指定 a.js 和 b.js 要首先出現(xiàn)在合并的文件中,<fileset>元素指定了

之后要添加到目錄中的其他所有文件,a.js 和 b.js 除外。結(jié)果文件最后輸出到/js/output.js。

如果安裝了 Ant,就可以進(jìn)入 build.xml 文件所在的目錄,并運(yùn)行以下命令:

ant

然后構(gòu)建過(guò)程就開始了,最后生成合并了的文件。如果在文件中還有其他目標(biāo),可以使用以下代碼

僅執(zhí)行 js.concatenate 目標(biāo):

ant js.concatenate

可以根據(jù)需求,修改構(gòu)建過(guò)程以包含其他步驟。在開發(fā)周期中引入構(gòu)建這一步能讓你在部署之前對(duì)

JavaScript 文件進(jìn)行更多的處理。

24.3.2 驗(yàn)證

盡管現(xiàn)在出現(xiàn)了一些可以理解并支持 JavaScript 的 IDE,大多數(shù)開發(fā)人員還是要在瀏覽器中運(yùn)行代

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第696頁(yè)

678 第 24 章 最佳實(shí)踐

碼以檢查其語(yǔ)法。這種方法有一些問(wèn)題。首先,驗(yàn)證過(guò)程難以自動(dòng)化或者在不同系統(tǒng)間直接移植。其次,

除了語(yǔ)法錯(cuò)誤外,很多問(wèn)題只有在執(zhí)行代碼的時(shí)候才會(huì)遇到,這給錯(cuò)誤留下了空間;有些工具可以幫助

確定 JavaScript 代碼中潛在的問(wèn)題,其中最著名的就是 Douglas Crockford 的 JSLint (www.jslint.com)。

JSLint 可以查找 JavaScript 代碼中的語(yǔ)法錯(cuò)誤以及常見的編碼錯(cuò)誤。它可以發(fā)掘的一些潛在問(wèn)題

如下:

? eval()的使用;

? 未聲明變量的使用;

? 遺漏的分號(hào);

? 不恰當(dāng)?shù)膿Q行;

? 錯(cuò)誤的逗號(hào)使用;

? 語(yǔ)句周圍遺漏的括號(hào);

? switch 分支語(yǔ)句中遺漏的 break;

? 重復(fù)聲明的變量;

? with 的使用;

? 錯(cuò)誤使用的等號(hào)(替代了雙等號(hào)或三等號(hào));

? 無(wú)法到達(dá)的代碼。

為了方便訪問(wèn),它有一個(gè)在線版本,不過(guò)它也可以使用基于 Java的 Rhino JavaScript引擎(www.mozilla.

org/rhino/)運(yùn)行于命令行模式下。要在命令行中運(yùn)行 JSLint,首先要下載 Rhino,并從 www.jslint.com/下

載 Rhino 版本的 JSLint。一旦安裝完成,便可以使用下面的語(yǔ)法從命令行運(yùn)行 JSLint 了:

java -jar rhino-1.6R7.jar jslint.js [input files]

如這個(gè)例子:

java -jar rhino-1.6R7.jar jslint.js a.js b.js c.js

如果給定文件中有任何語(yǔ)法問(wèn)題或者是潛在的錯(cuò)誤,則會(huì)輸出有關(guān)錯(cuò)誤和警告的報(bào)告。如果沒有問(wèn)

題,代碼會(huì)直接結(jié)束而不顯示任何信息。

可以使用 Ant 將 JSLint 作為構(gòu)建過(guò)程的一部分運(yùn)行,添加如下一個(gè)目標(biāo):

<target name=\"js.verify\">

<apply executable=\"java\" parallel=\"false\">

<fileset dir=\"${build.dir}\" includes=\"output.js\"/>

<arg line=\"-jar\"/>

<arg path=\"${rhino.jar}\"/>

<arg path=\"${jslint.js}\" />

<srcfile/>

</apply>

</target>

SampleAntDir/build.xml

這個(gè)目標(biāo)假設(shè) Rhino jar 文件的位置已經(jīng)由叫做 rhino.jar 的屬性指定了,同時(shí) JSLint Rhino 文件

的位置由叫做 jslint.js 的屬性指定了。output.js 文件被傳遞給 JSLint 進(jìn)行校驗(yàn),然后顯示找到的

任何問(wèn)題。

給開發(fā)周期添加代碼驗(yàn)證這個(gè)環(huán)節(jié)有助于避免將來(lái)可能出現(xiàn)的一些錯(cuò)誤。建議開發(fā)人員給構(gòu)建過(guò)程

加入某種類型的代碼驗(yàn)證作為確定潛在問(wèn)題的一個(gè)方法,防患于未然。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第697頁(yè)

24.3 部署 679

14

2

3

17

18

13

19

7

8

22

10

24

12

JavaScript 代碼校驗(yàn)工具的列表可以在附錄 D 中找到。

24.3.3 壓縮

當(dāng)談及 JavaScript 文件壓縮,其實(shí)在討論兩個(gè)東西:代碼長(zhǎng)度和配重(Wire weight)。代碼長(zhǎng)度指的

是瀏覽器所需解析的字節(jié)數(shù),配重指的是實(shí)際從服務(wù)器傳送到瀏覽器的字節(jié)數(shù)。在 Web 開發(fā)的早期,

這兩個(gè)數(shù)字幾乎是一樣的,因?yàn)閺姆?wù)器端到客戶端原封不動(dòng)地傳遞了源文件。而在今天的 Web 上,

這兩者很少相等,實(shí)際上也不應(yīng)相等。

1. 文件壓縮

因?yàn)?JavaScript 并非編譯為字節(jié)碼,而是按照源代碼傳送的,代碼文件通常包含瀏覽器執(zhí)行所不需

要的額外的信息和格式。注釋,額外的空白,以及長(zhǎng)長(zhǎng)的變量名和函數(shù)名雖然提高了可讀性,但卻是傳

送給瀏覽器時(shí)不必要的字節(jié)。不過(guò),我們可以使用壓縮工具減少文件的大小。

壓縮器一般進(jìn)行如下一些步驟:

? 刪除額外的空白(包括換行);

? 刪除所有注釋;

? 縮短變量名。

JavaScript 有不少壓縮工具可用(附錄 D 中有一個(gè)完整列表),其中最優(yōu)秀的(有爭(zhēng)議的)是 YUI 壓

縮器,http://yuilibrary.com /projects/yuicompressor。YUI 壓縮器使用了 Rhino JavaScript 解析器將 JavaScript

代碼令牌化。然后使用這個(gè)令牌流創(chuàng)建代碼不包含空白和注釋的優(yōu)化版本。與一般的基于表達(dá)式的壓縮

器不同的地方在于,YUI 壓縮可以確保不引入任何語(yǔ)法錯(cuò)誤,并可以安全地縮短局部變量名。

YUI 壓縮器是作為 Java 的一個(gè) jar 文件發(fā)布的,名字叫 yuicompressor-x.y.z.jar,其中 x.y.z

是版本號(hào)。在寫本書的時(shí)候,2.3.5 是最新的版本??梢允褂靡韵旅钚懈袷絹?lái)使用 YUI 壓縮器:

java -jar yuicompressor-x.y.z.jar [options] [input files]

YUI 壓縮器的選項(xiàng)列在了下面的表格內(nèi)。

選 項(xiàng) 描 述

-h 顯示幫助信息

-o outputFile 指定輸出文件的文件名。如果沒有該選項(xiàng),那么輸出文件名是輸入文件名加上-min。例

如,叫做 input.js 的輸入文件,那么會(huì)產(chǎn)生 input-min.js

--line-break column 指定每行多少個(gè)字符之后添加換行。默認(rèn)情況下,壓縮過(guò)的文件只輸出為一行,可能在

某些版本控制系統(tǒng)中會(huì)出錯(cuò)

-v, --verbose 詳細(xì)模式,輸出可以進(jìn)行更好壓縮的提示和警告

--charset charset 指定輸入文件所使用的字符集。輸出文件會(huì)使用同樣的字符集

--nomunge 關(guān)閉局部變量替換

--disable-optimizations 關(guān)閉 YUI 壓縮器的細(xì)節(jié)優(yōu)化

--preserve-semi 保留本來(lái)要被刪除的無(wú)用的分號(hào)

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第698頁(yè)

680 第 24 章 最佳實(shí)踐

例如,以下命令行可以用來(lái)將 CookieUtil.js 壓縮成一個(gè)叫做 cookie.js 的文件:

java -jar yuicompressor-2.3.5.jar -o cookie.js CookieUtil.js

YUI 壓縮器也可以通過(guò)直接調(diào)用 java 可執(zhí)行文件在 Ant 中使用,如下面的例子所示:

<!-- Credit: Julien Lecomte, http://www.julienlecomte.net/blog/2007/09/16/ -->

<target name=\"js.compress\">

<apply executable=\"java\" parallel=\"false\">

<fileset dir=\"${build.dir}\" includes=\"output.js\"/>

<arg line=\"-jar\"/>

<arg path=\"${yuicompressor.jar}\"/>

<arg line=\"-o ${build.dir}/output-min.js\"/>

<srcfile/>

</apply>

</target>

SampleAntDir/build.xml

該目標(biāo)包含了一個(gè)文件 output.js,由構(gòu)建過(guò)程生成的,并傳遞給 YUI 壓縮器。輸出文件指定為

同一目錄下的 output-min.js。這里假設(shè) yuicompressor.jar 屬性包含了 YUI 壓縮器的 jar 文件

的位置。然后可以使用以下命令運(yùn)行這個(gè)目標(biāo):

ant js.compress

所有的 JavaScript 文件在部署到生產(chǎn)環(huán)境之前,都應(yīng)該使用 YUI 壓縮器或者類似的工具進(jìn)行壓縮。

給構(gòu)建過(guò)程添加一個(gè)壓縮 JavaScript 文件的環(huán)節(jié)以確保每次都進(jìn)行這個(gè)操作。

2. HTTP 壓縮

配重指的是實(shí)際從服務(wù)器傳送到瀏覽器的字節(jié)數(shù)。因?yàn)楝F(xiàn)在的服務(wù)器和瀏覽器都有壓縮功能,這個(gè)

字節(jié)數(shù)不一定和代碼長(zhǎng)度一樣。所有的五大 Web 瀏覽器(IE、Firefox、Safari、Chrome 和 Opera)都支

持對(duì)所接收的資源進(jìn)行客戶端解壓縮。這樣服務(wù)器端就可以使用服務(wù)器端相關(guān)功能來(lái)壓縮 JavaScript 文

件。一個(gè)指定了文件使用了給定格式進(jìn)行了壓縮的 HTTP 頭包含在了服務(wù)器響應(yīng)中。接著瀏覽器會(huì)查看

該 HTTP 頭確定文件是否已被壓縮,然后使用合適的格式進(jìn)行解壓縮。結(jié)果是和原來(lái)的代碼量相比在網(wǎng)

絡(luò)中傳遞的字節(jié)數(shù)量大大減少了。

對(duì)于 Apache Web 服務(wù)器,有兩個(gè)模塊可以進(jìn)行 HTTP 壓縮:mod_gzip(Apache1.3.x)和 mod_deflate

(Apache 2.0.x)。對(duì)于 mod_gzip,可以給 httpd.conf 文件或者是.htaccess 文件添加以下代碼啟用對(duì)

JavaScript 的自動(dòng)壓縮:

#告訴 mod_zip 要包含任何以.js 結(jié)尾的文件

mod_gzip_item_include file \\.js$

該行代碼告訴 mod_zip 要包含來(lái)自瀏覽器請(qǐng)求的任何以.js 結(jié)尾的文件。假設(shè)你所有的 JavaScript

文件都以.js 結(jié)尾,就可以壓縮所有請(qǐng)求并應(yīng)用合適的 HTTP 頭以表示內(nèi)容已被壓縮。關(guān)于 mod_zip

的更多信息,請(qǐng)?jiān)L問(wèn)項(xiàng)目網(wǎng)站 http://www.sourceforge.net/projects/mod-gzip/。

對(duì)于 mod_deflate,可以類似添加一行代碼以保證 JavaScript 文件在被發(fā)送之前已被壓縮。將以下

這一行代碼添加到 httpd.conf 文件或者是.htaccess 文件中:

#告訴 mod_deflate 要包含所有的 JavaScript 文件

AddOutputFilterByType DEFLATE application/x-javascript

注意這一行代碼用到了響應(yīng)的 MIME 類型來(lái)確定是否對(duì)其進(jìn)行壓縮。記住雖然<script>的 type

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第699頁(yè)

24.4 小結(jié) 681

14

2

3

17

18

13

19

7

8

22

10

24

12

屬性用的是 text/javascript,但是 JavaScript 文件一般還是用 application/x-javascript 作為

其服務(wù)的 MIME 類型。關(guān)于 mod_deflate 的更多信息,請(qǐng)?jiān)L問(wèn) http://httpd.apache.org/docs/2.0/mod/

mod_deflate.html。

mod_gzip 和 mod_deflate 都可以節(jié)省大約 70%的 JavaScript 文件大小。這很大程度上是因?yàn)?/p>

JavaScript 都是文本文件,因此可以非常有效地進(jìn)行壓縮。減少文件的配重可以減少需要傳輸?shù)綖g覽器

的時(shí)間。記住有一點(diǎn)點(diǎn)細(xì)微的代價(jià),因?yàn)榉?wù)器必須花時(shí)間對(duì)每個(gè)請(qǐng)求壓縮文件,當(dāng)瀏覽器接收到這些

文件后也需要花一些時(shí)間解壓縮。不過(guò),一般來(lái)說(shuō),這個(gè)代價(jià)還是值得的。

大部分 Web 服務(wù)器,開源的或是商業(yè)的,都有一些 HTTP 壓縮功能。請(qǐng)查看服

務(wù)器的文檔說(shuō)明以確定如何合適地配置壓縮。

24.4 小結(jié)

隨著 JavaScript 開發(fā)的成熟,也出現(xiàn)了很多最佳實(shí)踐。過(guò)去一度認(rèn)為只是一種愛好的東西現(xiàn)在變成

了正當(dāng)?shù)穆殬I(yè),同時(shí)還需要經(jīng)歷過(guò)去其他編程語(yǔ)言要做的一些研究,如可維護(hù)性、性能和部署。

JavaScript 中的可維護(hù)性部分涉及到下面的代碼約定。

? 來(lái)自其他語(yǔ)言中的代碼約定可以用于決定何時(shí)進(jìn)行注釋,以及如何進(jìn)行縮進(jìn),不過(guò) JavaScript

需要針對(duì)其松散類型的性質(zhì)創(chuàng)造一些特殊的約定。

? 由于 JavaScript 必須與 HTML 和 CSS 共存,所以讓各自完全定義其自己的目的非常重要:

JavaScript 應(yīng)該定義行為,HTML 應(yīng)該定義內(nèi)容,CSS 應(yīng)該定義外觀。

? 這些職責(zé)的混淆會(huì)導(dǎo)致難以調(diào)試的錯(cuò)誤和維護(hù)上的問(wèn)題。

隨著 Web 應(yīng)用中的 JavaScript 數(shù)量的增加,性能變得更加重要,因此,你需要牢記以下事項(xiàng)。

? JavaScript 執(zhí)行所花費(fèi)的時(shí)間直接影響到整個(gè) Web 頁(yè)面的性能,所以其重要性是不能忽略的。

? 針對(duì)基于 C 的語(yǔ)言的很多性能的建議也適用于 JavaScript,如有關(guān)循環(huán)性能和使用 switch 語(yǔ)句

替代 if 語(yǔ)句。

? 還有一個(gè)要記住的重要事情,即 DOM 交互開銷很大,所以需要限制 DOM 操作的次數(shù)。

流程的最后一步是部署。本章討論了以下一些關(guān)鍵點(diǎn)。

? 為了協(xié)助部署,推薦設(shè)置一個(gè)可以將 JavaScript 合并為較少文件(理想情況是一個(gè))的構(gòu)建過(guò)程。

? 有了構(gòu)建過(guò)程也可以對(duì)源代碼自動(dòng)運(yùn)行額外的處理和過(guò)濾。例如,你可以運(yùn)行 JavaScript 驗(yàn)證器

來(lái)確保沒有語(yǔ)法錯(cuò)誤或者是代碼沒有潛在的問(wèn)題。

? 在部署前推薦使用壓縮器將文件盡可能變小。

? 和 HTTP 壓縮一起使用可以讓 JavaScript 文件盡可能小,因此對(duì)整體頁(yè)面性能的影響也會(huì)最小。

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第700頁(yè)

682 第 25 章 新興的 API

新興的 API

本章內(nèi)容

? 創(chuàng)建平滑的動(dòng)畫

? 操作文件

? 使用 Web Workers 在后臺(tái)執(zhí)行 JavaScript

著 HTML5 的出現(xiàn),面向未來(lái) Web 應(yīng)用的 JavaScript API 也得到了極大的發(fā)展。這些 API 沒有

包含在 HTML5 規(guī)范中,而是各自有各自的規(guī)范。但是,它們都屬于“HTML5 相關(guān)的 API”。

本章介紹的所有 API 都在持續(xù)制定中,還沒有完全固定下來(lái)。

無(wú)論如何,瀏覽器已經(jīng)著手實(shí)現(xiàn)這些 API,而 Web 應(yīng)用開發(fā)人員也都開始使用它們了。讀者應(yīng)該能

夠注意到,其中很多 API 都帶有特定于瀏覽器的前綴,比如微軟是 ms,而 Chrome 和 Safari 是 webkit。

通過(guò)添加這些前綴,不同的瀏覽器可以測(cè)試還在開發(fā)中的新 API,不過(guò)請(qǐng)記住,去掉前綴之后的部分在

所有瀏覽器中都是一致的。

25.1 requestAnimationFrame()

很長(zhǎng)時(shí)間以來(lái),計(jì)時(shí)器和循環(huán)間隔一直都是 JavaScript 動(dòng)畫的最核心技術(shù)。雖然 CSS 變換及動(dòng)畫為

Web 開發(fā)人員提供了實(shí)現(xiàn)動(dòng)畫的簡(jiǎn)單手段,但 JavaScript 動(dòng)畫開發(fā)領(lǐng)域的狀況這些年來(lái)并沒有大的變化。

Firefox 4 最早為 JavaScript 動(dòng)畫添加了一個(gè)新 API,即 mozRequestAnimationFrame()。這個(gè)方法會(huì)

告訴瀏覽器:有一個(gè)動(dòng)畫開始了。進(jìn)而瀏覽器就可以確定重繪的最佳方式。

25.1.1 早期動(dòng)畫循環(huán)

在 JavaScript 中創(chuàng)建動(dòng)畫的典型方式,就是使用 setInterval()方法來(lái)控制所有動(dòng)畫。以下是一個(gè)

使用 setInterval()的基本動(dòng)畫循環(huán):

(function(){

function updateAnimations(){

doAnimation1();

doAnimation2();

//其他動(dòng)畫

}

setInterval(updateAnimations, 100);

})();

第 25 章

圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

百萬(wàn)用戶使用云展網(wǎng)進(jìn)行書冊(cè)翻頁(yè)效果制作,只要您有文檔,即可一鍵上傳,自動(dòng)生成鏈接和二維碼(獨(dú)立電子書),支持分享到微信和網(wǎng)站!
收藏
轉(zhuǎn)發(fā)
下載
免費(fèi)制作
其他案例
更多案例
免費(fèi)制作
x
{{item.desc}}
下載
{{item.title}}
{{toast}}