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

javascript-gaojichengx有目錄u

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

javascript-gaojichengx有目錄u

21.4 跨源資源共享 583 1 15 16 45 13 620 21 9 1011 12var xdr = new XDomainRequest(); xdr.onload = function(){ alert(xdr.responseText); }; xdr.open(\"get\", \"http://www.somewhere-else.com/page/\"); xdr.send(null); XDomainRequestExample01.htm 在接收到響應(yīng)后,你只能訪問響應(yīng)的原始文本;沒有辦法確定響應(yīng)的狀態(tài)代碼。而且,只要響應(yīng)有效就會(huì)觸發(fā) load 事件,如果失?。ò憫?yīng)中缺少 Access-Control-Allow-Origin 頭部)就會(huì)觸發(fā) error 事件。遺憾的是,除了錯(cuò)誤本身之外,沒有其他信息可用,因此唯一能夠確定的就只有請(qǐng)求未成功了。要檢測(cè)錯(cuò)誤,可以像下面這樣指定一個(gè) onerror 事件處理程序。var xdr = new XDomainRequest(); xdr.on... [收起]
[展開]
javascript-gaojichengx有目錄u
粉絲: {{bookData.followerCount}}
文本內(nèi)容
第601頁

21.4 跨源資源共享 583

1

15

16

4

5

13

6

20

21

9

10

11

12

var xdr = new XDomainRequest();

xdr.onload = function(){

alert(xdr.responseText);

};

xdr.open(\"get\", \"http://www.somewhere-else.com/page/\");

xdr.send(null);

XDomainRequestExample01.htm

在接收到響應(yīng)后,你只能訪問響應(yīng)的原始文本;沒有辦法確定響應(yīng)的狀態(tài)代碼。而且,只要響應(yīng)有

效就會(huì)觸發(fā) load 事件,如果失?。ò憫?yīng)中缺少 Access-Control-Allow-Origin 頭部)就會(huì)觸

發(fā) error 事件。遺憾的是,除了錯(cuò)誤本身之外,沒有其他信息可用,因此唯一能夠確定的就只有請(qǐng)求

未成功了。要檢測(cè)錯(cuò)誤,可以像下面這樣指定一個(gè) onerror 事件處理程序。

var xdr = new XDomainRequest();

xdr.onload = function(){

alert(xdr.responseText);

};

xdr.onerror = function(){

alert(\"An error occurred.\");

};

xdr.open(\"get\", \"http://www.somewhere-else.com/page/\");

xdr.send(null);

XDomainRequestExample01.htm

鑒于導(dǎo)致 XDR 請(qǐng)求失敗的因素很多,因此建議你不要忘記通過 onerror 事件處

理程序來捕獲該事件;否則,即使請(qǐng)求失敗也不會(huì)有任何提示。

在請(qǐng)求返回前調(diào)用 abort()方法可以終止請(qǐng)求:

xdr.abort(); //終止請(qǐng)求

與 XHR 一樣,XDR 對(duì)象也支持 timeout 屬性以及 ontimeout 事件處理程序。下面是一個(gè)例子。

var xdr = new XDomainRequest();

xdr.onload = function(){

alert(xdr.responseText);

};

xdr.onerror = function(){

alert(\"An error occurred.\");

};

xdr.timeout = 1000;

xdr.ontimeout = function(){

alert(\"Request took too long.\");

};

xdr.open(\"get\", \"http://www.somewhere-else.com/page/\");

xdr.send(null);

這個(gè)例子會(huì)在運(yùn)行 1 秒鐘后超時(shí),并隨即調(diào)用 ontimeout 事件處理程序。

為支持 POST 請(qǐng)求,XDR 對(duì)象提供了 contentType 屬性,用來表示發(fā)送數(shù)據(jù)的格式,如下面的例

子所示。

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

第602頁

584 第 21 章 Ajax 與 Comet

var xdr = new XDomainRequest();

xdr.onload = function(){

alert(xdr.responseText);

};

xdr.onerror = function(){

alert(\"An error occurred.\");

};

xdr.open(\"post\", \"http://www.somewhere-else.com/page/\");

xdr.contentType = \"application/x-www-form-urlencoded\";

xdr.send(\"name1=value1&name2=value2\");

這個(gè)屬性是通過 XDR 對(duì)象影響頭部信息的唯一方式。

21.4.2 其他瀏覽器對(duì)CORS的實(shí)現(xiàn)

Firefox 3.5+、Safari 4+、Chrome、iOS 版 Safari 和 Android 平臺(tái)中的 WebKit 都通過 XMLHttpRequest

對(duì)象實(shí)現(xiàn)了對(duì) CORS 的原生支持。在嘗試打開不同來源的資源時(shí),無需額外編寫代碼就可以觸發(fā)這個(gè)行

為。要請(qǐng)求位于另一個(gè)域中的資源,使用標(biāo)準(zhǔn)的 XHR 對(duì)象并在 open()方法中傳入絕對(duì) URL 即可,例如:

var xhr = createXHR();

xhr.onreadystatechange = function(){

if (xhr.readyState == 4){

if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){

alert(xhr.responseText);

} else {

alert(\"Request was unsuccessful: \" + xhr.status);

}

}

};

xhr.open(\"get\", \"http://www.somewhere-else.com/page/\", true);

xhr.send(null);

與 IE 中的 XDR 對(duì)象不同,通過跨域 XHR 對(duì)象可以訪問 status 和 statusText 屬性,而且還支

持同步請(qǐng)求??缬?XHR 對(duì)象也有一些限制,但為了安全這些限制是必需的。以下就是這些限制。

? 不能使用 setRequestHeader()設(shè)置自定義頭部。

? 不能發(fā)送和接收 cookie。

? 調(diào)用 getAllResponseHeaders()方法總會(huì)返回空字符串。

由于無論同源請(qǐng)求還是跨源請(qǐng)求都使用相同的接口,因此對(duì)于本地資源,最好使用相對(duì) URL,在訪

問遠(yuǎn)程資源時(shí)再使用絕對(duì) URL。這樣做能消除歧義,避免出現(xiàn)限制訪問頭部或本地 cookie 信息等問題。

21.4.3 Preflighted Reqeusts

CORS 通過一種叫做 Preflighted Requests 的透明服務(wù)器驗(yàn)證機(jī)制支持開發(fā)人員使用自定義的頭部、

GET 或 POST 之外的方法,以及不同類型的主體內(nèi)容。在使用下列高級(jí)選項(xiàng)來發(fā)送請(qǐng)求時(shí),就會(huì)向服務(wù)

器發(fā)送一個(gè) Preflight 請(qǐng)求。這種請(qǐng)求使用 OPTIONS 方法,發(fā)送下列頭部。

? Origin:與簡(jiǎn)單的請(qǐng)求相同。

? Access-Control-Request-Method:請(qǐng)求自身使用的方法。

? Access-Control-Request-Headers:(可選)自定義的頭部信息,多個(gè)頭部以逗號(hào)分隔。

以下是一個(gè)帶有自定義頭部 NCZ 的使用 POST 方法發(fā)送的請(qǐng)求。

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

第603頁

21.4 跨源資源共享 585

1

15

16

4

5

13

6

20

21

9

10

11

12

Origin: http://www.nczonline.net

Access-Control-Request-Method: POST

Access-Control-Request-Headers: NCZ

發(fā)送這個(gè)請(qǐng)求后,服務(wù)器可以決定是否允許這種類型的請(qǐng)求。服務(wù)器通過在響應(yīng)中發(fā)送如下頭部與

瀏覽器進(jìn)行溝通。

? Access-Control-Allow-Origin:與簡(jiǎn)單的請(qǐng)求相同。

? Access-Control-Allow-Methods:允許的方法,多個(gè)方法以逗號(hào)分隔。

? Access-Control-Allow-Headers:允許的頭部,多個(gè)頭部以逗號(hào)分隔。

? Access-Control-Max-Age:應(yīng)該將這個(gè) Preflight 請(qǐng)求緩存多長(zhǎng)時(shí)間(以秒表示)。

例如:

Access-Control-Allow-Origin: http://www.nczonline.net

Access-Control-Allow-Methods: POST, GET

Access-Control-Allow-Headers: NCZ

Access-Control-Max-Age: 1728000

Preflight 請(qǐng)求結(jié)束后,結(jié)果將按照響應(yīng)中指定的時(shí)間緩存起來。而為此付出的代價(jià)只是第一次發(fā)送

這種請(qǐng)求時(shí)會(huì)多一次 HTTP 請(qǐng)求。

支持 Preflight 請(qǐng)求的瀏覽器包括 Firefox 3.5+、Safari 4+和 Chrome。IE 10 及更早版本都不支持。

21.4.4 帶憑據(jù)的請(qǐng)求

默認(rèn)情況下,跨源請(qǐng)求不提供憑據(jù)(cookie、HTTP 認(rèn)證及客戶端 SSL 證明 等 )。 通 過 將

withCredentials 屬性設(shè)置為 true,可以指定某個(gè)請(qǐng)求應(yīng)該發(fā)送憑據(jù)。如果服務(wù)器接受帶憑據(jù)的請(qǐng)

求,會(huì)用下面的 HTTP 頭部來響應(yīng)。

Access-Control-Allow-Credentials: true

如果發(fā)送的是帶憑據(jù)的請(qǐng)求,但服務(wù)器的響應(yīng)中沒有包含這個(gè)頭部,那么瀏覽器就不會(huì)把響應(yīng)交給

JavaScript(于是,responseText 中將是空字符串,status 的值為 0,而且會(huì)調(diào)用 onerror()事件處

理程序)。另外,服務(wù)器還可以在 Preflight 響應(yīng)中發(fā)送這個(gè) HTTP 頭部,表示允許源發(fā)送帶憑據(jù)的請(qǐng)求。

支持 withCredentials 屬性的瀏覽器有 Firefox 3.5+、Safari 4+和 Chrome。IE 10 及更早版本都不

支持。

21.4.5 跨瀏覽器的CORS

即使瀏覽器對(duì) CORS 的支持程度并不都一樣,但所有瀏覽器都支持簡(jiǎn)單的(非 Preflight 和不帶憑據(jù)

的)請(qǐng)求,因此有必要實(shí)現(xiàn)一個(gè)跨瀏覽器的方案。檢測(cè) XHR 是否支持 CORS 的最簡(jiǎn)單方式,就是檢查

是否存在 withCredentials 屬性。再結(jié)合檢測(cè) XDomainRequest 對(duì)象是否存在,就可以兼顧所有瀏

覽器了。

function createCORSRequest(method, url){

var xhr = new XMLHttpRequest();

if (\"withCredentials\" in xhr){

xhr.open(method, url, true);

} else if (typeof XDomainRequest != \"undefined\"){

vxhr = new XDomainRequest();

xhr.open(method, url);

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

第604頁

586 第 21 章 Ajax 與 Comet

} else {

xhr = null;

}

return xhr;

}

var request = createCORSRequest(\"get\", \"http://www.somewhere-else.com/page/\");

if (request){

request.onload = function(){

//對(duì) request.responseText 進(jìn)行處理

};

request.send();

}

CrossBrowserCORSRequestExample01.htm

Firefox、Safari 和 Chrome 中的 XMLHttpRequest 對(duì)象與 IE 中的 XDomainRequest 對(duì)象類似,都

提供了夠用的接口,因此以上模式還是相當(dāng)有用的。這兩個(gè)對(duì)象共同的屬性/方法如下。

? abort():用于停止正在進(jìn)行的請(qǐng)求。

? onerror:用于替代 onreadystatechange 檢測(cè)錯(cuò)誤。

? onload:用于替代 onreadystatechange 檢測(cè)成功。

? responseText:用于取得響應(yīng)內(nèi)容。

? send():用于發(fā)送請(qǐng)求。

以上成員都包含在 createCORSRequest()函數(shù)返回的對(duì)象中,在所有瀏覽器中都能正常使用。

21.5 其他跨域技術(shù)

在 CORS 出現(xiàn)以前,要實(shí)現(xiàn)跨域 Ajax 通信頗費(fèi)一些周折。開發(fā)人員想出了一些辦法,利用 DOM 中

能夠執(zhí)行跨域請(qǐng)求的功能,在不依賴 XHR 對(duì)象的情況下也能發(fā)送某種請(qǐng)求。雖然 CORS 技術(shù)已經(jīng)無處

不在,但開發(fā)人員自己發(fā)明的這些技術(shù)仍然被廣泛使用,畢竟這樣不需要修改服務(wù)器端代碼。

21.5.1 圖像Ping

上述第一種跨域請(qǐng)求技術(shù)是使用<img>標(biāo)簽。我們知道,一個(gè)網(wǎng)頁可以從任何網(wǎng)頁中加載圖像,不

用擔(dān)心跨域不跨域。這也是在線廣告跟蹤瀏覽量的主要方式。正如第 13 章討論過的,也可以動(dòng)態(tài)地創(chuàng)

建圖像,使用它們的 onload 和 onerror 事件處理程序來確定是否接收到了響應(yīng)。

動(dòng)態(tài)創(chuàng)建圖像經(jīng)常用于圖像 Ping。圖像 Ping 是與服務(wù)器進(jìn)行簡(jiǎn)單、單向的跨域通信的一種方式。

請(qǐng)求的數(shù)據(jù)是通過查詢字符串形式發(fā)送的,而響應(yīng)可以是任意內(nèi)容,但通常是像素圖或 204 響應(yīng)。通過

圖像 Ping,瀏覽器得不到任何具體的數(shù)據(jù),但通過偵聽 load 和 error 事件,它能知道響應(yīng)是什么時(shí)

候接收到的。來看下面的例子。

var img = new Image();

img.onload = img.onerror = function(){

alert(\"Done!\");

};

img.src = \"http://www.example.com/test?name=Nicholas\";

ImagePingExample01.htm

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

第605頁

21.5 其他跨域技術(shù) 587

1

15

16

4

5

13

6

20

21

9

10

11

12

這里創(chuàng)建了一個(gè) Image 的實(shí)例,然后將 onload 和 onerror 事件處理程序指定為同一個(gè)函數(shù)。這

樣無論是什么響應(yīng),只要請(qǐng)求完成,就能得到通知。請(qǐng)求從設(shè)置 src 屬性那一刻開始,而這個(gè)例子在請(qǐng)

求中發(fā)送了一個(gè) name 參數(shù)。

圖像 Ping 最常用于跟蹤用戶點(diǎn)擊頁面或動(dòng)態(tài)廣告曝光次數(shù)。圖像 Ping 有兩個(gè)主要的缺點(diǎn),一是只

能發(fā)送 GET 請(qǐng)求,二是無法訪問服務(wù)器的響應(yīng)文本。因此,圖像 Ping 只能用于瀏覽器與服務(wù)器間的單

向通信。

21.5.2 JSONP

JSONP 是 JSON with padding(填充式 JSON 或參數(shù)式 JSON)的簡(jiǎn)寫,是應(yīng)用 JSON 的一種新方法,

在后來的 Web 服務(wù)中非常流行。JSONP 看起來與 JSON 差不多,只不過是被包含在函數(shù)調(diào)用中的 JSON,

就像下面這樣。

callback({ \"name\": \"Nicholas\" });

JSONP 由兩部分組成:回調(diào)函數(shù)和數(shù)據(jù)?;卣{(diào)函數(shù)是當(dāng)響應(yīng)到來時(shí)應(yīng)該在頁面中調(diào)用的函數(shù)。回調(diào)

函數(shù)的名字一般是在請(qǐng)求中指定的。而數(shù)據(jù)就是傳入回調(diào)函數(shù)中的JSON數(shù)據(jù)。下面是一個(gè)典型的JSONP

請(qǐng)求。

http://freegeoip.net/json/?callback=handleResponse

這個(gè) URL 是在請(qǐng)求一個(gè) JSONP 地理定位服務(wù)。通過查詢字符串來指定 JSONP 服務(wù)的回調(diào)參數(shù)是很

常見的,就像上面的 URL 所示,這里指定的回調(diào)函數(shù)的名字叫 handleResponse()。

JSONP 是通過動(dòng)態(tài)<script>元素(要了解詳細(xì)信息,請(qǐng)參考第 13 章)來使用的,使用時(shí)可以為

src 屬性指定一個(gè)跨域 URL。這里的<script>元素與<img>元素類似,都有能力不受限制地從其他域

加載資源。因?yàn)?JSONP 是有效的 JavaScript 代碼,所以在請(qǐng)求完成后,即在 JSONP 響應(yīng)加載到頁面中

以后,就會(huì)立即執(zhí)行。來看一個(gè)例子。

function handleResponse(response){

alert(\"You’re at IP address \" + response.ip + \", which is in \" +

response.city + \", \" + response.region_name);

}

var script = document.createElement(\"script\");

script.src = \"http://freegeoip.net/json/?callback=handleResponse\";

document.body.insertBefore(script, document.body.firstChild);

JSONPExample01.htm

這個(gè)例子通過查詢地理定位服務(wù)來顯示你的 IP 地址和位置信息。

JSONP 之所以在開發(fā)人員中極為流行,主要原因是它非常簡(jiǎn)單易用。與圖像 Ping 相比,它的優(yōu)點(diǎn)

在于能夠直接訪問響應(yīng)文本,支持在瀏覽器與服務(wù)器之間雙向通信。不過,JSONP 也有兩點(diǎn)不足。

首先,JSONP 是從其他域中加載代碼執(zhí)行。如果其他域不安全,很可能會(huì)在響應(yīng)中夾帶一些惡意代

碼,而此時(shí)除了完全放棄 JSONP 調(diào)用之外,沒有辦法追究。因此在使用不是你自己運(yùn)維的 Web 服務(wù)時(shí),

一定得保證它安全可靠。

其次,要確定 JSONP 請(qǐng)求是否失敗并不容易。雖然 HTML5 給<script>元素新增了一個(gè) onerror

事件處理程序,但目前還沒有得到任何瀏覽器支持。為此,開發(fā)人員不得不使用計(jì)時(shí)器檢測(cè)指定時(shí)間內(nèi)

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

第606頁

588 第 21 章 Ajax 與 Comet

是否接收到了響應(yīng)。但就算這樣也不能盡如人意,畢竟不是每個(gè)用戶上網(wǎng)的速度和帶寬都一樣。

21.5.3 Comet

Comet 是 Alex Russell①發(fā)明的一個(gè)詞兒,指的是一種更高級(jí)的 Ajax 技術(shù)(經(jīng)常也有人稱為“服務(wù)器

推送”)。Ajax 是一種從頁面向服務(wù)器請(qǐng)求數(shù)據(jù)的技術(shù),而 Comet 則是一種服務(wù)器向頁面推送數(shù)據(jù)的技

術(shù)。Comet 能夠讓信息近乎實(shí)時(shí)地被推送到頁面上,非常適合處理體育比賽的分?jǐn)?shù)和股票報(bào)價(jià)。

有兩種實(shí)現(xiàn) Comet 的方式:長(zhǎng)輪詢和流。長(zhǎng)輪詢是傳統(tǒng)輪詢(也稱為短輪詢)的一個(gè)翻版,即瀏覽

器定時(shí)向服務(wù)器發(fā)送請(qǐng)求,看有沒有更新的數(shù)據(jù)。圖 21-1 展示的是短輪詢的時(shí)間線。

圖 21-1

長(zhǎng)輪詢把短輪詢顛倒了一下。頁面發(fā)起一個(gè)到服務(wù)器的請(qǐng)求,然后服務(wù)器一直保持連接打開,直到

有數(shù)據(jù)可發(fā)送。發(fā)送完數(shù)據(jù)之后,瀏覽器關(guān)閉連接,隨即又發(fā)起一個(gè)到服務(wù)器的新請(qǐng)求。這一過程在頁

面打開期間一直持續(xù)不斷。圖 21-2 展示了長(zhǎng)輪詢的時(shí)間線。

圖 21-2

無論是短輪詢還是長(zhǎng)輪詢,瀏覽器都要在接收數(shù)據(jù)之前,先發(fā)起對(duì)服務(wù)器的連接。兩者最大的區(qū)別

在于服務(wù)器如何發(fā)送數(shù)據(jù)。短輪詢是服務(wù)器立即發(fā)送響應(yīng),無論數(shù)據(jù)是否有效,而長(zhǎng)輪詢是等待發(fā)送響

應(yīng)。輪詢的優(yōu)勢(shì)是所有瀏覽器都支持,因?yàn)槭褂?XHR 對(duì)象和 setTimeout()就能實(shí)現(xiàn)。而你要做的就

是決定什么時(shí)候發(fā)送請(qǐng)求。

第二種流行的 Comet 實(shí)現(xiàn)是 HTTP 流。流不同于上述兩種輪詢,因?yàn)樗陧撁娴恼麄€(gè)生命周期內(nèi)只

使用一個(gè) HTTP 連接。具體來說,就是瀏覽器向服務(wù)器發(fā)送一個(gè)請(qǐng)求,而服務(wù)器保持連接打開,然后周

期性地向?yàn)g覽器發(fā)送數(shù)據(jù)。比如,下面這段 PHP 腳本就是采用流實(shí)現(xiàn)的服務(wù)器中常見的形式。

<?php

$i = 0;

while(true){

——————————

① Alex Russell 是著名 JavaScript 框架 Dojo 的創(chuàng)始人。

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

第607頁

21.5 其他跨域技術(shù) 589

1

15

16

4

5

13

6

20

21

9

10

11

12

//輸出一些數(shù)據(jù),然后立即刷新輸出緩存

echo \"Number is $i\";

flush();

//等幾秒鐘

sleep(10);

$i++;

}

所有服務(wù)器端語言都支持打印到輸出緩存然后刷新(將輸出緩存中的內(nèi)容一次性全部發(fā)送到客戶

端)的功能。而這正是實(shí)現(xiàn) HTTP 流的關(guān)鍵所在。

在 Firefox、Safari、Opera 和 Chrome 中,通過偵聽 readystatechange 事件及檢測(cè) readyState

的值是否為 3,就可以利用 XHR 對(duì)象實(shí)現(xiàn) HTTP 流。在上述這些瀏覽器中,隨著不斷從服務(wù)器接收數(shù)

據(jù),readyState 的值會(huì)周期性地變?yōu)?3。當(dāng) readyState 值變?yōu)?3 時(shí),responseText 屬性中就會(huì)保

存接收到的所有數(shù)據(jù)。此時(shí),就需要比較此前接收到的數(shù)據(jù),決定從什么位置開始取得最新的數(shù)據(jù)。使

用 XHR 對(duì)象實(shí)現(xiàn) HTTP 流的典型代碼如下所示。

function createStreamingClient(url, progress, finished){

var xhr = new XMLHttpRequest(),

received = 0;

xhr.open(\"get\", url, true);

xhr.onreadystatechange = function(){

var result;

if (xhr.readyState == 3){

//只取得最新數(shù)據(jù)并調(diào)整計(jì)數(shù)器

result = xhr.responseText.substring(received);

received += result.length;

//調(diào)用 progress 回調(diào)函數(shù)

progress(result);

} else if (xhr.readyState == 4){

finished(xhr.responseText);

}

};

xhr.send(null);

return xhr;

}

var client = createStreamingClient(\"streaming.php\", function(data){

alert(\"Received: \" + data);

}, function(data){

alert(\"Done!\");

});

HTTPStreamingExample01.htm

這個(gè) createStreamingClient()函數(shù)接收三個(gè)參數(shù):要連接的 URL、在接收到數(shù)據(jù)時(shí)調(diào)用的函

數(shù)以及關(guān)閉連接時(shí)調(diào)用的函數(shù)。有時(shí)候,當(dāng)連接關(guān)閉時(shí),很可能還需要重新建立,所以關(guān)注連接什么時(shí)

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

第608頁

590 第 21 章 Ajax 與 Comet

候關(guān)閉還是有必要的。

只要 readystatechange 事件發(fā)生,而且 readyState 值為 3,就對(duì) responseText 進(jìn)行分割以

取得最新數(shù)據(jù)。這里的 received 變量用于記錄已經(jīng)處理了多少個(gè)字符,每次 readyState 值為 3 時(shí)都

遞增。然后,通過 progress 回調(diào)函數(shù)來處理傳入的新數(shù)據(jù)。而當(dāng) readyState 值為 4 時(shí),則執(zhí)行

finished 回調(diào)函數(shù),傳入響應(yīng)返回的全部?jī)?nèi)容。

雖然這個(gè)例子比較簡(jiǎn)單,而且也能在大多數(shù)瀏覽器中正常運(yùn)行(IE 除外),但管理 Comet 的連接是

很容易出錯(cuò)的,需要時(shí)間不斷改進(jìn)才能達(dá)到完美。瀏覽器社區(qū)認(rèn)為 Comet 是未來 Web 的一個(gè)重要組成

部分,為了簡(jiǎn)化這一技術(shù),又為 Comet 創(chuàng)建了兩個(gè)新的接口。

21.5.4 服務(wù)器發(fā)送事件

SSE(Server-Sent Events,服務(wù)器發(fā)送事件)是圍繞只讀 Comet 交互推出的 API 或者模式。SSE API

用于創(chuàng)建到服務(wù)器的單向連接,服務(wù)器通過這個(gè)連接可以發(fā)送任意數(shù)量的數(shù)據(jù)。服務(wù)器響應(yīng)的 MIME

類型必須是 text/event-stream,而且是瀏覽器中的 JavaScript API 能解析格式輸出。SSE 支持短輪

詢、長(zhǎng)輪詢和 HTTP 流,而且能在斷開連接時(shí)自動(dòng)確定何時(shí)重新連接。有了這么簡(jiǎn)單實(shí)用的 API,再實(shí)

現(xiàn) Comet 就容易多了。

支持 SSE 的瀏覽器有 Firefox 6+、Safari 5+、Opera 11+、Chrome 和 iOS 4+版 Safari。

1. SSE API

SSE 的 JavaScript API 與其他傳遞消息的 JavaScript API 很相似。要預(yù)訂新的事件流,首先要?jiǎng)?chuàng)建一

個(gè)新的 EventSource 對(duì)象,并傳進(jìn)一個(gè)入口點(diǎn):

var source = new EventSource(\"myevents.php\");

注意,傳入的 URL 必須與創(chuàng)建對(duì)象的頁面同源(相同的 URL 模式、域及端口)。EventSource 的

實(shí)例有一個(gè) readyState 屬性,值為 0 表示正連接到服務(wù)器,值為 1 表示打開了連接,值為 2 表示關(guān)閉

了連接。

另外,還有以下三個(gè)事件。

? open:在建立連接時(shí)觸發(fā)。

? message:在從服務(wù)器接收到新事件時(shí)觸發(fā)。

? error:在無法建立連接時(shí)觸發(fā)。

就一般的用法而言,onmessage 事件處理程序也沒有什么特別的。

source.onmessage = function(event){

var data = event.data;

//處理數(shù)據(jù)

};

服務(wù)器發(fā)回的數(shù)據(jù)以字符串形式保存在 event.data 中。

默認(rèn)情況下,EventSource 對(duì)象會(huì)保持與服務(wù)器的活動(dòng)連接。如果連接斷開,還會(huì)重新連接。這

就意味著 SSE 適合長(zhǎng)輪詢和 HTTP 流。如果想強(qiáng)制立即斷開連接并且不再重新連接,可以調(diào)用 close()

方法。

source.close();

2. 事件流

所謂的服務(wù)器事件會(huì)通過一個(gè)持久的 HTTP 響應(yīng)發(fā)送,這個(gè)響應(yīng)的 MIME 類型為 text/event圖靈社區(qū)會(huì)員 StinkBC(StinkBC@gmail.com) 專享 尊重版權(quán)

第609頁

21.5 其他跨域技術(shù) 591

1

15

16

4

5

13

6

20

21

9

10

11

12

stream。響應(yīng)的格式是純文本,最簡(jiǎn)單的情況是每個(gè)數(shù)據(jù)項(xiàng)都帶有前綴 data:,例如:

data: foo

data: bar

data: foo

data: bar

對(duì)以上響應(yīng)而言,事件流中的第一個(gè) message 事件返回的 event.data 值為\"foo\",第二個(gè)

message 事件返回的 event.data 值為\"bar\",第三個(gè) message 事件返回的 event.data 值為

\"foo\

bar\"(注意中間的換行符)。對(duì)于多個(gè)連續(xù)的以 data:開頭的數(shù)據(jù)行,將作為多段數(shù)據(jù)解析,

每個(gè)值之間以一個(gè)換行符分隔。只有在包含 data:的數(shù)據(jù)行后面有空行時(shí),才會(huì)觸發(fā) message 事件,

因此在服務(wù)器上生成事件流時(shí)不能忘了多添加這一行。

通過 id:前綴可以給特定的事件指定一個(gè)關(guān)聯(lián)的 ID,這個(gè) ID 行位于 data:行前面或后面皆可:

data: foo

id: 1

設(shè)置了 ID 后,EventSource 對(duì)象會(huì)跟蹤上一次觸發(fā)的事件。如果連接斷開,會(huì)向服務(wù)器發(fā)送一個(gè)

包含名為 Last-Event-ID 的特殊 HTTP 頭部的請(qǐng)求,以便服務(wù)器知道下一次該觸發(fā)哪個(gè)事件。在多次

連接的事件流中,這種機(jī)制可以確保瀏覽器以正確的順序收到連接的數(shù)據(jù)段。

21.5.5 Web Sockets

要說最令人津津樂道的新瀏覽器 API,就得數(shù) Web Sockets 了。Web Sockets 的目標(biāo)是在一個(gè)單獨(dú)的

持久連接上提供全雙工、雙向通信。在 JavaScript 中創(chuàng)建了 Web Socket 之后,會(huì)有一個(gè) HTTP 請(qǐng)求發(fā)送

到瀏覽器以發(fā)起連接。在取得服務(wù)器響應(yīng)后,建立的連接會(huì)使用 HTTP 升級(jí)從 HTTP 協(xié)議交換為 Web

Socket 協(xié)議。也就是說,使用標(biāo)準(zhǔn)的 HTTP 服務(wù)器無法實(shí)現(xiàn) Web Sockets,只有支持這種協(xié)議的專門服

務(wù)器才能正常工作。

由于 Web Sockets 使用了自定義的協(xié)議,所以 URL 模式也略有不同。未加密的連接不再是 http://,

而是 ws://;加密的連接也不是 https://,而是 wss://。在使用 Web Socket URL 時(shí),必須帶著這個(gè)

模式,因?yàn)閷磉€有可能支持其他模式。

使用自定義協(xié)議而非 HTTP 協(xié)議的好處是,能夠在客戶端和服務(wù)器之間發(fā)送非常少量的數(shù)據(jù),而不

必?fù)?dān)心 HTTP 那樣字節(jié)級(jí)的開銷。由于傳遞的數(shù)據(jù)包很小,因此 Web Sockets 非常適合移動(dòng)應(yīng)用。畢竟

對(duì)移動(dòng)應(yīng)用而言,帶寬和網(wǎng)絡(luò)延遲都是關(guān)鍵問題。使用自定義協(xié)議的缺點(diǎn)在于,制定協(xié)議的時(shí)間比制定

JavaScript API 的時(shí)間還要長(zhǎng)。Web Sockets 曾幾度擱淺,就因?yàn)椴粩嘤腥税l(fā)現(xiàn)這個(gè)新協(xié)議存在一致性和

安全性的問題。Firefox 4 和 Opera 11 都曾默認(rèn)啟用 Web Sockets,但在發(fā)布前夕又禁用了,因?yàn)橛职l(fā)現(xiàn)

了安全隱患。目前支持 Web Sockets 的瀏覽器有 Firefox 6+、Safari 5+、Chrome 和 iOS 4+版 Safari。

1. Web Sockets API

要?jiǎng)?chuàng)建 Web Socket,先實(shí)例一個(gè) WebSocket 對(duì)象并傳入要連接的 URL:

var socket = new WebSocket(\"ws://www.example.com/server.php\");

注意,必須給 WebSocket 構(gòu)造函數(shù)傳入絕對(duì) URL。同源策略對(duì) Web Sockets 不適用,因此可以通

過它打開到任何站點(diǎn)的連接。至于是否會(huì)與某個(gè)域中的頁面通信,則完全取決于服務(wù)器。(通過握手信

息就可以知道請(qǐng)求來自何方。)

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

第610頁

592 第 21 章 Ajax 與 Comet

實(shí)例化了 WebSocket 對(duì)象后,瀏覽器就會(huì)馬上嘗試創(chuàng)建連接。與 XHR 類似,WebSocket 也有一

個(gè)表示當(dāng)前狀態(tài)的 readyState 屬性。不過,這個(gè)屬性的值與 XHR 并不相同,而是如下所示。

? WebSocket.OPENING (0):正在建立連接。

? WebSocket.OPEN (1):已經(jīng)建立連接。

? WebSocket.CLOSING (2):正在關(guān)閉連接。

? WebSocket.CLOSE (3):已經(jīng)關(guān)閉連接。

WebSocket 沒有 readystatechange 事件;不過,它有其他事件,對(duì)應(yīng)著不同的狀態(tài)。readyState

的值永遠(yuǎn)從 0 開始。

要關(guān)閉 Web Socket 連接,可以在任何時(shí)候調(diào)用 close()方法。

socket.close();

調(diào)用了 close()之后,readyState 的值立即變?yōu)?2(正在關(guān)閉),而在關(guān)閉連接后就會(huì)變成 3。

2. 發(fā)送和接收數(shù)據(jù)

Web Socket 打開之后,就可以通過連接發(fā)送和接收數(shù)據(jù)。要向服務(wù)器發(fā)送數(shù)據(jù),使用 send()方法

并傳入任意字符串,例如:

var socket = new WebSocket(\"ws://www.example.com/server.php\");

socket.send(\"Hello world!\");

因?yàn)?Web Sockets 只能通過連接發(fā)送純文本數(shù)據(jù),所以對(duì)于復(fù)雜的數(shù)據(jù)結(jié)構(gòu),在通過連接發(fā)送之前,

必須進(jìn)行序列化。下面的例子展示了先將數(shù)據(jù)序列化為一個(gè) JSON 字符串,然后再發(fā)送到服務(wù)器:

var message = {

time: new Date(),

text: \"Hello world!\",

clientId: \"asdfp8734rew\"

};

socket.send(JSON.stringify(message));

接下來,服務(wù)器要讀取其中的數(shù)據(jù),就要解析接收到的 JSON 字符串。

當(dāng)服務(wù)器向客戶端發(fā)來消息時(shí),WebSocket 對(duì)象就會(huì)觸發(fā) message 事件。這個(gè) message 事件與

其他傳遞消息的協(xié)議類似,也是把返回的數(shù)據(jù)保存在 event.data 屬性中。

socket.onmessage = function(event){

var data = event.data;

//處理數(shù)據(jù)

};

與通過 send()發(fā)送到服務(wù)器的數(shù)據(jù)一樣,event.data 中返回的數(shù)據(jù)也是字符串。如果你想得到

其他格式的數(shù)據(jù),必須手工解析這些數(shù)據(jù)。

3. 其他事件

WebSocket 對(duì)象還有其他三個(gè)事件,在連接生命周期的不同階段觸發(fā)。

? open:在成功建立連接時(shí)觸發(fā)。

? error:在發(fā)生錯(cuò)誤時(shí)觸發(fā),連接不能持續(xù)。

? close:在連接關(guān)閉時(shí)觸發(fā)。

WebSocket 對(duì)象不支持 DOM 2 級(jí)事件偵聽器,因此必須使用 DOM 0 級(jí)語法分別定義每個(gè)事件處

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

第611頁

21.6 安全 593

1

15

16

4

5

13

6

20

21

9

10

11

12

理程序。

var socket = new WebSocket(\"ws://www.example.com/server.php\");

socket.onopen = function(){

alert(\"Connection established.\");

};

socket.onerror = function(){

alert(\"Connection error.\");

};

socket.onclose = function(){

alert(\"Connection closed.\");

};

在這三個(gè)事件中,只有 close 事件的 event 對(duì)象有額外的信息。這個(gè)事件的事件對(duì)象有三個(gè)額外

的屬性:wasClean、code 和 reason。其中,wasClean 是一個(gè)布爾值,表示連接是否已經(jīng)明確地關(guān)

閉;code 是服務(wù)器返回的數(shù)值狀態(tài)碼;而 reason 是一個(gè)字符串,包含服務(wù)器發(fā)回的消息??梢园堰@

些信息顯示給用戶,也可以記錄到日志中以便將來分析。

socket.onclose = function(event){

console.log(\"Was clean? \" + event.wasClean + \" Code=\" + event.code + \" Reason=\"

+ event.reason);

};

21.5.6 SSE與Web Sockets

面對(duì)某個(gè)具體的用例,在考慮是使用 SSE 還是使用 Web Sockets 時(shí),可以考慮如下幾個(gè)因素。首先,

你是否有自由度建立和維護(hù) Web Sockets 服務(wù)器?因?yàn)?Web Socket 協(xié)議不同于 HTTP,所以現(xiàn)有服務(wù)器

不能用于 Web Socket 通信。SSE 倒是通過常規(guī) HTTP 通信,因此現(xiàn)有服務(wù)器就可以滿足需求。

第二個(gè)要考慮的問題是到底需不需要雙向通信。如果用例只需讀取服務(wù)器數(shù)據(jù)(如比賽成績(jī)),那

么 SSE 比較容易實(shí)現(xiàn)。如果用例必須雙向通信(如聊天室),那么 Web Sockets 顯然更好。別忘了,在

不能選擇 Web Sockets 的情況下,組合 XHR 和 SSE 也是能實(shí)現(xiàn)雙向通信的。

21.6 安全

討論 Ajax 和 Comet 安全的文章可謂連篇累牘,而相關(guān)主題的書也已經(jīng)出了很多本了。大型 Ajax 應(yīng)

用程序的安全問題涉及面非常之廣,但我們可以從普遍意義上探討一些基本的問題。

首先,可以通過 XHR 訪問的任何 URL 也可以通過瀏覽器或服務(wù)器來訪問。下面的 URL 就是一個(gè)

例子。

/getuserinfo.php?id=23

如果是向這個(gè) URL 發(fā)送請(qǐng)求,可以想象結(jié)果會(huì)返回 ID 為 23 的用戶的某些數(shù)據(jù)。誰也無法保證別

人不會(huì)將這個(gè) URL 的用戶 ID 修改為 24、56 或其他值。因此,getuserinfo.php 文件必須知道請(qǐng)求者

是否真的有權(quán)限訪問要請(qǐng)求的數(shù)據(jù);否則,你的服務(wù)器就會(huì)門戶大開,任何人的數(shù)據(jù)都可能被泄漏出去。

對(duì)于未被授權(quán)系統(tǒng)有權(quán)訪問某個(gè)資源的情況,我們稱之為 CSRF(Cross-Site Request Forgery,跨站

點(diǎn)請(qǐng)求偽造)。未被授權(quán)系統(tǒng)會(huì)偽裝自己,讓處理請(qǐng)求的服務(wù)器認(rèn)為它是合法的。受到 CSRF 攻擊的 Ajax

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

第612頁

594 第 21 章 Ajax 與 Comet

程序有大有小,攻擊行為既有旨在揭示系統(tǒng)漏洞的惡作劇,也有惡意的數(shù)據(jù)竊取或數(shù)據(jù)銷毀。

為確保通過 XHR 訪問的 URL 安全,通行的做法就是驗(yàn)證發(fā)送請(qǐng)求者是否有權(quán)限訪問相應(yīng)的資源。

有下列幾種方式可供選擇。

? 要求以 SSL 連接來訪問可以通過 XHR 請(qǐng)求的資源。

? 要求每一次請(qǐng)求都要附帶經(jīng)過相應(yīng)算法計(jì)算得到的驗(yàn)證碼。

請(qǐng)注意,下列措施對(duì)防范 CSRF 攻擊不起作用。

? 要求發(fā)送 POST 而不是 GET 請(qǐng)求——很容易改變。

? 檢查來源 URL 以確定是否可信——來源記錄很容易偽造。

? 基于 cookie 信息進(jìn)行驗(yàn)證——同樣很容易偽造。

XHR 對(duì)象也提供了一些安全機(jī)制,雖然表面上看可以保證安全,但實(shí)際上卻相當(dāng)不可靠。實(shí)際上,

前面介紹的 open()方法還能再接收兩個(gè)參數(shù):要隨請(qǐng)求一起發(fā)送的用戶名和密碼。帶有這兩個(gè)參數(shù)的

請(qǐng)求可以通過 SSL 發(fā)送給服務(wù)器上的頁面,如下面的例子所示。

xhr.open(\"get\", \"example.php\", true, \"username\", \"password\"); //不要這樣做?。?/p>

即便可以考慮這種安全機(jī)制,但還是盡量不要這樣做。把用戶名和密碼保存在

JavaScript 代碼中本身就是極為不安全的。任何人,只要他會(huì)使用 JavaScript 調(diào)試器,

就可以通過查看相應(yīng)的變量發(fā)現(xiàn)純文本形式的用戶名和密碼。

21.7 小結(jié)

Ajax 是無需刷新頁面就能夠從服務(wù)器取得數(shù)據(jù)的一種方法。關(guān)于 Ajax,可以從以下幾方面來總結(jié)

一下。

? 負(fù)責(zé) Ajax 運(yùn)作的核心對(duì)象是 XMLHttpRequest(XHR)對(duì)象。

? XHR 對(duì)象由微軟最早在 IE5 中引入,用于通過 JavaScript 從服務(wù)器取得 XML 數(shù)據(jù)。

? 在此之后,F(xiàn)irefox、Safari、Chrome 和 Opera 都實(shí)現(xiàn)了相同的特性,使 XHR 成為了 Web 的一個(gè)

事實(shí)標(biāo)準(zhǔn)。

? 雖然實(shí)現(xiàn)之間存在差異,但 XHR 對(duì)象的基本用法在不同瀏覽器間還是相對(duì)規(guī)范的,因此可以放

心地用在 Web 開發(fā)當(dāng)中。

同源策略是對(duì) XHR 的一個(gè)主要約束,它為通信設(shè)置了“相同的域、相同的端口、相同的協(xié)議”這一

限制。試圖訪問上述限制之外的資源,都會(huì)引發(fā)安全錯(cuò)誤,除非采用被認(rèn)可的跨域解決方案。這個(gè)解決

方案叫做 CORS(Cross-Origin Resource Sharing,跨源資源共享),IE8 通過 XDomainRequest 對(duì)象支持

CORS,其他瀏覽器通過 XHR 對(duì)象原生支持 CORS。圖像 Ping 和 JSONP 是另外兩種跨域通信的技術(shù),

但不如 CORS 穩(wěn)妥。

Comet 是對(duì) Ajax 的進(jìn)一步擴(kuò)展,讓服務(wù)器幾乎能夠?qū)崟r(shí)地向客戶端推送數(shù)據(jù)。實(shí)現(xiàn) Comet 的手段

主要有兩個(gè):長(zhǎng)輪詢和 HTTP 流。所有瀏覽器都支持長(zhǎng)輪詢,而只有部分瀏覽器原生支持 HTTP 流。SSE

(Server-Sent Events,服務(wù)器發(fā)送事件)是一種實(shí)現(xiàn) Comet 交互的瀏覽器 API,既支持長(zhǎng)輪詢,也支持

HTTP 流。

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

第613頁

21.7 小結(jié) 595

1

15

16

4

5

13

6

20

21

9

10

11

12

Web Sockets 是一種與服務(wù)器進(jìn)行全雙工、雙向通信的信道。與其他方案不同,Web Sockets 不使用

HTTP 協(xié)議,而使用一種自定義的協(xié)議。這種協(xié)議專門為快速傳輸小數(shù)據(jù)設(shè)計(jì)。雖然要求使用不同的

Web 服務(wù)器,但卻具有速度上的優(yōu)勢(shì)。

各方面對(duì) Ajax 和 Comet 的鼓吹吸引了越來越多的開發(fā)人員學(xué)習(xí) JavaScript,人們對(duì) Web 開發(fā)的關(guān)注

也再度升溫。與 Ajax 有關(guān)的概念都還相對(duì)比較新,這些概念會(huì)隨著時(shí)間推移繼續(xù)發(fā)展。

Ajax 是一個(gè)非常龐大的主題,完整地討論這個(gè)主題超出了本書的范圍。要想了解

有關(guān) Ajax 的更多信息,請(qǐng)讀者參考《Ajax 高級(jí)程序設(shè)計(jì)(第 2 版)》。

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

第614頁

596 第 22 章 高級(jí)技巧

高 級(jí) 技 巧

本章內(nèi)容

? 使用高級(jí)函數(shù)

? 防篡改對(duì)象

? Yielding Timers

avaScript 是一種極其靈活的語言,具有多種使用風(fēng)格。一般來說,編寫 JavaScript 要么使用過

程方式,要么使用面向?qū)ο蠓绞?。然而,由于它天生的?dòng)態(tài)屬性,這種語言還能使用更為復(fù)雜

和有趣的模式。這些技巧要利用 ECMAScript 的語言特點(diǎn)、BOM 擴(kuò)展和 DOM 功能來獲得強(qiáng)大的效果。

22.1 高級(jí)函數(shù)

函數(shù)是 JavaScript 中最有趣的部分之一。它們本質(zhì)上是十分簡(jiǎn)單和過程化的,但也可以是非常復(fù)雜

和動(dòng)態(tài)的。一些額外的功能可以通過使用閉包來實(shí)現(xiàn)。此外,由于所有的函數(shù)都是對(duì)象,所以使用函數(shù)

指針非常簡(jiǎn)單。這些令 JavaScript 函數(shù)不僅有趣而且強(qiáng)大。以下幾節(jié)描繪了幾種在 JavaScript 中使用函數(shù)

的高級(jí)方法。

22.1.1 安全的類型檢測(cè)

JavaScript 內(nèi)置的類型檢測(cè)機(jī)制并非完全可靠。事實(shí)上,發(fā)生錯(cuò)誤否定及錯(cuò)誤肯定的情況也不在少

數(shù)。比如說 typeof 操作符吧,由于它有一些無法預(yù)知的行為,經(jīng)常會(huì)導(dǎo)致檢測(cè)數(shù)據(jù)類型時(shí)得到不靠譜

的結(jié)果。Safari(直至第 4 版)在對(duì)正則表達(dá)式應(yīng)用 typeof 操作符時(shí)會(huì)返回\"function\",因此很難確

定某個(gè)值到底是不是函數(shù)。

再比如,instanceof 操作符在存在多個(gè)全局作用域(像一個(gè)頁面包含多個(gè) frame)的情況下,也

是問題多多。一個(gè)經(jīng)典的例子(第 5 章也提到過)就是像下面這樣將對(duì)象標(biāo)識(shí)為數(shù)組。

var isArray = value instanceof Array;

以上代碼要返回 true,value 必須是一個(gè)數(shù)組,而且還必須與 Array 構(gòu)造函數(shù)在同個(gè)全局作用域

中。(別忘了,Array 是 window 的屬性。)如果 value 是在另個(gè) frame 中定義的數(shù)組,那么以上代碼

就會(huì)返回 false。

在檢測(cè)某個(gè)對(duì)象到底是原生對(duì)象還是開發(fā)人員自定義的對(duì)象的時(shí)候,也會(huì)有問題。出現(xiàn)這個(gè)問題的

原因是瀏覽器開始原生支持 JSON 對(duì)象了。因?yàn)楹芏嗳艘恢痹谑褂?Douglas Crockford 的 JSON 庫,而該

庫定義了一個(gè)全局 JSON 對(duì)象。于是開發(fā)人員很難確定頁面中的 JSON 對(duì)象到底是不是原生的。

解決上述問題的辦法都一樣。大家知道,在任何值上調(diào)用 Object 原生的 toString()方法,都會(huì)

J

第 22 章

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

第615頁

22.1 高級(jí)函數(shù) 597

14

2

3

17

18

13

19

7

8

22

10

11

12

返回一個(gè)[object NativeConstructorName]格式的字符串。每個(gè)類在內(nèi)部都有一個(gè)[[Class]]屬

性,這個(gè)屬性中就指定了上述字符串中的構(gòu)造函數(shù)名。舉個(gè)例子吧。

alert(Object.prototype.toString.call(value)); //\"[object Array]\"

由于原生數(shù)組的構(gòu)造函數(shù)名與全局作用域無關(guān),因此使用 toString()就能保證返回一致的值。利

用這一點(diǎn),可以創(chuàng)建如下函數(shù):

function isArray(value){

return Object.prototype.toString.call(value) == \"[object Array]\";

}

同樣,也可以基于這一思路來測(cè)試某個(gè)值是不是原生函數(shù)或正則表達(dá)式:

function isFunction(value){

return Object.prototype.toString.call(value) == \"[object Function]\";

}

function isRegExp(value){

return Object.prototype.toString.call(value) == \"[object RegExp]\";

}

不過要注意,對(duì)于在 IE 中以 COM 對(duì)象形式實(shí)現(xiàn)的任何函數(shù),isFunction()都將返回 false(因

為它們并非原生的 JavaScript 函數(shù),請(qǐng)參考第 10 章中更詳細(xì)的介紹)。

這一技巧也廣泛應(yīng)用于檢測(cè)原生 JSON 對(duì)象。Object 的 toString()方法不能檢測(cè)非原生構(gòu)造函

數(shù)的構(gòu)造函數(shù)名。因此,開發(fā)人員定義的任何構(gòu)造函數(shù)都將返回[object Object]。有些 JavaScript 庫會(huì)包

含與下面類似的代碼。

var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) ==

\"[object JSON]\";

在 Web 開發(fā)中能夠區(qū)分原生與非原生 JavaScript 對(duì)象非常重要。只有這樣才能確切知道某個(gè)對(duì)象到

底有哪些功能。這個(gè)技巧可以對(duì)任何對(duì)象給出正確的結(jié)論。

請(qǐng)注意,Object.prototpye.toString()本身也可能會(huì)被修改。本節(jié)討論的

技巧假設(shè) Object.prototpye.toString()是未被修改過的原生版本。

22.1.2 作用域安全的構(gòu)造函數(shù)

第 6 章講述了用于自定義對(duì)象的構(gòu)造函數(shù)的定義和用法。你應(yīng)該還記得,構(gòu)造函數(shù)其實(shí)就是一個(gè)使

用 new 操作符調(diào)用的函數(shù)。當(dāng)使用 new 調(diào)用時(shí),構(gòu)造函數(shù)內(nèi)用到的 this 對(duì)象會(huì)指向新創(chuàng)建的對(duì)象實(shí)

例,如下面的例子所示:

function Person(name, age, job){

this.name = name;

this.age = age;

this.job = job;

}

var person = new Person(\"Nicholas\", 29, \"Software Engineer\");

ScopeSafeConstructorsExample01.htm

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

第616頁

598 第 22 章 高級(jí)技巧

上面這個(gè)例子中,Person 構(gòu)造函數(shù)使用 this 對(duì)象給三個(gè)屬性賦值:name、age 和 job。當(dāng)和 new

操作符連用時(shí),則會(huì)創(chuàng)建一個(gè)新的 Person 對(duì)象,同時(shí)會(huì)給它分配這些屬性。問題出在當(dāng)沒有使用 new

操作符來調(diào)用該構(gòu)造函數(shù)的情況上。由于該 this 對(duì)象是在運(yùn)行時(shí)綁定的,所以直接調(diào)用 Person(),

this 會(huì)映射到全局對(duì)象 window 上,導(dǎo)致錯(cuò)誤對(duì)象屬性的意外增加。例如:

var person = Person(\"Nicholas\", 29, \"Software Engineer\");

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

alert(window.age); //29

alert(window.job); //\"Software Engineer\"

ScopeSafeConstructorsExample01.htm

這里,原本針對(duì) Person 實(shí)例的三個(gè)屬性被加到 window 對(duì)象上,因?yàn)闃?gòu)造函數(shù)是作為普通函數(shù)調(diào)

用的,忽略了 new 操作符。這個(gè)問題是由 this 對(duì)象的晚綁定造成的,在這里 this 被解析成了 window

對(duì)象。由于 window 的 name 屬性是用于識(shí)別鏈接目標(biāo)和 frame 的,所以這里對(duì)該屬性的偶然覆蓋可能

會(huì)導(dǎo)致該頁面上出現(xiàn)其他錯(cuò)誤。這個(gè)問題的解決方法就是創(chuàng)建一個(gè)作用域安全的構(gòu)造函數(shù)。

作用域安全的構(gòu)造函數(shù)在進(jìn)行任何更改前,首先確認(rèn) this 對(duì)象是正確類型的實(shí)例。如果不是,那

么會(huì)創(chuàng)建新的實(shí)例并返回。請(qǐng)看以下例子:

function Person(name, age, job){

if (this instanceof Person){

this.name = name;

this.age = age;

this.job = job;

} else {

return new Person(name, age, job);

}

}

var person1 = Person(\"Nicholas\", 29, \"Software Engineer\");

alert(window.name); //\"\"

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

var person2 = new Person(\"Shelby\", 34, \"Ergonomist\");

alert(person2.name); //\"Shelby\"

ScopeSafeConstructorsExample02.htm

這段代碼中的 Person 構(gòu)造函數(shù)添加了一個(gè)檢查并確保 this 對(duì)象是 Person 實(shí)例的 if 語句,它

表示要么使用 new 操作符,要么在現(xiàn)有的 Person 實(shí)例環(huán)境中調(diào)用構(gòu)造函數(shù)。任何一種情況下,對(duì)象初

始化都能正常進(jìn)行。如果 this 并非 Person 的實(shí)例,那么會(huì)再次使用 new 操作符調(diào)用構(gòu)造函數(shù)并返回

結(jié)果。最后的結(jié)果是,調(diào)用 Person 構(gòu)造函數(shù)時(shí)無論是否使用 new 操作符,都會(huì)返回一個(gè) Person 的新

實(shí)例,這就避免了在全局對(duì)象上意外設(shè)置屬性。

關(guān)于作用域安全的構(gòu)造函數(shù)的貼心提示。實(shí)現(xiàn)這個(gè)模式后,你就鎖定了可以調(diào)用構(gòu)造函數(shù)的環(huán)境。

如果你使用構(gòu)造函數(shù)竊取模式的繼承且不使用原型鏈,那么這個(gè)繼承很可能被破壞。這里有個(gè)例子:

function Polygon(sides){

if (this instanceof Polygon) {

this.sides = sides;

this.getArea = function(){

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

第617頁

22.1 高級(jí)函數(shù) 599

14

2

3

17

18

13

19

7

8

22

10

11

12

return 0;

};

} else {

return new Polygon(sides);

}

}

function Rectangle(width, height){

Polygon.call(this, 2);

this.width = width;

this.height = height;

this.getArea = function(){

return this.width * this.height;

};

}

var rect = new Rectangle(5, 10);

alert(rect.sides); //undefined

ScopeSafeConstructorsExample03.htm

在這段代碼中,Polygon 構(gòu)造函數(shù)是作用域安全的,然而 Rectangle 構(gòu)造函數(shù)則不是。新創(chuàng)建一

個(gè) Rectangle 實(shí)例之后,這個(gè)實(shí)例應(yīng)該通過 Polygon.call()來繼承 Polygon 的 sides 屬性。但是,

由于 Polygon 構(gòu)造函數(shù)是作用域安全的,this 對(duì)象并非 Polygon 的實(shí)例,所以會(huì)創(chuàng)建并返回一個(gè)新

的 Polygon 對(duì)象。Rectangle 構(gòu)造函數(shù)中的 this 對(duì)象并沒有得到增長(zhǎng),同時(shí) Polygon.call()返回

的值也沒有用到,所以 Rectangle 實(shí)例中就不會(huì)有 sides 屬性。

如果構(gòu)造函數(shù)竊取結(jié)合使用原型鏈或者寄生組合則可以解決這個(gè)問題??紤]以下例子:

function Polygon(sides){

if (this instanceof Polygon) {

this.sides = sides;

this.getArea = function(){

return 0;

};

} else {

return new Polygon(sides);

}

}

function Rectangle(width, height){

Polygon.call(this, 2);

this.width = width;

this.height = height;

this.getArea = function(){

return this.width * this.height;

};

}

Rectangle.prototype = new Polygon();

var rect = new Rectangle(5, 10);

alert(rect.sides); //2

ScopeSafeConstructorsExample04.htm

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

第618頁

600 第 22 章 高級(jí)技巧

上面這段重寫的代碼中,一個(gè)Rectangle實(shí)例也同時(shí)是一個(gè)Polygon實(shí)例,所以Polygon.call()

會(huì)照原意執(zhí)行,最終為 Rectangle 實(shí)例添加了 sides 屬性。

多個(gè)程序員在同一個(gè)頁面上寫 JavaScript 代碼的環(huán)境中,作用域安全構(gòu)造函數(shù)就很有用了。屆時(shí),

對(duì)全局對(duì)象意外的更改可能會(huì)導(dǎo)致一些常常難以追蹤的錯(cuò)誤。除非你單純基于構(gòu)造函數(shù)竊取來實(shí)現(xiàn)繼

承,推薦作用域安全的構(gòu)造函數(shù)作為最佳實(shí)踐。

22.1.3 惰性載入函數(shù)

因?yàn)闉g覽器之間行為的差異,多數(shù) JavaScript 代碼包含了大量的 if 語句,將執(zhí)行引導(dǎo)到正確的代

碼中??纯聪旅鎭碜陨弦徽碌?createXHR()函數(shù)。

function createXHR(){

if (typeof XMLHttpRequest != \"undefined\"){

return new XMLHttpRequest();

} else if (typeof ActiveXObject != \"undefined\"){

if (typeof arguments.callee.activeXString != \"string\"){

var versions = [\"MSXML2.XMLHttp.6.0\", \"MSXML2.XMLHttp.3.0\",

\"MSXML2.XMLHttp\"],

i,len;

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

try {

new ActiveXObject(versions[i]);

arguments.callee.activeXString = versions[i];

break;

} catch (ex){

//跳過

}

}

}

return new ActiveXObject(arguments.callee.activeXString);

} else {

throw new Error(\"No XHR object available.\");

}

}

每次調(diào)用 createXHR()的時(shí)候,它都要對(duì)瀏覽器所支持的能力仔細(xì)檢查。首先檢查內(nèi)置的 XHR,

然后測(cè)試有沒有基于 ActiveX 的 XHR,最后如果都沒有發(fā)現(xiàn)的話就拋出一個(gè)錯(cuò)誤。每次調(diào)用該函數(shù)都是

這樣,即使每次調(diào)用時(shí)分支的結(jié)果都不變:如果瀏覽器支持內(nèi)置 XHR,那么它就一直支持了,那么這

種測(cè)試就變得沒必要了。即使只有一個(gè) if 語句的代碼,也肯定要比沒有 if 語句的慢,所以如果 if 語

句不必每次執(zhí)行,那么代碼可以運(yùn)行地更快一些。解決方案就是稱之為惰性載入的技巧。

惰性載入表示函數(shù)執(zhí)行的分支僅會(huì)發(fā)生一次。有兩種實(shí)現(xiàn)惰性載入的方式,第一種就是在函數(shù)被調(diào)

用時(shí)再處理函數(shù)。在第一次調(diào)用的過程中,該函數(shù)會(huì)被覆蓋為另外一個(gè)按合適方式執(zhí)行的函數(shù),這樣任

何對(duì)原函數(shù)的調(diào)用都不用再經(jīng)過執(zhí)行的分支了。例如,可以用下面的方式使用惰性載入重寫

createXHR()。

function createXHR(){

if (typeof XMLHttpRequest != \"undefined\"){

createXHR = function(){

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

第619頁

22.1 高級(jí)函數(shù) 601

14

2

3

17

18

13

19

7

8

22

10

11

12

return new XMLHttpRequest();

};

} else if (typeof ActiveXObject != \"undefined\"){

createXHR = function(){

if (typeof arguments.callee.activeXString != \"string\"){

var versions = [\"MSXML2.XMLHttp.6.0\", \"MSXML2.XMLHttp.3.0\",

\"MSXML2.XMLHttp\"],

i, len;

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

try {

new ActiveXObject(versions[i]);

arguments.callee.activeXString = versions[i];

break;

} catch (ex){

//skip

}

}

}

return new ActiveXObject(arguments.callee.activeXString);

};

} else {

createXHR = function(){

throw new Error(\"No XHR object available.\");

};

}

return createXHR();

}

LazyLoadingExample01.htm

在這個(gè)惰性載入的 createXHR()中,if 語句的每一個(gè)分支都會(huì)為 createXHR 變量賦值,有效覆

蓋了原有的函數(shù)。最后一步便是調(diào)用新賦的函數(shù)。下一次調(diào)用 createXHR()的時(shí)候,就會(huì)直接調(diào)用被

分配的函數(shù),這樣就不用再次執(zhí)行 if 語句了。

第二種實(shí)現(xiàn)惰性載入的方式是在聲明函數(shù)時(shí)就指定適當(dāng)?shù)暮瘮?shù)。這樣,第一次調(diào)用函數(shù)時(shí)就不會(huì)損

失性能了,而在代碼首次加載時(shí)會(huì)損失一點(diǎn)性能。以下就是按照這一思路重寫前面例子的結(jié)果。

var createXHR = (function(){

if (typeof XMLHttpRequest != \"undefined\"){

return function(){

return new XMLHttpRequest();

};

} else if (typeof ActiveXObject != \"undefined\"){

return function(){

if (typeof arguments.callee.activeXString != \"string\"){

var versions = [\"MSXML2.XMLHttp.6.0\", \"MSXML2.XMLHttp.3.0\",

\"MSXML2.XMLHttp\"],

i, len;

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

try {

new ActiveXObject(versions[i]);

arguments.callee.activeXString = versions[i];

break;

} catch (ex){

//skip

}

}

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

第620頁

602 第 22 章 高級(jí)技巧

}

return new ActiveXObject(arguments.callee.activeXString);

};

} else {

return function(){

throw new Error(\"No XHR object available.\");

};

}

})();

LazyLoadingExample02.htm

這個(gè)例子中使用的技巧是創(chuàng)建一個(gè)匿名、自執(zhí)行的函數(shù),用以確定應(yīng)該使用哪一個(gè)函數(shù)實(shí)現(xiàn)。實(shí)際

的邏輯都一樣。不一樣的地方就是第一行代碼(使用 var 定義函數(shù))、新增了自執(zhí)行的匿名函數(shù),另外

每個(gè)分支都返回正確的函數(shù)定義,以便立即將其賦值給 createXHR()。

惰性載入函數(shù)的優(yōu)點(diǎn)是只在執(zhí)行分支代碼時(shí)犧牲一點(diǎn)兒性能。至于哪種方式更合適,就要看你的具

體需求而定了。不過這兩種方式都能避免執(zhí)行不必要的代碼。

22.1.4 函數(shù)綁定

另一個(gè)日益流行的高級(jí)技巧叫做函數(shù)綁定。函數(shù)綁定要?jiǎng)?chuàng)建一個(gè)函數(shù),可以在特定的 this 環(huán)境中

以指定參數(shù)調(diào)用另一個(gè)函數(shù)。該技巧常常和回調(diào)函數(shù)與事件處理程序一起使用,以便在將函數(shù)作為變量

傳遞的同時(shí)保留代碼執(zhí)行環(huán)境。請(qǐng)看以下例子:

var handler = {

message: \"Event handled\",

handleClick: function(event){

alert(this.message);

}

};

var btn = document.getElementById(\"my-btn\");

EventUtil.addHandler(btn, \"click\", handler.handleClick);

在上面這個(gè)例子中,創(chuàng)建了一個(gè)叫做 handler 的對(duì)象。handler.handleClick()方法被分配為

一個(gè) DOM 按鈕的事件處理程序。當(dāng)按下該按鈕時(shí),就調(diào)用該函數(shù),顯示一個(gè)警告框。雖然貌似警告框

應(yīng)該顯示 Event handled ,然而實(shí)際上顯示的是 undefiend 。這個(gè)問題在于沒有保存

handler.handleClick()的環(huán)境,所以 this 對(duì)象最后是指向了 DOM 按鈕而非 handler(在 IE8 中,

this 指向 window。)可以如下面例子所示,使用一個(gè)閉包來修正這個(gè)問題。

var handler = {

message: \"Event handled\",

handleClick: function(event){

alert(this.message);

}

};

var btn = document.getElementById(\"my-btn\");

EventUtil.addHandler(btn, \"click\", function(event){

handler.handleClick(event);

});

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

第621頁

22.1 高級(jí)函數(shù) 603

14

2

3

17

18

13

19

7

8

22

10

11

12

這個(gè)解決方案在 onclick 事件處理程序內(nèi)使用了一個(gè)閉包直接調(diào)用 handler.handleClick()。

當(dāng)然,這是特定于這段代碼的解決方案。創(chuàng)建多個(gè)閉包可能會(huì)令代碼變得難于理解和調(diào)試。因此,很多

JavaScript 庫實(shí)現(xiàn)了一個(gè)可以將函數(shù)綁定到指定環(huán)境的函數(shù)。這個(gè)函數(shù)一般都叫 bind()。

一個(gè)簡(jiǎn)單的 bind()函數(shù)接受一個(gè)函數(shù)和一個(gè)環(huán)境,并返回一個(gè)在給定環(huán)境中調(diào)用給定函數(shù)的函數(shù),

并且將所有參數(shù)原封不動(dòng)傳遞過去。語法如下:

function bind(fn, context){

return function(){

return fn.apply(context, arguments);

};

}

FunctionBindingExample01.htm

這個(gè)函數(shù)似乎簡(jiǎn)單,但其功能是非常強(qiáng)大的。在 bind()中創(chuàng)建了一個(gè)閉包,閉包使用 apply()調(diào)

用傳入的函數(shù),并給 apply()傳遞 context 對(duì)象和參數(shù)。注意這里使用的 arguments 對(duì)象是內(nèi)部函

數(shù)的,而非 bind()的。當(dāng)調(diào)用返回的函數(shù)時(shí),它會(huì)在給定環(huán)境中執(zhí)行被傳入的函數(shù)并給出所有參數(shù)。

bind()函數(shù)按如下方式使用:

var handler = {

message: \"Event handled\",

handleClick: function(event){

alert(this.message);

}

};

var btn = document.getElementById(\"my-btn\");

EventUtil.addHandler(btn, \"click\", bind(handler.handleClick, handler));

FunctionBindingExample01.htm

在這個(gè)例子中,我們用 bind()函數(shù)創(chuàng)建了一個(gè)保持了執(zhí)行環(huán)境的函數(shù),并將其傳給 EventUtil.

addHandler()。event 對(duì)象也被傳給了該函數(shù),如下所示:

var handler = {

message: \"Event handled\",

handleClick: function(event){

alert(this.message + \":\" + event.type);

}

};

var btn = document.getElementById(\"my-btn\");

EventUtil.addHandler(btn, \"click\", bind(handler.handleClick, handler));

FunctionBindingExample01.htm

handler.handleClick()方法和平時(shí)一樣獲得了 event 對(duì)象,因?yàn)樗械膮?shù)都通過被綁定的函

數(shù)直接傳給了它。

ECMAScript 5 為所有函數(shù)定義了一個(gè)原生的 bind()方法,進(jìn)一步簡(jiǎn)單了操作。換句話說,你不用

再自己定義 bind()函數(shù)了,而是可以直接在函數(shù)上調(diào)用這個(gè)方法。例如:

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

第622頁

604 第 22 章 高級(jí)技巧

var handler = {

message: \"Event handled\",

handleClick: function(event){

alert(this.message + \":\" + event.type);

}

};

var btn = document.getElementById(\"my-btn\");

EventUtil.addHandler(btn, \"click\", handler.handleClick.bind(handler));

FunctionBindingExample02.htm

原生的 bind()方法與前面介紹的自定義 bind()方法類似,都是要傳入作為 this 值的對(duì)象。支持

原生 bind()方法的瀏覽器有 IE9+、Firefox 4+和 Chrome。

只要是將某個(gè)函數(shù)指針以值的形式進(jìn)行傳遞,同時(shí)該函數(shù)必須在特定環(huán)境中執(zhí)行,被綁定函數(shù)的效

用就突顯出來了。它們主要用于事件處理程序以及 setTimeout() 和 setInterval()。然而,被綁

定函數(shù)與普通函數(shù)相比有更多的開銷,它們需要更多內(nèi)存,同時(shí)也因?yàn)槎嘀睾瘮?shù)調(diào)用稍微慢一點(diǎn),所

以最好只在必要時(shí)使用。

22.1.5 函數(shù)柯里化

與函數(shù)綁定緊密相關(guān)的主題是函數(shù)柯里化(function currying),它用于創(chuàng)建已經(jīng)設(shè)置好了一個(gè)或多

個(gè)參數(shù)的函數(shù)。函數(shù)柯里化的基本方法和函數(shù)綁定是一樣的:使用一個(gè)閉包返回一個(gè)函數(shù)。兩者的區(qū)別

在于,當(dāng)函數(shù)被調(diào)用時(shí),返回的函數(shù)還需要設(shè)置一些傳入的參數(shù)。請(qǐng)看以下例子。

function add(num1, num2){

return num1 + num2;

}

function curriedAdd(num2){

return add(5, num2);

}

alert(add(2, 3)); //5

alert(curriedAdd(3)); //8

這段代碼定義了兩個(gè)函數(shù):add()和 curriedAdd()。后者本質(zhì)上是在任何情況下第一個(gè)參數(shù)為 5

的 add()版本。盡管從技術(shù)上來說 curriedAdd()并非柯里化的函數(shù),但它很好地展示了其概念。

柯里化函數(shù)通常由以下步驟動(dòng)態(tài)創(chuàng)建:調(diào)用另一個(gè)函數(shù)并為它傳入要柯里化的函數(shù)和必要參數(shù)。下

面是創(chuàng)建柯里化函數(shù)的通用方式。

function curry(fn){

var args = Array.prototype.slice.call(arguments, 1);

return function(){

var innerArgs = Array.prototype.slice.call(arguments);

var finalArgs = args.concat(innerArgs);

return fn.apply(null, finalArgs);

};

}

FunctionCurryingExample01.htm

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

第623頁

22.1 高級(jí)函數(shù) 605

14

2

3

17

18

13

19

7

8

22

10

11

12

curry()函數(shù)的主要工作就是將被返回函數(shù)的參數(shù)進(jìn)行排序。curry()的第一個(gè)參數(shù)是要進(jìn)行柯里

化的函數(shù),其他參數(shù)是要傳入的值。為了獲取第一個(gè)參數(shù)之后的所有參數(shù),在 arguments 對(duì)象上調(diào)用

了 slice()方法,并傳入?yún)?shù) 1 表示被返回的數(shù)組包含從第二個(gè)參數(shù)開始的所有參數(shù)。然后 args 數(shù)組

包含了來自外部函數(shù)的參數(shù)。在內(nèi)部函數(shù)中,創(chuàng)建了 innerArgs 數(shù)組用來存放所有傳入的參數(shù)(又一

次用到了 slice())。有了存放來自外部函數(shù)和內(nèi)部函數(shù)的參數(shù)數(shù)組后,就可以使用 concat()方法將

它們組合為 finalArgs,然后使用 apply()將結(jié)果傳遞給該函數(shù)。注意這個(gè)函數(shù)并沒有考慮到執(zhí)行環(huán)

境,所以調(diào)用 apply()時(shí)第一個(gè)參數(shù)是 null。curry()函數(shù)可以按以下方式應(yīng)用。

function add(num1, num2){

return num1 + num2;

}

var curriedAdd = curry(add, 5);

alert(curriedAdd(3)); //8

FunctionCurryingExample01.htm

在這個(gè)例子中,創(chuàng)建了第一個(gè)參數(shù)綁定為 5 的 add()的柯里化版本。當(dāng)調(diào)用 curriedAdd()并傳

入 3 時(shí),3 會(huì)成為 add()的第二個(gè)參數(shù),同時(shí)第一個(gè)參數(shù)依然是 5,最后結(jié)果便是和 8。你也可以像下

面例子這樣給出所有的函數(shù)參數(shù):

function add(num1, num2){

return num1 + num2;

}

var curriedAdd = curry(add, 5, 12);

alert(curriedAdd()); //17

FunctionCurryingExample01.htm

在這里,柯里化的 add()函數(shù)兩個(gè)參數(shù)都提供了,所以以后就無需再傳遞它們了。

函數(shù)柯里化還常常作為函數(shù)綁定的一部分包含在其中,構(gòu)造出更為復(fù)雜的 bind()函數(shù)。例如:

function bind(fn, context){

var args = Array.prototype.slice.call(arguments, 2);

return function(){

var innerArgs = Array.prototype.slice.call(arguments);

var finalArgs = args.concat(innerArgs);

return fn.apply(context, finalArgs);

};

}

FunctionCurryingExample02.htm

對(duì) curry()函數(shù)的主要更改在于傳入的參數(shù)個(gè)數(shù),以及它如何影響代碼的結(jié)果。curry()僅僅接受

一個(gè)要包裹的函數(shù)作為參數(shù),而 bind()同時(shí)接受函數(shù)和一個(gè) object 對(duì)象。這表示給被綁定的函數(shù)的參

數(shù)是從第三個(gè)開始而不是第二個(gè),這就要更改 slice()的第一處調(diào)用。另一處更改是在倒數(shù)第 3 行將

object 對(duì)象傳給 apply()。當(dāng)使用 bind()時(shí),它會(huì)返回綁定到給定環(huán)境的函數(shù),并且可能它其中某些

函數(shù)參數(shù)已經(jīng)被設(shè)好。當(dāng)你想除了 event 對(duì)象再額外給事件處理程序傳遞參數(shù)時(shí),這非常有用,例如:

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

第624頁

606 第 22 章 高級(jí)技巧

var handler = {

message: \"Event handled\",

handleClick: function(name, event){

alert(this.message + \":\"+ name + \":\"+ event.type);

}

};

var btn = document.getElementById(\"my-btn\");

EventUtil.addHandler(btn, \"click\", bind(handler.handleClick, handler, \"my-btn\"));

FunctionCurryingExample02.htm

在這個(gè)更新過的例子中,handler.handleClick()方法接受了兩個(gè)參數(shù):要處理的元素的名字和

event 對(duì)象。作為第三個(gè)參數(shù)傳遞給 bind()函數(shù)的名字,又被傳遞給了 handler.handleClick(),

而 handler.handleClick()也會(huì)同時(shí)接收到 event 對(duì)象。

ECMAScript 5 的 bind()方法也實(shí)現(xiàn)函數(shù)柯里化,只要在 this 的值之后再傳入另一個(gè)參數(shù)即可。

var handler = {

message: \"Event handled\",

handleClick: function(name, event){

alert(this.message + \":\" + name + \":\" + event.type);

}

};

var btn = document.getElementById(\"my-btn\");

EventUtil.addHandler(btn, \"click\", handler.handleClick.bind(handler, \"my-btn\"));

FunctionCurryingExample03.htm

JavaScript 中的柯里化函數(shù)和綁定函數(shù)提供了強(qiáng)大的動(dòng)態(tài)函數(shù)創(chuàng)建功能。使用 bind()還是 curry()

要根據(jù)是否需要 object 對(duì)象響應(yīng)來決定。它們都能用于創(chuàng)建復(fù)雜的算法和功能,當(dāng)然兩者都不應(yīng)濫用,

因?yàn)槊總€(gè)函數(shù)都會(huì)帶來額外的開銷。

22.2 防篡改對(duì)象

JavaScript 共享的本質(zhì)一直是開發(fā)人員心頭的痛。因?yàn)槿魏螌?duì)象都可以被在同一環(huán)境中運(yùn)行的代碼

修改。開發(fā)人員很可能會(huì)意外地修改別人的代碼,甚至更糟糕地,用不兼容的功能重寫原生對(duì)象。

ECMAScript 5 致力于解決這個(gè)問題,可以讓開發(fā)人員定義防篡改對(duì)象(tamper-proof object)。

第6章討論了對(duì)象屬性的問題,也討論了如何手工設(shè)置每個(gè)屬性的[[Configurable]]、

[[Writable]]、 [[Enumerable]]、[[Value]]、[[Get]]以及[[Set]]特性,以改變屬性的行為。

類似地,ECMAScript 5也增加了幾個(gè)方法,通過它們可以指定對(duì)象的行為。

不過請(qǐng)注意:一旦把對(duì)象定義為防篡改,就無法撤銷了。

22.2.1 不可擴(kuò)展對(duì)象

默認(rèn)情況下,所有對(duì)象都是可以擴(kuò)展的。也就是說,任何時(shí)候都可以向?qū)ο笾刑砑訉傩院头椒?。?/p>

如,可以像下面這樣先定義一個(gè)對(duì)象,后來再給它添加一個(gè)屬性。

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

第625頁

22.2 防篡改對(duì)象 607

14

2

3

17

18

13

19

7

8

22

10

11

12

var person = { name: \"Nicholas\" };

person.age = 29;

即使第一行代碼已經(jīng)完整定義 person 對(duì)象,但第二行代碼仍然能給它添加屬性?,F(xiàn)在,使用

Object.preventExtensions()方法可以改變這個(gè)行為,讓你不能再給對(duì)象添加屬性和方法。

例如:

var person = { name: \"Nicholas\" };

Object.preventExtensions(person);

person.age = 29;

alert(person.age); //undefined

NonExtensibleObjectsExample01.htm

在調(diào)用了 Object.preventExtensions()方法后,就不能給 person 對(duì)象添加新屬性和方法了。

在非嚴(yán)格模式下,給對(duì)象添加新成員會(huì)導(dǎo)致靜默失敗,因此 person.age 將是 undefined。而在嚴(yán)格

模式下,嘗試給不可擴(kuò)展的對(duì)象添加新成員會(huì)導(dǎo)致拋出錯(cuò)誤。

雖然不能給對(duì)象添加新成員,但已有的成員則絲毫不受影響。你仍然還可以修改和刪除已有的成員。

另外,使用 Object.istExtensible()方法還可以確定對(duì)象是否可以擴(kuò)展。

var person = { name: \"Nicholas\" };

alert(Object.isExtensible(person)); //true

Object.preventExtensions(person);

alert(Object.isExtensible(person)); //false

NonExtensibleObjectsExample02.htm

22.2.2 密封的對(duì)象

ECMAScript 5 為對(duì)象定義的第二個(gè)保護(hù)級(jí)別是密封對(duì)象(sealed object)。密封對(duì)象不可擴(kuò)展,而

且已有成員的[[Configurable]]特性將被設(shè)置為 false。這就意味著不能刪除屬性和方法,因?yàn)椴荒?/p>

使用 Object.defineProperty()把數(shù)據(jù)屬性修改為訪問器屬性,或者相反。屬性值是可以修改的。

要密封對(duì)象,可以使用 Object.seal()方法。

var person = { name: \"Nicholas\" };

Object.seal(person);

person.age = 29;

alert(person.age); //undefined

delete person.name;

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

SealedObjectsExample01.htm

在這個(gè)例子中,添加 age 屬性的行為被忽略了。而嘗試刪除 name 屬性的操作也被忽略了,因此這

個(gè)屬性沒有受任何影響。這是在非嚴(yán)格模式下的行為。在嚴(yán)格模式下,嘗試添加或刪除對(duì)象成員都會(huì)導(dǎo)

致拋出錯(cuò)誤。

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

第626頁

608 第 22 章 高級(jí)技巧

使用 Object.isSealed()方法可以確定對(duì)象是否被密封了。因?yàn)楸幻芊獾膶?duì)象不可擴(kuò)展,所以用

Object.isExtensible()檢測(cè)密封的對(duì)象也會(huì)返回 false。

var person = { name: \"Nicholas\" };

alert(Object.isExtensible(person)); //true

alert(Object.isSealed(person)); //false

Object.seal(person);

alert(Object.isExtensible(person)); //false

alert(Object.isSealed(person)); //true

SealedObjectsExample02.htm

22.2.3 凍結(jié)的對(duì)象

最嚴(yán)格的防篡改級(jí)別是凍結(jié)對(duì)象(frozen object)。凍結(jié)的對(duì)象既不可擴(kuò)展,又是密封的,而且對(duì)象

數(shù)據(jù)屬性的[[Writable]]特性會(huì)被設(shè)置為 false。如果定義[[Set]]函數(shù),訪問器屬性仍然是可寫的。

ECMAScript 5 定義的 Object.freeze()方法可以用來凍結(jié)對(duì)象。

var person = { name: \"Nicholas\" };

Object.freeze(person);

person.age = 29;

alert(person.age); //undefined

delete person.name;

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

person.name = \"Greg\";

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

FrozenObjectsExample01.htm

與密封和不允許擴(kuò)展一樣,對(duì)凍結(jié)的對(duì)象執(zhí)行非法操作在非嚴(yán)格模式下會(huì)被忽略,而在嚴(yán)格模式下

會(huì)拋出錯(cuò)誤。

當(dāng)然,也有一個(gè) Object.isFrozen()方法用于檢測(cè)凍結(jié)對(duì)象。因?yàn)閮鼋Y(jié)對(duì)象既是密封的又是不可

擴(kuò)展的,所以用 Object.isExtensible()和 Object.isSealed()檢測(cè)凍結(jié)對(duì)象將分別返回 false

和 true。

var person = { name: \"Nicholas\" };

alert(Object.isExtensible(person)); //true

alert(Object.isSealed(person)); //false

alert(Object.isFrozen(person)); //false

Object.freeze(person);

alert(Object.isExtensible(person)); //false

alert(Object.isSealed(person)); //true

alert(Object.isFrozen(person)); //true

FrozenObjectsExample02.htm

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

第627頁

22.3 高級(jí)定時(shí)器 609

14

2

3

17

18

13

19

7

8

22

10

11

12

對(duì) JavaScript 庫的作者而言,凍結(jié)對(duì)象是很有用的。因?yàn)?JavaScript 庫最怕有人意外(或有意)地修

改了庫中的核心對(duì)象。凍結(jié)(或密封)主要的庫對(duì)象能夠防止這些問題的發(fā)生。

22.3 高級(jí)定時(shí)器

使用 setTimeout()和 setInterval()創(chuàng)建的定時(shí)器可以用于實(shí)現(xiàn)有趣且有用的功能。雖然人們

對(duì) JavaScript 的定時(shí)器存在普遍的誤解,認(rèn)為它們是線程,其實(shí) JavaScript 是運(yùn)行于單線程的環(huán)境中的,

而定時(shí)器僅僅只是計(jì)劃代碼在未來的某個(gè)時(shí)間執(zhí)行。執(zhí)行時(shí)機(jī)是不能保證的,因?yàn)樵陧撁娴纳芷谥校?/p>

不同時(shí)間可能有其他代碼在控制 JavaScript 進(jìn)程。在頁面下載完后的代碼運(yùn)行、事件處理程序、Ajax 回

調(diào)函數(shù)都必須使用同樣的線程來執(zhí)行。實(shí)際上,瀏覽器負(fù)責(zé)進(jìn)行排序,指派某段代碼在某個(gè)時(shí)間點(diǎn)運(yùn)行

的優(yōu)先級(jí)。

可以把 JavaScript 想象成在時(shí)間線上運(yùn)行的。當(dāng)頁面載入時(shí),首先執(zhí)行是任何包含在<script>元素

中的代碼,通常是頁面生命周期后面要用到的一些簡(jiǎn)單的函數(shù)和變量的聲明,不過有時(shí)候也包含一些初

始數(shù)據(jù)的處理。在這之后,JavaScript 進(jìn)程將等待更多代碼執(zhí)行。當(dāng)進(jìn)程空閑的時(shí)候,下一個(gè)代碼會(huì)被

觸發(fā)并立刻執(zhí)行。例如,當(dāng)點(diǎn)擊某個(gè)按鈕時(shí),onclick 事件處理程序會(huì)立刻執(zhí)行,只要 JavaScript 進(jìn)程

處于空閑狀態(tài)。這樣一個(gè)頁面的時(shí)間線類似于圖 22-1。

圖 22-1

除了主 JavaScript 執(zhí)行進(jìn)程外,還有一個(gè)需要在進(jìn)程下一次空閑時(shí)執(zhí)行的代碼隊(duì)列。隨著頁面在其

生命周期中的推移,代碼會(huì)按照?qǐng)?zhí)行順序添加入隊(duì)列。例如,當(dāng)某個(gè)按鈕被按下時(shí),它的事件處理程序

代碼就會(huì)被添加到隊(duì)列中,并在下一個(gè)可能的時(shí)間里執(zhí)行。當(dāng)接收到某個(gè) Ajax 響應(yīng)時(shí),回調(diào)函數(shù)的代

碼會(huì)被添加到隊(duì)列。在 JavaScript 中沒有任何代碼是立刻執(zhí)行的,但一旦進(jìn)程空閑則盡快執(zhí)行。

定時(shí)器對(duì)隊(duì)列的工作方式是,當(dāng)特定時(shí)間過去后將代碼插入。注意,給隊(duì)列添加代碼并不意味著對(duì)

它立刻執(zhí)行,而只能表示它會(huì)盡快執(zhí)行。設(shè)定一個(gè) 150ms 后執(zhí)行的定時(shí)器不代表到了 150ms 代碼就立刻

執(zhí)行,它表示代碼會(huì)在 150ms 后被加入到隊(duì)列中。如果在這個(gè)時(shí)間點(diǎn)上,隊(duì)列中沒有其他東西,那么這

段代碼就會(huì)被執(zhí)行,表面上看上去好像代碼就在精確指定的時(shí)間點(diǎn)上執(zhí)行了。其他情況下,代碼可能明

顯地等待更長(zhǎng)時(shí)間才執(zhí)行。

請(qǐng)看以下代碼:

var btn = document.getElementById(\"my-btn\");

btn.onclick = function(){

setTimeout(function(){

document.getElementById(\"message\").style.visibility = \"visible\";

}, 250);

//其他代碼

};

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

第628頁

610 第 22 章 高級(jí)技巧

在這里給一個(gè)按鈕設(shè)置了一個(gè)事件處理程序。事件處理程序設(shè)置了一個(gè) 250ms 后調(diào)用的定時(shí)器。

點(diǎn)擊該按鈕后,首先將 onclick 事件處理程序加入隊(duì)列。該程序執(zhí)行后才設(shè)置定時(shí)器,再有 250ms

后,指定的代碼才被添加到隊(duì)列中等待執(zhí)行。實(shí)際上,對(duì) setTimeout()的調(diào)用表示要晚點(diǎn)執(zhí)行某些

代碼。

關(guān)于定時(shí)器要記住的最重要的事情是,指定的時(shí)間間隔表示何時(shí)將定時(shí)器的代碼添加到隊(duì)列,而不

是何時(shí)實(shí)際執(zhí)行代碼。如果前面例子中的 onclick 事件處理程序執(zhí)行了 300ms,那么定時(shí)器的代碼至

少要在定時(shí)器設(shè)置之后的 300ms 后才會(huì)被執(zhí)行。隊(duì)列中所有的代碼都要等到 JavaScript 進(jìn)程空閑之后才

能執(zhí)行,而不管它們是如何添加到隊(duì)列中的。見圖 22-2。

圖 22-2

如圖 22-2 所示,盡管在 255ms 處添加了定時(shí)器代碼,但這時(shí)候還不能執(zhí)行,因?yàn)?onclick 事件處

理程序仍在運(yùn)行。定時(shí)器代碼最早能執(zhí)行的時(shí)機(jī)是在 300ms 處,即 onclick 事件處理程序結(jié)束之后。

實(shí)際上 Firefox 中定時(shí)器的實(shí)現(xiàn)還能讓你確定定時(shí)器過了多久才執(zhí)行,這需傳遞一個(gè)實(shí)際執(zhí)行的時(shí)

間與指定的間隔的差值。如下面的例子所示。

//僅 Firefox 中

setTimeout(function(diff){

if (diff > 0) {

//晚調(diào)用

} else if (diff < 0){

//早調(diào)用

} else {

//調(diào)用及時(shí)

}

}, 250);

執(zhí)行完一套代碼后,JavaScript 進(jìn)程返回一段很短的時(shí)間,這樣頁面上的其他處理就可以進(jìn)行了。

由于 JavaScript 進(jìn)程會(huì)阻塞其他頁面處理,所以必須有這些小間隔來防止用戶界面被鎖定(代碼長(zhǎng)時(shí)間

運(yùn)行中還有可能出現(xiàn))。這樣設(shè)置一個(gè)定時(shí)器,可以確保在定時(shí)器代碼執(zhí)行前至少有一個(gè)進(jìn)程間隔。

22.3.1 重復(fù)的定時(shí)器

使用 setInterval()創(chuàng)建的定時(shí)器確保了定時(shí)器代碼規(guī)則地插入隊(duì)列中。這個(gè)方式的問題在于,

定時(shí)器代碼可能在代碼再次被添加到隊(duì)列之前還沒有完成執(zhí)行,結(jié)果導(dǎo)致定時(shí)器代碼連續(xù)運(yùn)行好幾次,

而之間沒有任何停頓。幸好,JavaScript 引擎夠聰明,能避免這個(gè)問題。當(dāng)使用 setInterval()時(shí),僅

當(dāng)沒有該定時(shí)器的任何其他代碼實(shí)例時(shí),才將定時(shí)器代碼添加到隊(duì)列中。這確保了定時(shí)器代碼加入到隊(duì)

JavaScript 進(jìn)程時(shí)間線

onclick 空閑

定時(shí)器代碼添加到隊(duì)列中

定時(shí)器代碼

創(chuàng)建了間隔為 250 的定時(shí)器

單位:毫秒

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

第629頁

22.3 高級(jí)定時(shí)器 611

14

2

3

17

18

13

19

7

8

22

10

11

12

列中的最小時(shí)間間隔為指定間隔。

這種重復(fù)定時(shí)器的規(guī)則有兩個(gè)問題:(1) 某些間隔會(huì)被跳過;(2) 多個(gè)定時(shí)器的代碼執(zhí)行之間的間隔

可能會(huì)比預(yù)期的小。假設(shè),某個(gè) onclick 事件處理程序使用 setInterval()設(shè)置了一個(gè) 200ms 間隔

的重復(fù)定時(shí)器。如果事件處理程序花了 300ms 多一點(diǎn)的時(shí)間完成,同時(shí)定時(shí)器代碼也花了差不多的時(shí)間,

就會(huì)同時(shí)出現(xiàn)跳過間隔且連續(xù)運(yùn)行定時(shí)器代碼的情況。參見圖 22-3。

圖 22-3

這個(gè)例子中的第 1 個(gè)定時(shí)器是在 205ms 處添加到隊(duì)列中的,但是直到過了 300ms 處才能夠執(zhí)行。當(dāng)

執(zhí)行這個(gè)定時(shí)器代碼時(shí),在 405ms 處又給隊(duì)列添加了另外一個(gè)副本。在下一個(gè)間隔,即 605ms 處,第一

個(gè)定時(shí)器代碼仍在運(yùn)行,同時(shí)在隊(duì)列中已經(jīng)有了一個(gè)定時(shí)器代碼的實(shí)例。結(jié)果是,在這個(gè)時(shí)間點(diǎn)上的定

時(shí)器代碼不會(huì)被添加到隊(duì)列中。結(jié)果在 5ms 處添加的定時(shí)器代碼結(jié)束之后,405ms 處添加的定時(shí)器代碼

就立刻執(zhí)行。

為了避免setInterval()的重復(fù)定時(shí)器的這2個(gè)缺點(diǎn),你可以用如下模式使用鏈?zhǔn)絪etTimeout()

調(diào)用。

setTimeout(function(){

//處理中

setTimeout(arguments.callee, interval);

}, interval);

這個(gè)模式鏈?zhǔn)秸{(diào)用了 setTimeout(),每次函數(shù)執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)新的定時(shí)器。第二個(gè)

setTimeout()調(diào)用使用了 arguments.callee 來獲取對(duì)當(dāng)前執(zhí)行的函數(shù)的引用,并為其設(shè)置另外一

個(gè)定時(shí)器。這樣做的好處是,在前一個(gè)定時(shí)器代碼執(zhí)行完之前,不會(huì)向隊(duì)列插入新的定時(shí)器代碼,確保

不會(huì)有任何缺失的間隔。而且,它可以保證在下一次定時(shí)器代碼執(zhí)行之前,至少要等待指定的間隔,避

免了連續(xù)的運(yùn)行。這個(gè)模式主要用于重復(fù)定時(shí)器,如下例所示。

setTimeout(function(){

var div = document.getElementById(\"myDiv\");

left = parseInt(div.style.left) + 5;

div.style.left = left + \"px\";

if (left < 200){

JavaScript 進(jìn)程時(shí)間線

onclick 定時(shí)器代碼

定時(shí)器代碼添加到隊(duì)列中

定時(shí)器代碼

創(chuàng)建了間隔為 200 的定時(shí)器

定時(shí)器代碼被跳過

定時(shí)器代碼添加到隊(duì)列中

單位:毫秒

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

第630頁

612 第 22 章 高級(jí)技巧

setTimeout(arguments.callee, 50);

}

}, 50);

RepeatingTimersExample.htm

這段定時(shí)器代碼每次執(zhí)行的時(shí)候?qū)⒁粋€(gè)<div>元素向右移動(dòng),當(dāng)左坐標(biāo)在 200 像素的時(shí)候停止。

JavaScript 動(dòng)畫中使用這個(gè)模式很常見。

每個(gè)瀏覽器窗口、標(biāo)簽頁、或者 frame 都有其各自的代碼執(zhí)行隊(duì)列。這意味著,

進(jìn)行跨 frame 或者跨窗口的定時(shí)調(diào)用,當(dāng)代碼同時(shí)執(zhí)行的時(shí)候可能會(huì)導(dǎo)致競(jìng)爭(zhēng)條件。

無論何時(shí)需要使用這種通信類型,最好是在接收 frame 或者窗口中創(chuàng)建一個(gè)定時(shí)器來

執(zhí)行代碼。

22.3.2 Yielding Processes

運(yùn)行在瀏覽器中的 JavaScript 都被分配了一個(gè)確定數(shù)量的資源。不同于桌面應(yīng)用往往能夠隨意控制

他們要的內(nèi)存大小和處理器時(shí)間,JavaScript 被嚴(yán)格限制了,以防止惡意的 Web 程序員把用戶的計(jì)算機(jī)

搞掛了。其中一個(gè)限制是長(zhǎng)時(shí)間運(yùn)行腳本的制約,如果代碼運(yùn)行超過特定的時(shí)間或者特定語句數(shù)量就不

讓它繼續(xù)執(zhí)行。如果代碼達(dá)到了這個(gè)限制,會(huì)彈出一個(gè)瀏覽器錯(cuò)誤的對(duì)話框,告訴用戶某個(gè)腳本會(huì)用過

長(zhǎng)的時(shí)間執(zhí)行,詢問是允許其繼續(xù)執(zhí)行還是停止它。所有 JavaScript 開發(fā)人員的目標(biāo)就是,確保用戶永

遠(yuǎn)不會(huì)在瀏覽器中看到這個(gè)令人費(fèi)解的對(duì)話框。定時(shí)器是繞開此限制的方法之一。

腳本長(zhǎng)時(shí)間運(yùn)行的問題通常是由兩個(gè)原因之一造成的:過長(zhǎng)的、過深嵌套的函數(shù)調(diào)用或者是進(jìn)行大

量處理的循環(huán)。這兩者中,后者是較為容易解決的問題。長(zhǎng)時(shí)間運(yùn)行的循環(huán)通常遵循以下模式:

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

process(data[i]);

}

這個(gè)模式的問題在于要處理的項(xiàng)目的數(shù)量在運(yùn)行前是不可知的。如果完成 process()要花 100ms,

只有 2 個(gè)項(xiàng)目的數(shù)組可能不會(huì)造成影響,但是 10 個(gè)的數(shù)組可能會(huì)導(dǎo)致腳本要運(yùn)行一秒鐘才能完成。數(shù)

組中的項(xiàng)目數(shù)量直接關(guān)系到執(zhí)行完該循環(huán)的時(shí)間長(zhǎng)度。同時(shí)由于 JavaScript 的執(zhí)行是一個(gè)阻塞操作,腳

本運(yùn)行所花時(shí)間越久,用戶無法與頁面交互的時(shí)間也越久。

在展開該循環(huán)之前,你需要回答以下兩個(gè)重要的問題。

? 該處理是否必須同步完成?如果這個(gè)數(shù)據(jù)的處理會(huì)造成其他運(yùn)行的阻塞,那么最好不要改動(dòng)它。

不過,如果你對(duì)這個(gè)問題的回答確定為“否”,那么將某些處理推遲到以后是個(gè)不錯(cuò)的備選項(xiàng)。

? 數(shù)據(jù)是否必須按順序完成?通常,數(shù)組只是對(duì)項(xiàng)目的組合和迭代的一種簡(jiǎn)便的方法而無所謂順

序。如果項(xiàng)目的順序不是非常重要,那么可能可以將某些處理推遲到以后。

當(dāng)你發(fā)現(xiàn)某個(gè)循環(huán)占用了大量時(shí)間,同時(shí)對(duì)于上述兩個(gè)問題,你的回答都是“否”,那么你就可以

使用定時(shí)器分割這個(gè)循環(huán)。這是一種叫做數(shù)組分塊(array chunking)的技術(shù),小塊小塊地處理數(shù)組,通

常每次一小塊?;镜乃悸肥菫橐幚淼捻?xiàng)目創(chuàng)建一個(gè)隊(duì)列,然后使用定時(shí)器取出下一個(gè)要處理的項(xiàng)目

進(jìn)行處理,接著再設(shè)置另一個(gè)定時(shí)器。基本的模式如下。

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

第631頁

22.3 高級(jí)定時(shí)器 613

14

2

3

17

18

13

19

7

8

22

10

11

12

setTimeout(function(){

//取出下一個(gè)條目并處理

var item = array.shift();

process(item);

//若還有條目,再設(shè)置另一個(gè)定時(shí)器

if(array.length > 0){

setTimeout(arguments.callee, 100);

}

}, 100);

在數(shù)組分塊模式中,array 變量本質(zhì)上就是一個(gè)“待辦事宜”列表,它包含了要處理的項(xiàng)目。使用

shift()方法可以獲取隊(duì)列中下一個(gè)要處理的項(xiàng)目,然后將其傳遞給某個(gè)函數(shù)。如果在隊(duì)列中還有其他

項(xiàng)目,則設(shè)置另一個(gè)定時(shí)器,并通過 arguments.callee 調(diào)用同一個(gè)匿名函數(shù)。要實(shí)現(xiàn)數(shù)組分塊非常

簡(jiǎn)單,可以使用以下函數(shù)。

function chunk(array, process, context){

setTimeout(function(){

var item = array.shift();

process.call(context, item);

if (array.length > 0){

setTimeout(arguments.callee, 100);

}

}, 100);

}

ArrayChunkingExample.htm

chunk()方法接受三個(gè)參數(shù):要處理的項(xiàng)目的數(shù)組,用于處理項(xiàng)目的函數(shù),以及可選的運(yùn)行該函數(shù)

的環(huán)境。函數(shù)內(nèi)部用了之前描述過的基本模式,通過 call()調(diào)用的 process()函數(shù),這樣可以設(shè)置一

個(gè)合適的執(zhí)行環(huán)境(如果必須)。定時(shí)器的時(shí)間間隔設(shè)置為了 100ms,使得 JavaScript 進(jìn)程有時(shí)間在處

理項(xiàng)目的事件之間轉(zhuǎn)入空閑。你可以根據(jù)你的需要更改這個(gè)間隔大小,不過 100ms 在大多數(shù)情況下效果

不錯(cuò)。可以按如下所示使用該函數(shù):

var data = [12,123,1234,453,436,23,23,5,4123,45,346,5634,2234,345,342];

function printValue(item){

var div = document.getElementById(\"myDiv\");

div.innerHTML += item + \"<br>\";

}

chunk(data, printValue);

ArrayChunkingExample.htm

這個(gè)例子使用 printValue()函數(shù)將 data 數(shù)組中的每個(gè)值輸出到一個(gè)<div>元素。由于函數(shù)處在

全局作用域內(nèi),因此無需給 chunk()傳遞一個(gè) context 對(duì)象。

必須當(dāng)心的地方是,傳遞給 chunk()的數(shù)組是用作一個(gè)隊(duì)列的,因此當(dāng)處理數(shù)據(jù)的同時(shí),數(shù)組中的

條目也在改變。如果你想保持原數(shù)組不變,則應(yīng)該將該數(shù)組的克隆傳遞給 chunk(),如下例所示:

chunk(data.concat(), printValue);

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

第632頁

614 第 22 章 高級(jí)技巧

當(dāng)不傳遞任何參數(shù)調(diào)用某個(gè)數(shù)組的 concat()方法時(shí),將返回和原來數(shù)組中項(xiàng)目一樣的數(shù)組。這樣

你就可以保證原數(shù)組不會(huì)被該函數(shù)更改。

數(shù)組分塊的重要性在于它可以將多個(gè)項(xiàng)目的處理在執(zhí)行隊(duì)列上分開,在每個(gè)項(xiàng)目處理之后,給予其

他的瀏覽器處理機(jī)會(huì)運(yùn)行,這樣就可能避免長(zhǎng)時(shí)間運(yùn)行腳本的錯(cuò)誤。

一旦某個(gè)函數(shù)需要花 50ms 以上的時(shí)間完成,那么最好看看能否將任務(wù)分割為一

系列可以使用定時(shí)器的小任務(wù)。

22.3.3 函數(shù)節(jié)流

瀏覽器中某些計(jì)算和處理要比其他的昂貴很多。例如,DOM 操作比起非 DOM 交互需要更多的內(nèi)

存和 CPU 時(shí)間。連續(xù)嘗試進(jìn)行過多的 DOM 相關(guān)操作可能會(huì)導(dǎo)致瀏覽器掛起,有時(shí)候甚至?xí)罎ⅰS绕?/p>

在 IE 中使用 onresize 事件處理程序的時(shí)候容易發(fā)生,當(dāng)調(diào)整瀏覽器大小的時(shí)候,該事件會(huì)連續(xù)觸發(fā)。

在 onresize 事件處理程序內(nèi)部如果嘗試進(jìn)行 DOM 操作,其高頻率的更改可能會(huì)讓瀏覽器崩潰。為了

繞開這個(gè)問題,你可以使用定時(shí)器對(duì)該函數(shù)進(jìn)行節(jié)流。

函數(shù)節(jié)流背后的基本思想是指,某些代碼不可以在沒有間斷的情況連續(xù)重復(fù)執(zhí)行。第一次調(diào)用函數(shù),

創(chuàng)建一個(gè)定時(shí)器,在指定的時(shí)間間隔之后運(yùn)行代碼。當(dāng)?shù)诙握{(diào)用該函數(shù)時(shí),它會(huì)清除前一次的定時(shí)器

并設(shè)置另一個(gè)。如果前一個(gè)定時(shí)器已經(jīng)執(zhí)行過了,這個(gè)操作就沒有任何意義。然而,如果前一個(gè)定時(shí)器

尚未執(zhí)行,其實(shí)就是將其替換為一個(gè)新的定時(shí)器。目的是只有在執(zhí)行函數(shù)的請(qǐng)求停止了一段時(shí)間之后才

執(zhí)行。以下是該模式的基本形式:

var processor = {

timeoutId: null,

//實(shí)際進(jìn)行處理的方法

performProcessing: function(){

//實(shí)際執(zhí)行的代碼

},

//初始處理調(diào)用的方法

process: function(){

clearTimeout(this.timeoutId);

var that = this;

this.timeoutId = setTimeout(function(){

that.performProcessing();

}, 100);

}

};

//嘗試開始執(zhí)行

processor.process();

在這段代碼中,創(chuàng)建了一個(gè)叫做 processor 對(duì)象。這個(gè)對(duì)象還有 2 個(gè)方法:process()和

performProcessing()。前者是初始化任何處理所必須調(diào)用的,后者則實(shí)際進(jìn)行應(yīng)完成的處理。當(dāng)調(diào)

用了 process(),第一步是清除存好的 timeoutId,來阻止之前的調(diào)用被執(zhí)行。然后,創(chuàng)建一個(gè)新的

定時(shí)器調(diào)用 performProcessing()。由于 setTimeout()中用到的函數(shù)的環(huán)境總是 window,所以有

必要保存 this 的引用以方便以后使用。

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

第633頁

22.3 高級(jí)定時(shí)器 615

14

2

3

17

18

13

19

7

8

22

10

11

12

時(shí)間間隔設(shè)為了 100ms,這表示最后一次調(diào)用 process()之后至少 100ms 后才會(huì)調(diào)用 performProcessing()。所以如果 100ms 之內(nèi)調(diào)用了 process()共 20 次,performanceProcessing()仍只

會(huì)被調(diào)用一次。

這個(gè)模式可以使用 throttle()函數(shù)來簡(jiǎn)化,這個(gè)函數(shù)可以自動(dòng)進(jìn)行定時(shí)器的設(shè)置和清除,如下例

所示:

function throttle(method, context) {

clearTimeout(method.tId);

method.tId= setTimeout(function(){

method.call(context);

}, 100);

}

ThrottlingExample.htm

throttle()函數(shù)接受兩個(gè)參數(shù):要執(zhí)行的函數(shù)以及在哪個(gè)作用域中執(zhí)行。上面這個(gè)函數(shù)首先清除

之前設(shè)置的任何定時(shí)器。定時(shí)器 ID 是存儲(chǔ)在函數(shù)的 tId 屬性中的,第一次把方法傳遞給 throttle()

的時(shí)候,這個(gè)屬性可能并不存在。接下來,創(chuàng)建一個(gè)新的定時(shí)器,并將其 ID 儲(chǔ)存在方法的 tId 屬性中。

如果這是第一次對(duì)這個(gè)方法調(diào)用 throttle()的話,那么這段代碼會(huì)創(chuàng)建該屬性。定時(shí)器代碼使用

call()來確保方法在適當(dāng)?shù)沫h(huán)境中執(zhí)行。如果沒有給出第二個(gè)參數(shù),那么就在全局作用域內(nèi)執(zhí)行該方

法。

前面提到過,節(jié)流在 resize 事件中是最常用的。如果你基于該事件來改變頁面布局的話,最好控

制處理的頻率,以確保瀏覽器不會(huì)在極短的時(shí)間內(nèi)進(jìn)行過多的計(jì)算。例如,假設(shè)有一個(gè)<div/>元素需

要保持它的高度始終等同于寬度。那么實(shí)現(xiàn)這一功能的 JavaScript 可以如下編寫:

window.onresize = function(){

var div = document.getElementById(\"myDiv\");

div.style.height = div. offsetWidth + \"px\";

};

這段非常簡(jiǎn)單的例子有兩個(gè)問題可能會(huì)造成瀏覽器運(yùn)行緩慢。首先,要計(jì)算 offsetWidth 屬性,

如果該元素或者頁面上其他元素有非常復(fù)雜的 CSS 樣式,那么這個(gè)過程將會(huì)很復(fù)雜。其次,設(shè)置某個(gè)元

素的高度需要對(duì)頁面進(jìn)行回流來令改動(dòng)生效。如果頁面有很多元素同時(shí)應(yīng)用了相當(dāng)數(shù)量的 CSS 的話,這

又需要很多計(jì)算。這就可以用到 throttle()函數(shù),如下例所示:

function resizeDiv(){

var div = document.getElementById(\"myDiv\");

div.style.height = div.offsetWidth + \"px\";

}

window.onresize = function(){

throttle(resizeDiv);

};

ThrottlingExample.htm

這里,調(diào)整大小的功能被放入了一個(gè)叫做 resizeDiv()的單獨(dú)函數(shù)中。然后 onresize 事件處理

程序調(diào)用 throttle()并傳入 resizeDiv 函數(shù),而不是直接調(diào)用 resizeDiv()。多數(shù)情況下,用戶是

感覺不到變化的,雖然給瀏覽器節(jié)省的計(jì)算可能會(huì)非常大。

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

第634頁

616 第 22 章 高級(jí)技巧

只要代碼是周期性執(zhí)行的,都應(yīng)該使用節(jié)流,但是你不能控制請(qǐng)求執(zhí)行的速率。這里展示的

throttle()函數(shù)用了 100ms 作為間隔,你當(dāng)然可以根據(jù)你的需要來修改它。

22.4 自定義事件

在本書前面,你已經(jīng)學(xué)到事件是 JavaScript 與瀏覽器交互的主要途徑。事件是一種叫做觀察者的設(shè)

計(jì)模式,這是一種創(chuàng)建松散耦合代碼的技術(shù)。對(duì)象可以發(fā)布事件,用來表示在該對(duì)象生命周期中某個(gè)有

趣的時(shí)刻到了。然后其他對(duì)象可以觀察該對(duì)象,等待這些有趣的時(shí)刻到來并通過運(yùn)行代碼來響應(yīng)。

觀察者模式由兩類對(duì)象組成:主體和觀察者。主體負(fù)責(zé)發(fā)布事件,同時(shí)觀察者通過訂閱這些事件來

觀察該主體。該模式的一個(gè)關(guān)鍵概念是主體并不知道觀察者的任何事情,也就是說它可以獨(dú)自存在并正

常運(yùn)作即使觀察者不存在。從另一方面來說,觀察者知道主體并能注冊(cè)事件的回調(diào)函數(shù)(事件處理程序)。

涉及 DOM 上時(shí),DOM 元素便是主體,你的事件處理代碼便是觀察者。

事件是與 DOM 交互的最常見的方式,但它們也可以用于非 DOM 代碼中——通過實(shí)現(xiàn)自定義事件。

自定義事件背后的概念是創(chuàng)建一個(gè)管理事件的對(duì)象,讓其他對(duì)象監(jiān)聽那些事件。實(shí)現(xiàn)此功能的基本模式

可以如下定義:

function EventTarget(){

this.handlers = {};

}

EventTarget.prototype = {

constructor: EventTarget,

addHandler: function(type, handler){

if (typeof this.handlers[type] == \"undefined\"){

this.handlers[type] = [];

}

this.handlers[type].push(handler);

},

fire: function(event){

if (!event.target){

event.target = this;

}

if (this.handlers[event.type] instanceof Array){

var handlers = this.handlers[event.type];

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

handlers[i](event);

}

}

},

removeHandler: function(type, handler){

if (this.handlers[type] instanceof Array){

var handlers = this.handlers[type];

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

if (handlers[i] === handler){

break;

}

}

handlers.splice(i, 1);

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

第635頁

22.4 自定義事件 617

14

2

3

17

18

13

19

7

8

22

10

11

12

}

}

};

EventTarget.js

EventTarget 類型有一個(gè)單獨(dú)的屬性 handlers,用于儲(chǔ)存事件處理程序。還有三個(gè)方法:

addHandler() ,用于注冊(cè)給定類型事件的事件處理程序; fire() ,用于觸發(fā)一個(gè)事件;

removeHandler(),用于注銷某個(gè)事件類型的事件處理程序。

addHandler()方法接受兩個(gè)參數(shù):事件類型和用于處理該事件的函數(shù)。當(dāng)調(diào)用該方法時(shí),會(huì)進(jìn)行

一次檢查,看看 handlers 屬性中是否已經(jīng)存在一個(gè)針對(duì)該事件類型的數(shù)組;如果沒有,則創(chuàng)建一個(gè)新

的。然后使用 push()將該處理程序添加到數(shù)組的末尾。

如果要觸發(fā)一個(gè)事件,要調(diào)用 fire()函數(shù)。該方法接受一個(gè)單獨(dú)的參數(shù),是一個(gè)至少包含 type

屬性的對(duì)象。fire()方法先給 event 對(duì)象設(shè)置一個(gè) target 屬性,如果它尚未被指定的話。然后它就

查找對(duì)應(yīng)該事件類型的一組處理程序,調(diào)用各個(gè)函數(shù),并給出 event 對(duì)象。因?yàn)檫@些都是自定義事件,

所以 event 對(duì)象上還需要的額外信息由你自己決定。

removeHandler()方法是 addHandler()的輔助,它們接受的參數(shù)一樣:事件的類型和事件處理

程序。這個(gè)方法搜索事件處理程序的數(shù)組找到要?jiǎng)h除的處理程序的位置。如果找到了,則使用 break

操作符退出 for 循環(huán)。然后使用 splice()方法將該項(xiàng)目從數(shù)組中刪除。

然后,使用 EventTarget 類型的自定義事件可以如下使用:

function handleMessage(event){

alert(\"Message received: \" + event.message);

}

//創(chuàng)建一個(gè)新對(duì)象

var target = new EventTarget();

//添加一個(gè)事件處理程序

target.addHandler(\"message\", handleMessage);

//觸發(fā)事件

target.fire({ type: \"message\", message: \"Hello world!\"});

//刪除事件處理程序

target.removeHandler(\"message\", handleMessage);

//再次,應(yīng)沒有處理程序

target.fire({ type: \"message\", message: \"Hello world!\"});

EventTargetExample01.htm

在這段代碼中,定義了 handleMessage()函數(shù)用于處理 message 事件。它接受 event 對(duì)象并輸

出 message 屬性。調(diào)用 target 對(duì)象的 addHandler()方法并傳給\"message\"以及 handleMessage()

函數(shù)。在接下來的一行上,調(diào)用了 fire()函數(shù),并傳遞了包含 2 個(gè)屬性,即 type 和 message 的對(duì)象

直接量。它會(huì)調(diào)用 message 事件的事件處理程序,這樣就會(huì)顯示一個(gè)警告框(來自 handleMessage())。

然后刪除了事件處理程序,這樣即使事件再次觸發(fā),也不會(huì)顯示任何警告框。

因?yàn)檫@種功能是封裝在一種自定義類型中的,其他對(duì)象可以繼承 EventTarget 并獲得這個(gè)行為,

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

第636頁

618 第 22 章 高級(jí)技巧

如下例所示:

function Person(name, age){

EventTarget.call(this);

this.name = name;

this.age = age;

}

inheritPrototype(Person,EventTarget);

Person.prototype.say = function(message){

this.fire({type: \"message\", message: message});

};

EventTargetExample02.htm

Person 類型使用了寄生組合繼承(參見第 6 章)方法來繼承 EventTarget。一旦調(diào)用了 say()

方法,便觸發(fā)了事件,它包含了消息的細(xì)節(jié)。在某種類型的另外的方法中調(diào)用 fire()方法是很常見的,

同時(shí)它通常不是公開調(diào)用的。這段代碼可以照如下方式使用:

function handleMessage(event){

alert(event.target.name + \" says: \" + event.message);

}

//創(chuàng)建新 person

var person = new Person(\"Nicholas\", 29);

//添加一個(gè)事件處理程序

person.addHandler(\"message\", handleMessage);

//在該對(duì)象上調(diào)用 1 個(gè)方法,它觸發(fā)消息事件

person.say(\"Hi there.\");

EventTargetExample02.htm

這個(gè)例子中的 handleMessage()函數(shù)顯示了某人名字(通過 event.target.name 獲得)的一個(gè)

警告框和消息正文。當(dāng)調(diào)用 say()方法并傳遞一個(gè)消息時(shí),就會(huì)觸發(fā) message 事件。接下來,它又會(huì)

調(diào)用 handleMessage()函數(shù)并顯示警告框。

當(dāng)代碼中存在多個(gè)部分在特定時(shí)刻相互交互的情況下,自定義事件就非常有用了。這時(shí),如果每個(gè)

對(duì)象都有對(duì)其他所有對(duì)象的引用,那么整個(gè)代碼就會(huì)緊密耦合,同時(shí)維護(hù)也變得很困難,因?yàn)閷?duì)某個(gè)對(duì)

象的修改也會(huì)影響到其他對(duì)象。使用自定義事件有助于解耦相關(guān)對(duì)象,保持功能的隔絕。在很多情況中,

觸發(fā)事件的代碼和監(jiān)聽事件的代碼是完全分離的。

22.5 拖放

拖放是一種非常流行的用戶界面模式。它的概念很簡(jiǎn)單:點(diǎn)擊某個(gè)對(duì)象,并按住鼠標(biāo)按鈕不放,將

鼠標(biāo)移動(dòng)到另一個(gè)區(qū)域,然后釋放鼠標(biāo)按鈕將對(duì)象“放”在這里。拖放功能也流行到了 Web 上,成為

了一些更傳統(tǒng)的配置界面的一種候選方案。

拖放的基本概念很簡(jiǎn)單:創(chuàng)建一個(gè)絕對(duì)定位的元素,使其可以用鼠標(biāo)移動(dòng)。這個(gè)技術(shù)源自一種叫做

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

第637頁

22.5 拖放 619

14

2

3

17

18

13

19

7

8

22

10

11

12

“鼠標(biāo)拖尾”的經(jīng)典網(wǎng)頁技巧。鼠標(biāo)拖尾是一個(gè)或者多個(gè)圖片在頁面上跟著鼠標(biāo)指針移動(dòng)。 單元素鼠標(biāo)

拖尾的基本代碼需要為文檔設(shè)置一個(gè) onmousemove 事件處理程序,它總是將指定元素移動(dòng)到鼠標(biāo)指針

的位置,如下面的例子所示。

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

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

myDiv.style.left = event.clientX + \"px\";

myDiv.style.top = event.clientY + \"px\";

});

DragAndDropExample01.htm

在這個(gè)例子中,元素的 left 和 top 坐標(biāo)設(shè)置為了 event 對(duì)象的 clientX 和 clientY 屬性,這

就將元素放到了視口中指針的位置上。它的效果是一個(gè)元素始終跟隨指針在頁面上的移動(dòng)。只要正確的

時(shí)刻(當(dāng)鼠標(biāo)按鈕按下的時(shí)候)實(shí)現(xiàn)該功能,并在之后刪除它(當(dāng)釋放鼠標(biāo)按鈕時(shí)),就可以實(shí)現(xiàn)拖放

了。最簡(jiǎn)單的拖放界面可用以下代碼實(shí)現(xiàn):

var DragDrop = function(){

var dragging = null;

function handleEvent(event){

//獲取事件和目標(biāo)

event = EventUtil.getEvent(event);

var target = EventUtil.getTarget(event);

//確定事件類型

switch(event.type){

case \"mousedown\":

if (target.className.indexOf(\"draggable\") > -1){

dragging = target;

}

break;

case \"mousemove\":

if (dragging !== null){

//指定位置

dragging.style.left = event.clientX + \"px\";

dragging.style.top = event.clientY + \"px\";

}

break;

case \"mouseup\":

dragging = null;

break;

}

};

//公共接口

return {

enable: function(){

EventUtil.addHandler(document, \"mousedown\", handleEvent);

EventUtil.addHandler(document, \"mousemove\", handleEvent);

EventUtil.addHandler(document, \"mouseup\", handleEvent);

},

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

第638頁

620 第 22 章 高級(jí)技巧

disable: function(){

EventUtil.removeHandler(document, \"mousedown\", handleEvent);

EventUtil.removeHandler(document, \"mousemove\", handleEvent);

EventUtil.removeHandler(document, \"mouseup\", handleEvent);

}

}

}();

DragAndDropExample02.htm

DragDrop 對(duì)象封裝了拖放的所有基本功能。這是一個(gè)單例對(duì)象,并使用了模塊模式來隱藏某些實(shí)

現(xiàn)細(xì)節(jié)。dragging 變量起初是 null,將會(huì)存放被拖動(dòng)的元素,所以當(dāng)該變量不為 null 時(shí),就知道正

在拖動(dòng)某個(gè)東西。handleEvent()函數(shù)處理拖放功能中的所有的三個(gè)鼠標(biāo)事件。它首先獲取 event 對(duì)

象和事件目標(biāo)的引用。之后,用一個(gè) switch 語句確定要觸發(fā)哪個(gè)事件樣式。當(dāng) mousedown 事件發(fā)生

時(shí),會(huì)檢查 target 的 class 是否包含\"draggable\"類,如果是,那么將 target 存放到 dragging

中。這個(gè)技巧可以很方便地通過標(biāo)記語言而非 JavaScript 腳本來確定可拖動(dòng)的元素。

handleEvent()的 mousemove 情況和前面的代碼一樣,不過要檢查 dragging 是否為 null。當(dāng)

它不是 null,就知道 dragging 就是要拖動(dòng)的元素,這樣就會(huì)把它放到恰當(dāng)?shù)奈恢蒙?。mouseup 情況

就僅僅是將 dragging 重置為 null,讓 mousemove 事件中的判斷失效。

DragDrop 還有兩個(gè)公共方法:enable()和 disable(),它們只是相應(yīng)添加和刪除所有的事件處

理程序。這兩個(gè)函數(shù)提供了額外的對(duì)拖放功能的控制手段。

要使用 DragDrop 對(duì)象,只要在頁面上包含這些代碼并調(diào)用 enable()。拖放會(huì)自動(dòng)針對(duì)所有包含

\"draggable\"類的元素啟用,如下例所示:

<div class=\"draggable\" style=\"position:absolute; background:red\"> </div>

注意為了元素能被拖放,它必須是絕對(duì)定位的。

22.5.1 修繕拖動(dòng)功能

當(dāng)你試了上面的例子之后,你會(huì)發(fā)現(xiàn)元素的左上角總是和指針在一起。這個(gè)結(jié)果對(duì)用戶來說有一點(diǎn)

不爽,因?yàn)楫?dāng)鼠標(biāo)開始移動(dòng)的時(shí)候,元素好像是突然跳了一下。理想情況是,這個(gè)動(dòng)作應(yīng)該看上去好像

這個(gè)元素是被指針“拾起”的,也就是說當(dāng)在拖動(dòng)元素的時(shí)候,用戶點(diǎn)擊的那一點(diǎn)就是指針應(yīng)該保持的

位置(見圖 22-4)。

圖 22-4

要達(dá)到需要的效果,必須做一些額外的計(jì)算。你需要計(jì)算元素左上角和指針位置之間的差值。這個(gè)

差值應(yīng)該在 mousedown 事件發(fā)生的時(shí)候確定,并且一直保持,直到 mouseup 事件發(fā)生。通過將 event

的 clientX 和 clientY 屬性與該元素的 offsetLeft 和 offsetTop 屬性進(jìn)行比較,就可以算出水平

用戶首先點(diǎn)擊的是這里 被拖動(dòng)后,指針就跑到這里了

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

第639頁

22.5 拖放 621

14

2

3

17

18

13

19

7

8

22

10

11

12

方向和垂直方向上需要多少空間,見圖 22-5。

圖 22-5

為了保存 x 和 y 坐標(biāo)上的差值,還需要幾個(gè)變量。diffX 和 diffY 這些變量需要在 onmousemove

事件處理程序中用到,來對(duì)元素進(jìn)行適當(dāng)?shù)亩ㄎ?,如下面的例子所示?/p>

var DragDrop = function(){

var dragging = null;

diffX = 0;

diffY = 0;

function handleEvent(event){

//獲取事件和目標(biāo)

event = EventUtil.getEvent(event);

var target = EventUtil.getTarget(event);

//確定事件類型

switch(event.type){

case \"mousedown\":

if (target.className.indexOf(\"draggable\") > -1){

dragging = target;

diffX = event.clientX - target.offsetLeft;

diffY = event.clientY - target.offsetTop;

}

break;

case \"mousemove\":

if (dragging !== null){

//指定位置

dragging.style.left = (event.clientX - diffX) + \"px\";

dragging.style.top = (event.clientY - diffY) + \"px\";

}

break;

case \"mouseup\":

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

第640頁

622 第 22 章 高級(jí)技巧

dragging = null;

break;

}

};

//公共接口

return {

enable: function(){

EventUtil.addHandler(document, \"mousedown\", handleEvent);

EventUtil.addHandler(document, \"mousemove\", handleEvent);

EventUtil.addHandler(document, \"mouseup\", handleEvent);

},

disable: function(){

EventUtil.removeHandler(document, \"mousedown\", handleEvent);

EventUtil.removeHandler(document, \"mousemove\", handleEvent);

EventUtil.removeHandler(document, \"mouseup\", handleEvent);

}

}

}();

DragAndDropExample03.htm

diffX 和 diffY 變量是私有的,因?yàn)橹挥?handleEvent()函數(shù)需要用到它們。當(dāng) mousedown 事

件發(fā)生時(shí),通過 clientX 減去目標(biāo)的 offsetLeft,clientY 減去目標(biāo)的 offsetTop,可以計(jì)算到這

兩個(gè)變量的值。當(dāng)觸發(fā)了 mousemove 事件后,就可以使用這些變量從指針坐標(biāo)中減去,得到最終的坐

標(biāo)。最后得到一個(gè)更加平滑的拖動(dòng)體驗(yàn),更加符合用戶所期望的方式。

22.5.2 添加自定義事件

拖放功能還不能真正應(yīng)用起來,除非能知道什么時(shí)候拖動(dòng)開始了。從這點(diǎn)上看,前面的代碼沒有提

供任何方法表示拖動(dòng)開始、正在拖動(dòng)或者已經(jīng)結(jié)束。這時(shí),可以使用自定義事件來指示這幾個(gè)事件的發(fā)

生,讓應(yīng)用的其他部分與拖動(dòng)功能進(jìn)行交互。

由于 DragDrop 對(duì)象是一個(gè)使用了模塊模式的單例,所以需要進(jìn)行一些更改來使用 EventTarget

類型。首先,創(chuàng)建一個(gè)新的 EventTarget 對(duì)象,然后添加 enable()和 disable()方法,最后返回這

個(gè)對(duì)象。看以下內(nèi)容。

var DragDrop = function(){

var dragdrop = new EventTarget(),

dragging = null,

diffX = 0,

diffY = 0;

function handleEvent(event){

//獲取事件和對(duì)象

event = EventUtil.getEvent(event);

var target = EventUtil.getTarget(event);

//確定事件類型

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

第641頁

22.5 拖放 623

14

2

3

17

18

13

19

7

8

22

10

11

12

switch(event.type){

case \"mousedown\":

if (target.className.indexOf(\"draggable\") > -1){

dragging = target;

diffX = event.clientX - target.offsetLeft;

diffY = event.clientY - target.offsetTop;

dragdrop.fire({type:\"dragstart\", target: dragging,

x: event.clientX, y: event.clientY});

}

break;

case \"mousemove\":

if (dragging !== null){

//指定位置

dragging.style.left = (event.clientX - diffX) + \"px\";

dragging.style.top = (event.clientY - diffY) + \"px\";

//觸發(fā)自定義事件

dragdrop.fire({type:\"drag\", target: dragging,

x: event.clientX, y: event.clientY});

}

break;

case \"mouseup\":

dragdrop.fire({type:\"dragend\", target: dragging,

x: event.clientX, y: event.clientY});

dragging = null;

break;

}

};

//公共接口

dragdrop.enable = function(){

EventUtil.addHandler(document, \"mousedown\", handleEvent);

EventUtil.addHandler(document, \"mousemove\", handleEvent);

EventUtil.addHandler(document, \"mouseup\", handleEvent);

};

dragdrop.disable = function(){

EventUtil.removeHandler(document, \"mousedown\", handleEvent);

EventUtil.removeHandler(document, \"mousemove\", handleEvent);

EventUtil.removeHandler(document, \"mouseup\", handleEvent);

};

return dragdrop;

}();

DragAndDropExample04.htm

這段代碼定義了三個(gè)事件:dragstart、drag 和 dragend。它們都將被拖動(dòng)的元素設(shè)置為了 target,

并給出了 x 和 y 屬性來表示當(dāng)前的位置。它們觸發(fā)于 dragdrop 對(duì)象上,之后在返回對(duì)象前給對(duì)象增

加 enable()和 disable()方法。這些模塊模式中的細(xì)小更改令 DragDrop 對(duì)象支持了事件,如下:

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

第642頁

624 第 22 章 高級(jí)技巧

DragDrop.addHandler(\"dragstart\", function(event){

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

status.innerHTML = \"Started dragging \" + event.target.id;

});

DragDrop.addHandler(\"drag\", function(event){

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

status.innerHTML += \"<br/> Dragged \" + event.target.id + \" to (\" + event.x +

\",\" + event.y + \")\";

});

DragDrop.addHandler(\"dragend\", function(event){

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

status.innerHTML += \"<br/> Dropped \" + event.target.id + \" at (\" + event.x +

\",\" + event.y + \")\";

});

DragAndDropExample04.htm

這里,為 DragDrop 對(duì)象的每個(gè)事件添加了事件處理程序。還使用了一個(gè)元素來實(shí)現(xiàn)被拖動(dòng)的元素

當(dāng)前的狀態(tài)和位置。一旦元素被放下了,就可以看到從它一開始被拖動(dòng)之后經(jīng)過的所有的中間步驟。

為 DragDrop 添加自定義事件可以使這個(gè)對(duì)象更健壯,它將可以在網(wǎng)絡(luò)應(yīng)用中處理復(fù)雜的拖放

功能。

22.6 小結(jié)

JavaScript 中的函數(shù)非常強(qiáng)大,因?yàn)樗鼈兪堑谝活悓?duì)象。使用閉包和函數(shù)環(huán)境切換,還可以有很多

使用函數(shù)的強(qiáng)大方法??梢詣?chuàng)建作用域安全的構(gòu)造函數(shù),確保在缺少 new 操作符時(shí)調(diào)用構(gòu)造函數(shù)不會(huì)改

變錯(cuò)誤的環(huán)境對(duì)象。

? 可以使用惰性載入函數(shù),將任何代碼分支推遲到第一次調(diào)用函數(shù)的時(shí)候。

? 函數(shù)綁定可以讓你創(chuàng)建始終在指定環(huán)境中運(yùn)行的函數(shù),同時(shí)函數(shù)柯里化可以讓你創(chuàng)建已經(jīng)填了

某些參數(shù)的函數(shù)。

? 將綁定和柯里化組合起來,就能夠給你一種在任意環(huán)境中以任意參數(shù)執(zhí)行任意函數(shù)的方法。

ECMAScript 5 允許通過以下幾種方式來創(chuàng)建防篡改對(duì)象。

? 不可擴(kuò)展的對(duì)象,不允許給對(duì)象添加新的屬性或方法。

? 密封的對(duì)象,也是不可擴(kuò)展的對(duì)象,不允許刪除已有的屬性和方法。

? 凍結(jié)的對(duì)象,也是密封的對(duì)象,不允許重寫對(duì)象的成員。

JavaScript 中可以使用 setTimeout()和 setInterval()如下創(chuàng)建定時(shí)器。

? 定時(shí)器代碼是放在一個(gè)等待區(qū)域,直到時(shí)間間隔到了之后,此時(shí)將代碼添加到 JavaScript 的處理

隊(duì)列中,等待下一次 JavaScript 進(jìn)程空閑時(shí)被執(zhí)行。

? 每次一段代碼執(zhí)行結(jié)束之后,都會(huì)有一小段空閑時(shí)間進(jìn)行其他瀏覽器處理。

? 這種行為意味著,可以使用定時(shí)器將長(zhǎng)時(shí)間運(yùn)行的腳本切分為一小塊一小塊可以在以后運(yùn)行的

代碼段。這種做法有助于 Web 應(yīng)用對(duì)用戶交互有更積極的響應(yīng)。

JavaScript 中經(jīng)常以事件的形式應(yīng)用觀察者模式。雖然事件常常和 DOM 一起使用,但是你也可以通

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

第643頁

22.6 小結(jié) 625

14

2

3

17

18

13

19

7

8

22

10

11

12

過實(shí)現(xiàn)自定義事件在自己的代碼中應(yīng)用。使用自定義事件有助于將不同部分的代碼相互之間解耦,讓維

護(hù)更加容易,并減少引入錯(cuò)誤的機(jī)會(huì)。

拖放對(duì)于桌面和 Web 應(yīng)用都是一個(gè)非常流行的用戶界面范例,它能夠讓用戶非常方便地以一種直

觀的方式重新排列或者配置東西。在 JavaScrip 中可以使用鼠標(biāo)事件和一些簡(jiǎn)單的計(jì)算來實(shí)現(xiàn)這種功能

類型。將拖放行為和自定義事件結(jié)合起來可以創(chuàng)建一個(gè)可重復(fù)使用的框架,它能應(yīng)用于各種不同的情

況下。

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

第644頁

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

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

本章內(nèi)容

? 進(jìn)行離線檢測(cè)

? 使用離線緩存

? 在瀏覽器中保存數(shù)據(jù)

持離線 Web 應(yīng)用開發(fā)是 HTML5 的另一個(gè)重點(diǎn)。所謂離線 Web 應(yīng)用,就是在設(shè)備不能上

網(wǎng)的情況下仍然可以運(yùn)行的應(yīng)用。HTML5 把離線應(yīng)用作為重點(diǎn),主要是基于開發(fā)人員的

心愿。前端開發(fā)人員一直希望 Web 應(yīng)用能夠與傳統(tǒng)的客戶端應(yīng)用同場(chǎng)競(jìng)技,起碼做到只要設(shè)備有電

就能使用。

開發(fā)離線 Web 應(yīng)用需要幾個(gè)步驟。首先是確保應(yīng)用知道設(shè)備是否能上網(wǎng),以便下一步執(zhí)行正確的

操作。然后,應(yīng)用還必須能訪問一定的資源(圖像、JavaScript、CSS 等),只有這樣才能正常工作。最

后,必須有一塊本地空間用于保存數(shù)據(jù),無論能否上網(wǎng)都不妨礙讀寫。HTML5 及其相關(guān)的 API 讓開發(fā)

離線應(yīng)用成為現(xiàn)實(shí)。

23.1 離線檢測(cè)

開發(fā)離線應(yīng)用的第一步是要知道設(shè)備是在線還是離線,HTML5為此定義了一個(gè)navigator.onLine

屬性,這個(gè)屬性值為 true 表示設(shè)備能上網(wǎng),值為 false 表示設(shè)備離線。這個(gè)屬性的關(guān)鍵是瀏覽器必須

知道設(shè)備能否訪問網(wǎng)絡(luò),從而返回正確的值。實(shí)際應(yīng)用中,navigator.onLine 在不同瀏覽器間還有

些小的差異。

? IE6+和 Safari 5+能夠正確檢測(cè)到網(wǎng)絡(luò)已斷開,并將 navigator.onLine 的值轉(zhuǎn)換為 false。

? Firefox 3+和 Opera 10.6+支持 navigator.onLine 屬性,但你必須手工選中菜單項(xiàng)“文件 → Web

開發(fā)人員(設(shè)置)→ 脫機(jī)工作”才能讓瀏覽器正常工作。

? Chrome 11 及之前版本始終將 navigator.onLine 屬性設(shè)置為 true。這是一個(gè)有待修復(fù)的

bug①。

由于存在上述兼容性問題,單獨(dú)使用 navigator.onLine 屬性不能確定網(wǎng)絡(luò)是否連通。即便如此,

在請(qǐng)求發(fā)生錯(cuò)誤的情況下,檢測(cè)這個(gè)屬性仍然是管用的。以下是檢測(cè)該屬性狀態(tài)的示例。

if (navigator.onLine){

//正常工作

} else {

//執(zhí)行離線狀態(tài)時(shí)的任務(wù)

——————————

① 這個(gè) bug 在 2011 年 10 月已被修復(fù)(http://code.google.com/p/chromium/issues/detail?id=7469)。

第 23 章

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

第645頁

23.2 應(yīng)用緩存 627

1

15

16

4

5

13

6

20

21

9

23

11

12

}

OnLineExample01.htm

除 navigator.onLine 屬性之外,為了更好地確定網(wǎng)絡(luò)是否可用,HTML5 還定義了兩個(gè)事件:

online 和 offline。當(dāng)網(wǎng)絡(luò)從離線變?yōu)樵诰€或者從在線變?yōu)殡x線時(shí),分別觸發(fā)這兩個(gè)事件。這兩個(gè)事

件在 window 對(duì)象上觸發(fā)。

EventUtil.addHandler(window, \"online\", function(){

alert(\"Online\");

});

EventUtil.addHandler(window, \"offline\", function(){

alert(\"Offline\");

});

OnlineEventsExample01.htm

為了檢測(cè)應(yīng)用是否離線,在頁面加載后,最好先通過 navigator.onLine 取得初始的狀態(tài)。然后,

就是通過上述兩個(gè)事件來確定網(wǎng)絡(luò)連接狀態(tài)是否變化。當(dāng)上述事件觸發(fā)時(shí),navigator.onLine 屬性

的值也會(huì)改變,不過必須要手工輪詢這個(gè)屬性才能檢測(cè)到網(wǎng)絡(luò)狀態(tài)的變化。

支持離線檢測(cè)的瀏覽器有 IE 6+(只支持 navigator.onLine 屬性)、Firefox 3、Safari 4、Opera 10.6、

Chrome、iOS 3.2 版 Safari 和 Android 版 WebKit。

23.2 應(yīng)用緩存

HTML5 的應(yīng)用緩存(application cache),或者簡(jiǎn)稱為 appcache,是專門為開發(fā)離線 Web 應(yīng)用而設(shè)計(jì)

的。Appcache 就是從瀏覽器的緩存中分出來的一塊緩存區(qū)。要想在這個(gè)緩存中保存數(shù)據(jù),可以使用一個(gè)

描述文件(manifest file),列出要下載和緩存的資源。下面是一個(gè)簡(jiǎn)單的描述文件示例。

CACHE MANIFEST

#Comment

file.js

file.css

在最簡(jiǎn)單的情況下,描述文件中列出的都是需要下載的資源,以備離線時(shí)使用。

設(shè)置描述文件的選項(xiàng)非常多,本書不打算詳細(xì)解釋每一個(gè)選項(xiàng)。要了解這些選項(xiàng),

推薦讀者閱讀 HTML5Doctor 中的文章“Go offline with application cache”,網(wǎng)址為

http://html5doctor.com/go-offline-with-application-cache。

要將描述文件與頁面關(guān)聯(lián)起來,可以在<html>中的 manifest 屬性中指定這個(gè)文件的路徑,例如:

<html manifest=\"/offline.manifest\">

以上代碼告訴頁面,/offline.manifest 中包含著描述文件。這個(gè)文件的 MIME 類型必須是

text/cache-manifest①。

——————————

① 描述文件的擴(kuò)展名以前推薦用 manifest,但現(xiàn)在推薦的是 appcache。

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

第646頁

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

雖然應(yīng)用緩存的意圖是確保離線時(shí)資源可用,但也有相應(yīng)的 JavaScript API 讓你知道它都在做什么。

這個(gè) API 的核心是 applicationCache 對(duì)象,這個(gè)對(duì)象有一個(gè) status 屬性,屬性的值是常量,表示

應(yīng)用緩存的如下當(dāng)前狀態(tài)。

? 0:無緩存,即沒有與頁面相關(guān)的應(yīng)用緩存。

? 1:閑置,即應(yīng)用緩存未得到更新。

? 2:檢查中,即正在下載描述文件并檢查更新。

? 3:下載中,即應(yīng)用緩存正在下載描述文件中指定的資源。

? 4:更新完成,即應(yīng)用緩存已經(jīng)更新了資源,而且所有資源都已下載完畢,可以通過 swapCache()

來使用了。

? 5:廢棄,即應(yīng)用緩存的描述文件已經(jīng)不存在了,因此頁面無法再訪問應(yīng)用緩存。

應(yīng)用緩存還有很多相關(guān)的事件,表示其狀態(tài)的改變。以下是這些事件。

? checking:在瀏覽器為應(yīng)用緩存查找更新時(shí)觸發(fā)。

? error:在檢查更新或下載資源期間發(fā)生錯(cuò)誤時(shí)觸發(fā)。

? noupdate:在檢查描述文件發(fā)現(xiàn)文件無變化時(shí)觸發(fā)。

? downloading:在開始下載應(yīng)用緩存資源時(shí)觸發(fā)。

? progress:在文件下載應(yīng)用緩存的過程中持續(xù)不斷地觸發(fā)。

? updateready:在頁面新的應(yīng)用緩存下載完畢且可以通過 swapCache()使用時(shí)觸發(fā)。

? cached:在應(yīng)用緩存完整可用時(shí)觸發(fā)。

一般來講,這些事件會(huì)隨著頁面加載按上述順序依次觸發(fā)。不過,通過調(diào)用 update()方法也可以

手工干預(yù),讓應(yīng)用緩存為檢查更新而觸發(fā)上述事件。

applicationCache.update();

update()一經(jīng)調(diào)用,應(yīng)用緩存就會(huì)去檢查描述文件是否更新(觸發(fā) checking 事件),然后就像頁

面剛剛加載一樣,繼續(xù)執(zhí)行后續(xù)操作。如果觸發(fā)了 cached 事件,就說明應(yīng)用緩存已經(jīng)準(zhǔn)備就緒,不會(huì)

再發(fā)生其他操作了。如果觸發(fā)了 updateready 事件,則說明新版本的應(yīng)用緩存已經(jīng)可用,而此時(shí)你需

要調(diào)用 swapCache()來啟用新應(yīng)用緩存。

EventUtil.addHandler(applicationCache, \"updateready\", function(){

applicationCache.swapCache();

});

支持 HTML5 應(yīng)用緩存的瀏覽器有 Firefox 3+、Safari 4+、Opera 10.6、Chrome、iOS 3.2+版 Safari

及 Android 版 WebKit。在 Firefox 4 及之前版本中調(diào)用 swapCache()會(huì)拋出錯(cuò)誤。

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

隨著 Web 應(yīng)用程序的出現(xiàn),也產(chǎn)生了對(duì)于能夠直接在客戶端上存儲(chǔ)用戶信息能力的要求。想法很

合乎邏輯,屬于某個(gè)特定用戶的信息應(yīng)該存在該用戶的機(jī)器上。無論是登錄信息、偏好設(shè)定或其他數(shù)據(jù),

Web 應(yīng)用提供者發(fā)現(xiàn)他們?cè)谡腋鞣N方式將數(shù)據(jù)存在客戶端上。這個(gè)問題的第一個(gè)方案是以 cookie 的形式

出現(xiàn)的,cookie 是原來的網(wǎng)景公司創(chuàng)造的。一份題為“Persistent Client State: HTTP Cookes”(持久客戶

端狀態(tài):HTTP Cookies)的標(biāo)準(zhǔn)中對(duì) cookie 機(jī)制進(jìn)行了闡述(該標(biāo)準(zhǔn)還可以在這里看到:

http://curl.haxx.se/rfc/cookie_spec.html)。今天,cookie 只是在客戶端存儲(chǔ)數(shù)據(jù)的其中一種選項(xiàng)。

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

第647頁

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

1

15

16

4

5

13

6

20

21

9

23

11

12

23.3.1 Cookie

HTTP Cookie,通常直接叫做 cookie,最初是在客戶端用于存儲(chǔ)會(huì)話信息的。該標(biāo)準(zhǔn)要求服務(wù)器對(duì)

任意 HTTP 請(qǐng)求發(fā)送 Set-Cookie HTTP 頭作為響應(yīng)的一部分,其中包含會(huì)話信息。例如,這種服務(wù)器響

應(yīng)的頭可能如下:

HTTP/1.1 200 OK

Content-type: text/html

Set-Cookie: name=value

Other-header: other-header-value

這個(gè) HTTP 響應(yīng)設(shè)置以 name 為名稱、以 value 為值的一個(gè) cookie,名稱和值在傳送時(shí)都必須是

URL 編碼的。瀏覽器會(huì)存儲(chǔ)這樣的會(huì)話信息,并在這之后,通過為每個(gè)請(qǐng)求添加 Cookie HTTP 頭將信

息發(fā)送回服務(wù)器,如下所示:

GET /index.html HTTP/1.1

Cookie: name=value

Other-header: other-header-value

發(fā)送回服務(wù)器的額外信息可以用于唯一驗(yàn)證客戶來自于發(fā)送的哪個(gè)請(qǐng)求。

1. 限制

cookie 在性質(zhì)上是綁定在特定的域名下的。當(dāng)設(shè)定了一個(gè) cookie 后,再給創(chuàng)建它的域名發(fā)送請(qǐng)求時(shí),

都會(huì)包含這個(gè) cookie。這個(gè)限制確保了儲(chǔ)存在 cookie 中的信息只能讓批準(zhǔn)的接受者訪問,而無法被其他

域訪問。

由于 cookie 是存在客戶端計(jì)算機(jī)上的,還加入了一些限制確保 cookie 不會(huì)被惡意使用,同時(shí)不會(huì)占

據(jù)太多磁盤空間。每個(gè)域的 cookie 總數(shù)是有限的,不過瀏覽器之間各有不同。如下所示。

? IE6 以及更低版本限制每個(gè)域名最多 20 個(gè) cookie。

? IE7 和之后版本每個(gè)域名最多 50 個(gè)。IE7 最初是支持每個(gè)域名最大 20 個(gè) cookie,之后被微軟的

一個(gè)補(bǔ)丁所更新。

? Firefox 限制每個(gè)域最多 50 個(gè) cookie。

? Opera 限制每個(gè)域最多 30 個(gè) cookie。

? Safari 和 Chrome 對(duì)于每個(gè)域的 cookie 數(shù)量限制沒有硬性規(guī)定。

當(dāng)超過單個(gè)域名限制之后還要再設(shè)置 cookie,瀏覽器就會(huì)清除以前設(shè)置的 cookie。IE 和 Opera 會(huì)刪

除最近最少使用過的(LRU,Least Recently Used)cookie,騰出空間給新設(shè)置的 cookie。Firefox 看上去

好像是隨機(jī)決定要清除哪個(gè) cookie,所以考慮 cookie 限制非常重要,以免出現(xiàn)不可預(yù)期的后果。

瀏覽器中對(duì)于 cookie 的尺寸也有限制。大多數(shù)瀏覽器都有大約 4096B(加減 1)的長(zhǎng)度限制。為了

最佳的瀏覽器兼容性,最好將整個(gè) cookie 長(zhǎng)度限制在 4095B(含 4095)以內(nèi)。尺寸限制影響到一個(gè)域

下所有的 cookie,而并非每個(gè) cookie 單獨(dú)限制。

如果你嘗試創(chuàng)建超過最大尺寸限制的 cookie,那么該 cookie 會(huì)被悄無聲息地丟掉。注意,雖然一個(gè)

字符通常占用一字節(jié),但是多字節(jié)情況則有不同。

2. cookie 的構(gòu)成

cookie 由瀏覽器保存的以下幾塊信息構(gòu)成。

? 名稱:一個(gè)唯一確定 cookie 的名稱。cookie 名稱是不區(qū)分大小寫的,所以 myCookie 和 MyCookie

被認(rèn)為是同一個(gè) cookie。然而,實(shí)踐中最好將 cookie 名稱看作是區(qū)分大小寫的,因?yàn)槟承┓?wù)

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

第648頁

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

器會(huì)這樣處理 cookie。cookie 的名稱必須是經(jīng)過 URL 編碼的。

? 值:儲(chǔ)存在 cookie 中的字符串值。值必須被 URL 編碼。

? 域:cookie 對(duì)于哪個(gè)域是有效的。所有向該域發(fā)送的請(qǐng)求中都會(huì)包含這個(gè) cookie 信息。這個(gè)值

可以包含子域(subdomain,如www.wrox.com),也可以不包含它(如.wrox.com,則對(duì)于wrox.com

的所有子域都有效)。如果沒有明確設(shè)定,那么這個(gè)域會(huì)被認(rèn)作來自設(shè)置 cookie 的那個(gè)域。

? 路徑:對(duì)于指定域中的那個(gè)路徑,應(yīng)該向服務(wù)器發(fā)送 cookie。例如,你可以指定 cookie 只有從

http://www.wrox.com/books/ 中才能訪問,那么 http://www.wrox.com 的頁面就不會(huì)發(fā)

送 cookie 信息,即使請(qǐng)求都是來自同一個(gè)域的。

? 失效時(shí)間:表示 cookie 何時(shí)應(yīng)該被刪除的時(shí)間戳(也就是,何時(shí)應(yīng)該停止向服務(wù)器發(fā)送這個(gè)

cookie)。默認(rèn)情況下,瀏覽器會(huì)話結(jié)束時(shí)即將所有 cookie 刪除;不過也可以自己設(shè)置刪除時(shí)間。

這個(gè)值是個(gè) GMT 格式的日期(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定應(yīng)該刪除

cookie 的準(zhǔn)確時(shí)間。因此,cookie 可在瀏覽器關(guān)閉后依然保存在用戶的機(jī)器上。如果你設(shè)置的失

效日期是個(gè)以前的時(shí)間,則 cookie 會(huì)被立刻刪除。

? 安全標(biāo)志:指定后,cookie 只有在使用 SSL 連接的時(shí)候才發(fā)送到服務(wù)器。例如,cookie 信息只

能發(fā)送給 https://www.wrox.com,而 http://www.wrox.com 的請(qǐng)求則不能發(fā)送 cookie。

每一段信息都作為 Set-Cookie 頭的一部分,使用分號(hào)加空格分隔每一段,如下例所示。

HTTP/1.1 200 OK

Content-type: text/html

Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com

Other-header: other-header-value

該頭信息指定了一個(gè)叫做 name 的 cookie,它會(huì)在格林威治時(shí)間 2007 年 1 月 22 日 7:10:24 失效,同

時(shí)對(duì)于 www.wrox.com 和 wrox.com 的任何子域(如 p2p.wrox.com)都有效。

secure 標(biāo)志是 cookie 中唯一一個(gè)非名值對(duì)兒的部分,直接包含一個(gè) secure 單詞。如下:

HTTP/1.1 200 OK

Content-type: text/html

Set-Cookie: name=value; domain=.wrox.com; path=/; secure

Other-header: other-header-value

這里,創(chuàng)建了一個(gè)對(duì)于所有 wrox.com 的子域和域名下(由 path 參數(shù)指定的)所有頁面都有效的

cookie。因?yàn)樵O(shè)置了 secure 標(biāo)志,這個(gè) cookie 只能通過 SSL 連接才能傳輸。

尤其要注意,域、路徑、失效時(shí)間和 secure 標(biāo)志都是服務(wù)器給瀏覽器的指示,以指定何時(shí)應(yīng)該發(fā)

送 cookie。這些參數(shù)并不會(huì)作為發(fā)送到服務(wù)器的 cookie 信息的一部分,只有名值對(duì)兒才會(huì)被發(fā)送。

3. JavaScript 中的 cookie

在JavaScript中處理cookie有些復(fù)雜,因?yàn)槠浔娝苤孽磕_的接口,即BOM的document. cookie

屬性。這個(gè)屬性的獨(dú)特之處在于它會(huì)因?yàn)槭褂盟姆绞讲煌憩F(xiàn)出不同的行為。當(dāng)用來獲取屬性值時(shí),

document.cookie 返回當(dāng)前頁面可用的(根據(jù) cookie 的域、路徑、失效時(shí)間和安全設(shè)置)所有 cookie

的字符串,一系列由分號(hào)隔開的名值對(duì)兒,如下例所示。

name1=value1;name2=value2;name3=value3

所有名字和值都是經(jīng)過 URL 編碼的,所以必須使用 decodeURIComponent()來解碼。

當(dāng)用于設(shè)置值的時(shí)候,document.cookie 屬性可以設(shè)置為一個(gè)新的 cookie 字符串。這個(gè) cookie 字

符串會(huì)被解釋并添加到現(xiàn)有的 cookie 集合中。設(shè)置 document.cookie 并不會(huì)覆蓋 cookie,除非設(shè)置的

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

第649頁

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

1

15

16

4

5

13

6

20

21

9

23

11

12

cookie 的名稱已經(jīng)存在。設(shè)置 cookie 的格式如下,和 Set-Cookie 頭中使用的格式一樣。

name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure

這些參數(shù)中,只有 cookie 的名字和值是必需的。下面是一個(gè)簡(jiǎn)單的例子。

document.cookie = \"name=Nicholas\";

這段代碼創(chuàng)建了一個(gè)叫 name 的 cookie,值為 Nicholas。當(dāng)客戶端每次向服務(wù)器端發(fā)送請(qǐng)求的時(shí)

候,都會(huì)發(fā)送這個(gè) cookie;當(dāng)瀏覽器關(guān)閉的時(shí)候,它就會(huì)被刪除。雖然這段代碼沒問題,但因?yàn)檫@里正

好名稱和值都無需編碼,所以最好每次設(shè)置 cookie 時(shí)都像下面這個(gè)例子中一樣使用 encodeURIComponent()。

document.cookie = encodeURIComponent(\"name\") + \"=\" +

encodeURIComponent(\"Nicholas\");

要給被創(chuàng)建的 cookie 指定額外的信息,只要將參數(shù)追加到該字符串,和 Set-Cookie 頭中的格式

一樣,如下所示。

document.cookie = encodeURIComponent(\"name\") + \"=\" +

encodeURIComponent(\"Nicholas\") + \"; domain=.wrox.com; path=/\";

由于 JavaScript 中讀寫 cookie 不是非常直觀,常常需要寫一些函數(shù)來簡(jiǎn)化 cookie 的功能?;镜?/p>

cookie 操作有三種:讀取、寫入和刪除。它們?cè)?CookieUtil 對(duì)象中如下表示。

var CookieUtil = {

get: function (name){

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

cookieStart = document.cookie.indexOf(cookieName),

cookieValue = null;

if (cookieStart > -1){

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

if (cookieEnd == -1){

cookieEnd = document.cookie.length;

}

cookieValue = decodeURIComponent(document.cookie.substring(cookieStart

+ cookieName.length, cookieEnd));

}

return cookieValue;

},

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

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

encodeURIComponent(value);

if (expires instanceof Date) {

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

}

if (path) {

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

}

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

第650頁

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

if (domain) {

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

}

if (secure) {

cookieText += \"; secure\";

}

document.cookie = cookieText;

},

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

this.set(name, \"\", new Date(0), path, domain, secure);

}

};

CookieUtil.js

CookieUtil.get()方法根據(jù) cookie 的名字獲取相應(yīng)的值。它會(huì)在 document.cookie 字符串中查

找 cookie 名加上等于號(hào)的位置。如果找到了,那么使用 indexOf()查找該位置之后的第一個(gè)分號(hào)(表

示了該 cookie 的結(jié)束位置)。如果沒有找到分號(hào),則表示該 cookie 是字符串中的最后一個(gè),則余下的字

符串都是 cookie 的值。該值使用 decodeURIComponent()進(jìn)行解碼并最后返回。如果沒有發(fā)現(xiàn) cookie,

則返回 null。

CookieUtil.set()方法在頁面上設(shè)置一個(gè) cookie,接收如下幾個(gè)參數(shù):cookie 的名稱,cookie 的值,

可選的用于指定 cookie 何時(shí)應(yīng)被刪除的 Date 對(duì)象,cookie 的可選的 URL 路徑,可選的域,以及可選的

表示是否要添加 secure 標(biāo)志的布爾值。參數(shù)是按照它們的使用頻率排列的,只有頭兩個(gè)是必需的。在

這個(gè)方法中,名稱和值都使用encodeURIComponent()進(jìn)行了URL編碼,并檢查其他選項(xiàng)。如果expires

參數(shù)是 Date 對(duì)象,那么會(huì)使用 Date 對(duì)象的 toGMTString()方法正確格式化 Date 對(duì)象,并添加到

expires 選項(xiàng)上。方法的其他部分就是構(gòu)造 cookie 字符串并將其設(shè)置到 document.cookie 中。

沒有刪除已有 cookie 的直接方法。所以,需要使用相同的路徑、域和安全選項(xiàng)再次設(shè)置 cookie,并

將失效時(shí)間設(shè)置為過去的時(shí)間。CookieUtil.unset()方法可以處理這種事情。它接收 4 個(gè)參數(shù):要?jiǎng)h

除的 cookie 的名稱、可選的路徑參數(shù)、可選的域參數(shù)和可選的安全參數(shù)。

這些參數(shù)加上空字符串并設(shè)置失效時(shí)間為 1970 年 1 月 1 日(初始化為 0ms 的 Date 對(duì)象的值),傳

給 CookieUtil.set()。這樣就能確保刪除 cookie。

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

//設(shè)置 cookie

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

CookieUtil.set(\"book\", \"Professional JavaScript\");

//讀取 cookie 的值

alert(CookieUtil.get(\"name\")); //\"Nicholas\"

alert(CookieUtil.get(\"book\")); //\"Professional JavaScript\"

//刪除 cookie

CookieUtil.unset(\"name\");

CookieUtil.unset(\"book\");

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

百萬用戶使用云展網(wǎng)進(jìn)行書冊(cè)翻頁效果制作,只要您有文檔,即可一鍵上傳,自動(dòng)生成鏈接和二維碼(獨(dú)立電子書),支持分享到微信和網(wǎng)站!
收藏
轉(zhuǎn)發(fā)
下載
免費(fèi)制作
其他案例
更多案例
免費(fèi)制作
x
{{item.desc}}
下載
{{item.title}}
{{toast}}