<h2 id="6.1">瀏覽器的JavaScript引擎</h2>
## 瀏覽器的組成
瀏覽器的核心是兩部分:渲染引擎和JavaScript解釋器(又稱JavaScript引擎)。
(1)渲染引擎
渲染引擎的主要作用是,將網頁從代碼”渲染“為用戶視覺上可以感知的平面文檔。不同的瀏覽器有不同的渲染引擎。
- Firefox:Gecko引擎
- Safari:WebKit引擎
- Chrome:Blink引擎
渲染引擎處理網頁,通常分成四個階段。
1. 解析代碼:HTML代碼解析為DOM,CSS代碼解析為CSSOM(CSS Object Model)
1. 對象合成:將DOM和CSSOM合成一棵渲染樹(render tree)
1. 布局:計算出渲染樹的布局(layout)
1. 繪制:將渲染樹繪制到屏幕
以上四步并非嚴格按順序執行,往往第一步還沒完成,第二步和第三步就已經開始了。所以,會看到這種情況:網頁的HTML代碼還沒下載完,但瀏覽器已經顯示出內容了。
(2)JavaScript引擎
JavaScript引擎的主要作用是,讀取網頁中的JavaScript代碼,對其處理后運行。
本節主要介紹JavaScript引擎的工作方式。
## JavaScript代碼嵌入網頁的方法
JavaScript代碼只有嵌入網頁,才能運行。網頁中嵌入JavaScript代碼有多種方法。
### 直接添加代碼塊
通過`<script>`標簽,可以直接將JavaScript代碼嵌入網頁。
```html
<script>
// some JavaScript code
</script>
```
`<script>`標簽有一個`type`屬性,用來指定腳本類型。不過,如果嵌入的是JavaScript腳本,HTML5推薦`type`屬性。
對JavaScript腳本來說,`type`屬性可以設為兩種值。
- `text/javascript`:這是默認值,也是歷史上一貫設定的值。如果你省略`type`屬性,默認就是這個值。對于老式瀏覽器,設為這個值比較好。
- `application/javascript`:對于較新的瀏覽器,建議設為這個值。
### 加載外部腳本
`script`標簽也可以指定加載外部的腳本文件。
```html
<script src="example.js"></script>
```
如果腳本文件使用了非英語字符,還應該注明編碼。
```html
<script charset="utf-8" src="example.js"></script>
```
加載外部腳本和直接添加代碼塊,這兩種方法不能混用。下面代碼的`console.log`語句直接被忽略。
```html
<script charset="utf-8" src="example.js">
console.log('Hello World!');
</script>
```
為了防止攻擊者篡改外部腳本,`script`標簽允許設置一個`integrity`屬性,寫入該外部腳本的Hash簽名,用來驗證腳本的一致性。
```html
<script src="/assets/application.js"
integrity="sha256-TvVUHzSfftWg1rcfL6TIJ0XKEGrgLyEq6lEpcmrG9qs=">
</script>
```
上面代碼中,`script`標簽有一個`integrity`屬性,指定了外部腳本`/assets/application.js`的SHA265簽名。一旦有人改了這個腳本,導致SHA265簽名不匹配,瀏覽器就會拒絕加載。
除了JavaScript腳本,外部的CSS樣式表也可以設置這個屬性。
### 行內代碼
除了上面兩種方法,HTML語言允許在某些元素的事件屬性和`a`元素的`href`屬性中,直接寫入JavaScript。
```html
<div onclick="alert('Hello')"></div>
<a href="javascript:alert('Hello')"></a>
```
這種寫法將HTML代碼與JavaScript代碼混寫在一起,非常不利于代碼管理,不建議使用。
## script標簽的工作原理
正常的網頁加載流程是這樣的。
1. 瀏覽器一邊下載HTML網頁,一邊開始解析
1. 解析過程中,發現script標簽
1. 暫停解析,網頁渲染的控制權轉交給JavaScript引擎
1. 如果script標簽引用了外部腳本,就下載該腳本,否則就直接執行
1. 執行完畢,控制權交還渲染引擎,恢復往下解析HTML網頁
也就是說,加載外部腳本時,瀏覽器會暫停頁面渲染,等待腳本下載并執行完成后,再繼續渲染。原因是JavaScript可以修改DOM(比如使用`document.write`方法),所以必須把控制權讓給它,否則會導致復雜的線程競賽的問題。
如果外部腳本加載時間很長(比如一直無法完成下載),就會造成網頁長時間失去響應,瀏覽器就會呈現“假死”狀態,這被稱為“阻塞效應”。
為了避免這種情況,較好的做法是將script標簽都放在頁面底部,而不是頭部。這樣即使遇到腳本失去響應,網頁主體的渲染也已經完成了,用戶至少可以看到內容,而不是面對一張空白的頁面。
如果某些腳本代碼非常重要,一定要放在頁面頭部的話,最好直接將代碼嵌入頁面,而不是連接外部腳本文件,這樣能縮短加載時間。
將腳本文件都放在網頁尾部加載,還有一個好處。在DOM結構生成之前就調用DOM,JavaScript會報錯,如果腳本都在網頁尾部加載,就不存在這個問題,因為這時DOM肯定已經生成了。
```html
<head>
<script>
console.log(document.body.innerHTML);
</script>
</head>
```
上面代碼執行時會報錯,因為此時`body`元素還未生成。
一種解決方法是設定`DOMContentLoaded`事件的回調函數。
```html
<head>
<script>
document.addEventListener(
'DOMContentLoaded',
function(event) {
console.log(document.body.innerHTML);
}
);
</script>
</head>
```
另一種解決方法是,使用`script`標簽的`onload`屬性。當script標簽指定的外部腳本文件下載和解析完成,會觸發一個load事件,可以把所需執行的代碼,放在這個事件的回調函數里面。
```html
<script src="jquery.min.js" onload="console.log(document.body.innerHTML)">
</script>
```
但是,如果將腳本放在頁面底部,就可以完全按照正常的方式寫,上面兩種方式都不需要。
```html
<body>
<!-- 其他代碼 -->
<script>
console.log(document.body.innerHTML);
</script>
</body>
```
如果有多個script標簽,比如下面這樣。
```html
<script src="1.js"></script>
<script src="2.js"></script>
```
瀏覽器會同時平行下載`1.js`和`2.js`,但是,執行時會保證先執行`1.js`,然后再執行`2.js`,即使后者先下載完成,也是如此。也就是說,腳本的執行順序由它們在頁面中的出現順序決定,這是為了保證腳本之間的依賴關系不受到破壞。
當然,加載這兩個腳本都會產生“阻塞效應”,必須等到它們都加載完成,瀏覽器才會繼續頁面渲染。
Gecko和Webkit引擎在網頁被阻塞后,會生成第二個線程解析文檔,下載外部資源,但是不會修改DOM,網頁還是處于阻塞狀態。
解析和執行CSS,也會產生阻塞。Firefox會等到腳本前面的所有樣式表,都下載并解析完,再執行腳本;Webkit則是一旦發現腳本引用了樣式,就會暫停執行腳本執行,等到樣式表下載并解析完,再恢復執行。
此外,對于來自同一個域名的資源,比如腳本文件、樣式表文件、圖片文件等,瀏覽器一般最多同時下載六個(IE11允許同時下載13個)。如果是來自不同域名的資源,就沒有這個限制。所以,通常把靜態文件放在不同的域名之下,以加快下載速度。
## defer屬性
為了解決腳本文件下載阻塞網頁渲染的問題,一個方法是加入defer屬性。
```html
<script src="1.js" defer></script>
<script src="2.js" defer></script>
```
`defer`屬性的作用是,告訴瀏覽器,等到DOM加載完成后,再執行指定腳本。
1. 瀏覽器開始解析HTML網頁
2. 解析過程中,發現帶有`defer`屬性的script標簽
3. 瀏覽器繼續往下解析HTML網頁,同時并行下載script標簽中的外部腳本
4. 瀏覽器完成解析HTML網頁,此時再執行下載的腳本
有了`defer`屬性,瀏覽器下載腳本文件的時候,不會阻塞頁面渲染。下載的腳本文件在`DOMContentLoaded`事件觸發前執行(即剛剛讀取完`</html>`標簽),而且可以保證執行順序就是它們在頁面上出現的順序。
對于內置而不是連接外部腳本的script標簽,以及動態生成的script標簽,`defer`屬性不起作用。
## async屬性
解決“阻塞效應”的另一個方法是加入`async`屬性。
```html
<script src="1.js" async></script>
<script src="2.js" async></script>
```
`async`屬性的作用是,使用另一個進程下載腳本,下載時不會阻塞渲染。
1. 瀏覽器開始解析HTML網頁
2. 解析過程中,發現帶有`async`屬性的`script`標簽
3. 瀏覽器繼續往下解析HTML網頁,同時并行下載`script`標簽中的外部腳本
4. 腳本下載完成,瀏覽器暫停解析HTML網頁,開始執行下載的腳本
5. 腳本執行完畢,瀏覽器恢復解析HTML網頁
`async`屬性可以保證腳本下載的同時,瀏覽器繼續渲染。需要注意的是,一旦采用這個屬性,就無法保證腳本的執行順序。哪個腳本先下載結束,就先執行那個腳本。另外,使用`async`屬性的腳本文件中,不應該使用`document.write`方法。
`defer`屬性和`async`屬性到底應該使用哪一個?
一般來說,如果腳本之間沒有依賴關系,就使用`async`屬性,如果腳本之間有依賴關系,就使用`defer`屬性。如果同時使用`async`和`defer`屬性,后者不起作用,瀏覽器行為由`async`屬性決定。
## 重流和重繪
渲染樹轉換為網頁布局,稱為“布局流”(flow);布局顯示到頁面的這個過程,稱為“繪制”(paint)。它們都具有阻塞效應,并且會耗費很多時間和計算資源。
頁面生成以后,腳本操作和樣式表操作,都會觸發重流(reflow)和重繪(repaint)。用戶的互動,也會觸發,比如設置了鼠標懸停(`a:hover`)效果、頁面滾動、在輸入框中輸入文本、改變窗口大小等等。
重流和重繪并不一定一起發生,重流必然導致重繪,重繪不一定需要重流。比如改變元素顏色,只會導致重繪,而不會導致重流;改變元素的布局,則會導致重繪和重流。
大多數情況下,瀏覽器會智能判斷,將“重流”和“重繪”只限制到相關的子樹上面,最小化所耗費的代價,而不會全局重新生成網頁。
作為開發者,應該盡量設法降低重繪的次數和成本。比如,盡量不要變動高層的DOM元素,而以底層DOM元素的變動代替;再比如,重繪table布局和flex布局,開銷都會比較大。
```javascript
var foo = document.getElementById(‘foobar’);
foo.style.color = ‘blue’;
foo.style.marginTop = ‘30px’;
```
上面的代碼只會導致一次重繪,因為瀏覽器會累積DOM變動,然后一次性執行。
下面的代碼則會導致兩次重繪。
```javascript
var foo = document.getElementById(‘foobar’);
foo.style.color = ‘blue’;
var margin = parseInt(foo.style.marginTop);
foo.style.marginTop = (margin + 10) + ‘px’;
```
下面是一些優化技巧。
- 讀取DOM或者寫入DOM,盡量寫在一起,不要混雜
- 緩存DOM信息
- 不要一項一項地改變樣式,而是使用CSS class一次性改變樣式
- 使用document fragment操作DOM
- 動畫時使用absolute定位或fixed定位,這樣可以減少對其他元素的影響
- 只在必要時才顯示元素
- 使用`window.requestAnimationFrame()`,因為它可以把代碼推遲到下一次重流時執行,而不是立即要求頁面重流
- 使用虛擬DOM(virtual DOM)庫
下面是一個`window.requestAnimationFrame()`對比效果的例子。
```javascript
// 重繪代價高
function doubleHeight(element) {
var currentHeight = element.clientHeight;
element.style.height = (currentHeight * 2) + ‘px’;
}
all_my_elements.forEach(doubleHeight);
// 重繪代價低
function doubleHeight(element) {
var currentHeight = element.clientHeight;
window.requestAnimationFrame(function () {
element.style.height = (currentHeight * 2) + ‘px’;
});
}
all_my_elements.forEach(doubleHeight);
```
## 腳本的動態嵌入
除了用靜態的`script`標簽,還可以動態嵌入`script`標簽。
```javascript
['1.js', '2.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
```
這種方法的好處是,動態生成的`script`標簽不會阻塞頁面渲染,也就不會造成瀏覽器假死。但是問題在于,這種方法無法保證腳本的執行順序,哪個腳本文件先下載完成,就先執行哪個。
如果想避免這個問題,可以設置async屬性為`false`。
```javascript
['1.js', '2.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
```
上面的代碼依然不會阻塞頁面渲染,而且可以保證`2.js`在`1.js`后面執行。不過需要注意的是,在這段代碼后面加載的腳本文件,會因此都等待`2.js`執行完成后再執行。
我們可以把上面的寫法,封裝成一個函數。
```javascript
(function() {
var scripts = document.getElementsByTagName('script')[0];
function load(url) {
var script = document.createElement('script');
script.async = true;
script.src = url;
scripts.parentNode.insertBefore(script, scripts);
}
load('//apis.google.com/js/plusone.js');
load('//platform.twitter.com/widgets.js');
load('//s.thirdpartywidget.com/widget.js');
}());
```
上面代碼中,`async`屬性設為`true`,是因為加載的腳本沒有互相依賴關系。而且,這樣就不會造成堵塞。
此外,動態嵌入還有一個地方需要注意。動態嵌入必須等待CSS文件加載完成后,才會去下載外部腳本文件。靜態加載就不存在這個問題,`script`標簽指定的外部腳本文件,都是與CSS文件同時并發下載的。
## 加載使用的協議
如果不指定協議,瀏覽器默認采用HTTP協議下載。
```html
<script src="example.js"></script>
```
上面的`example.js`默認就是采用HTTP協議下載,如果要采用HTTPs協議下載,必需寫明(假定服務器支持)。
```html
<script src="https://example.js"></script>
```
但是有時我們會希望,根據頁面本身的協議來決定加載協議,這時可以采用下面的寫法。
```html
<script src="//example.js"></script>
```
## JavaScript虛擬機
JavaScript是一種解釋型語言,也就是說,它不需要編譯,可以由解釋器實時運行。這樣的好處是運行和修改都比較方便,刷新頁面就可以重新解釋;缺點是每次運行都要調用解釋器,系統開銷較大,運行速度慢于編譯型語言。為了提高運行速度,目前的瀏覽器都將JavaScript進行一定程度的編譯,生成類似字節碼(bytecode)的中間代碼,以提高運行速度。
早期,瀏覽器內部對JavaScript的處理過程如下:
1. 讀取代碼,進行詞法分析(Lexical analysis),將代碼分解成詞元(token)。
2. 對詞元進行語法分析(parsing),將代碼整理成“語法樹”(syntax tree)。
3. 使用“翻譯器”(translator),將代碼轉為字節碼(bytecode)。
4. 使用“字節碼解釋器”(bytecode interpreter),將字節碼轉為機器碼。
逐行解釋將字節碼轉為機器碼,是很低效的。為了提高運行速度,現代瀏覽器改為采用“即時編譯”(Just In Time compiler,縮寫JIT),即字節碼只在運行時編譯,用到哪一行就編譯哪一行,并且把編譯結果緩存(inline cache)。通常,一個程序被經常用到的,只是其中一小部分代碼,有了緩存的編譯結果,整個程序的運行速度就會顯著提升。
不同的瀏覽器有不同的編譯策略。有的瀏覽器只編譯最經常用到的部分,比如循環的部分;有的瀏覽器索性省略了字節碼的翻譯步驟,直接編譯成機器碼,比如chrome瀏覽器的V8引擎。
字節碼不能直接運行,而是運行在一個虛擬機(Virtual Machine)之上,一般也把虛擬機稱為JavaScript引擎。因為JavaScript運行時未必有字節碼,所以JavaScript虛擬機并不完全基于字節碼,而是部分基于源碼,即只要有可能,就通過JIT(just in time)編譯器直接把源碼編譯成機器碼運行,省略字節碼步驟。這一點與其他采用虛擬機(比如Java)的語言不盡相同。這樣做的目的,是為了盡可能地優化代碼、提高性能。下面是目前最常見的一些JavaScript虛擬機:
- [Chakra](http://en.wikipedia.org/wiki/Chakra_(JScript_engine\))(Microsoft Internet Explorer)
- [Nitro/JavaScript Core](http://en.wikipedia.org/wiki/WebKit#JavaScriptCore) (Safari)
- [Carakan](http://dev.opera.com/articles/view/labs-carakan/) (Opera)
- [SpiderMonkey](https://developer.mozilla.org/en-US/docs/SpiderMonkey) (Firefox)
- [V8](http://en.wikipedia.org/wiki/V8_(JavaScript_engine\)) (Chrome, Chromium)
## 單線程模型
### 含義
首先,明確一個觀念:JavaScript只在一個線程上運行,不代表JavaScript引擎只有一個線程。事實上,JavaScript引擎有多個線程,其中單個腳本只能在一個線程上運行,其他線程都是在后臺配合。JavaScript腳本在一個線程里運行。這意味著,一次只能運行一個任務,其他任務都必須在后面排隊等待。
JavaScript之所以采用單線程,而不是多線程,跟歷史有關系。JavaScript從誕生起就是單線程,原因是不想讓瀏覽器變得太復雜,因為多線程需要共享資源、且有可能修改彼此的運行結果,對于一種網頁腳本語言來說,這就太復雜了。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質。
單線程模型帶來了一些問題,主要是新的任務被加在隊列的尾部,只有前面的所有任務運行結束,才會輪到它執行。如果有一個任務特別耗時,后面的任務都會停在那里等待,造成瀏覽器失去響應,又稱“假死”。為了避免“假死”,當某個操作在一定時間后仍無法結束,瀏覽器就會跳出提示框,詢問用戶是否要強行停止腳本運行。
如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑著的,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等著結果出來,再往下執行。JavaScript語言的設計者意識到,這時CPU完全可以不管IO設備,掛起處于等待中的任務,先運行排在后面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。這種機制就是JavaScript內部采用的Event Loop。
### 消息隊列
JavaScript運行時,除了一根運行線程,系統還提供一個消息隊列(message queue),里面是各種需要當前程序處理的消息。新的消息進入隊列的時候,會自動排在隊列的尾端。
運行線程只要發現消息隊列不為空,就會取出排在第一位的那個消息,執行它對應的回調函數。等到執行完,再取出排在第二位的消息,不斷循環,直到消息隊列變空為止。
每條消息與一個回調函數相聯系,也就是說,程序只要收到這條消息,就會執行對應的函數。另一方面,進入消息隊列的消息,必須有對應的回調函數。否則這個消息就會遺失,不會進入消息隊列。舉例來說,鼠標點擊就會產生一條消息,報告`click`事件發生了。如果沒有回調函數,這個消息就遺失了。如果有回調函數,這個消息進入消息隊列。等到程序收到這個消息,就會執行click事件的回調函數。
另一種情況是`setTimeout`會在指定時間向消息隊列添加一條消息。如果消息隊列之中,此時沒有其他消息,這條消息會立即得到處理;否則,這條消息會不得不等到其他消息處理完,才會得到處理。因此,`setTimeout`指定的執行時間,只是一個最早可能發生的時間,并不能保證一定會在那個時間發生。
一旦當前執行棧空了,消息隊列就會取出排在第一位的那條消息,傳入程序。程序開始執行對應的回調函數,等到執行完,再處理下一條消息。
### Event Loop
所謂Event Loop,指的是一種內部循環,用來一輪又一輪地處理消息隊列之中的消息,即執行對應的回調函數。[Wikipedia](http://en.wikipedia.org/wiki/Event_loop)的定義是:“**Event Loop是一個程序結構,用于等待和發送消息和事件**(a programming construct that waits for and dispatches events or messages in a program)”。可以就把Event Loop理解成動態更新的消息隊列本身。
下面是一些常見的JavaScript任務。
- 執行JavaScript代碼
- 對用戶的輸入(包含鼠標點擊、鍵盤輸入等等)做出反應
- 處理異步的網絡請求
所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在JavaScript執行進程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;異步任務指的是,不進入JavaScript執行進程、而進入“任務隊列”(task queue)的任務,只有“任務隊列”通知主進程,某個異步任務可以執行了,該任務(采用回調函數的形式)才會進入JavaScript進程執行。
以Ajax操作為例,它可以當作同步任務處理,也可以當作異步任務處理,由開發者決定。如果是同步任務,主線程就等著Ajax操作返回結果,再往下執行;如果是異步任務,該任務直接進入“任務隊列”,JavaScript進程跳過Ajax操作,直接往下執行,等到Ajax操作有了結果,JavaScript進程再執行對應的回調函數。
也就是說,雖然JavaScript只有一根進程用來執行,但是并行的還有其他進程(比如,處理定時器的進程、處理用戶輸入的進程、處理網絡通信的進程等等)。這些進程通過向任務隊列添加任務,實現與JavaScript進程通信。
想要理解Event Loop,就要從程序的運行模式講起。運行以后的程序叫做"進程"(process),一般情況下,一個進程一次只能執行一個任務。如果有很多任務需要執行,不外乎三種解決方法。
1. **排隊。**因為一個進程一次只能執行一個任務,只好等前面的任務執行完了,再執行后面的任務。
2. **新建進程。**使用fork命令,為每個任務新建一個進程。
3. **新建線程。**因為進程太耗費資源,所以如今的程序往往允許一個進程包含多個線程,由線程去完成任務。
如果某個任務很耗時,比如涉及很多I/O(輸入/輸出)操作,那么線程的運行大概是下面的樣子。

上圖的綠色部分是程序的運行時間,紅色部分是等待時間。可以看到,由于I/O操作很慢,所以這個線程的大部分運行時間都在空等I/O操作的返回結果。這種運行方式稱為"同步模式"(synchronous I/O)。
如果采用多線程,同時運行多個任務,那很可能就是下面這樣。

上圖表明,多線程不僅占用多倍的系統資源,也閑置多倍的資源,這顯然不合理。

上圖主線程的綠色部分,還是表示運行時間,而橙色部分表示空閑時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,然后接著往后運行,所以不存在紅色的等待時間。等到I/O程序完成操作,Event Loop線程再把結果返回主線程。主線程就調用事先設定的回調函數,完成整個任務。
可以看到,由于多出了橙色的空閑時間,所以主線程得以運行更多的任務,這就提高了效率。這種運行方式稱為"[異步模式](http://en.wikipedia.org/wiki/Asynchronous_I/O)"(asynchronous I/O)。
這正是JavaScript語言的運行方式。單線程模型雖然對JavaScript構成了很大的限制,但也因此使它具備了其他語言不具備的優勢。如果部署得好,JavaScript程序是不會出現堵塞的,這就是為什么node.js平臺可以用很少的資源,應付大流量訪問的原因。
如果有大量的異步任務(實際情況就是這樣),它們會在“消息隊列”中產生大量的消息。這些消息排成隊,等候進入主線程。本質上,“消息隊列”就是一個“先進先出”的數據結構。比如,點擊鼠標就產生一系列消息(各種事件),`mousedown`事件排在`mouseup`事件前面,`mouseup`事件又排在`click`事件的前面。
<h2 id="6.2">定時器</h2>
JavaScript提供定時執行代碼的功能,叫做定時器(timer),主要由`setTimeout()`和`setInterval()`這兩個函數來完成。它們向任務隊列添加定時任務。
## setTimeout()
`setTimeout`函數用來指定某個函數或某段代碼,在多少毫秒之后執行。它返回一個整數,表示定時器的編號,以后可以用來取消這個定時器。
```javascript
var timerId = setTimeout(func|code, delay)
```
上面代碼中,`setTimeout`函數接受兩個參數,第一個參數`func|code`是將要推遲執行的函數名或者一段代碼,第二個參數`delay`是推遲執行的毫秒數。
```javascript
console.log(1);
setTimeout('console.log(2)',1000);
console.log(3);
```
上面代碼的輸出結果就是1,3,2,因為`setTimeout`指定第二行語句推遲1000毫秒再執行。
需要注意的是,推遲執行的代碼必須以字符串的形式,放入setTimeout,因為引擎內部使用eval函數,將字符串轉為代碼。如果推遲執行的是函數,則可以直接將函數名,放入setTimeout。一方面eval函數有安全顧慮,另一方面為了便于JavaScript引擎優化代碼,setTimeout方法一般總是采用函數名的形式,就像下面這樣。
```javascript
function f(){
console.log(2);
}
setTimeout(f,1000);
// 或者
setTimeout(function (){console.log(2)},1000);
```
如果省略`setTimeout`的第二個參數,則該參數默認為0。
除了前兩個參數,setTimeout還允許添加更多的參數。它們將被傳入推遲執行的函數(回調函數)。
```javascript
setTimeout(function(a,b){
console.log(a+b);
},1000,1,1);
```
上面代碼中,setTimeout共有4個參數。最后那兩個參數,將在1000毫秒之后回調函數執行時,作為回調函數的參數。
IE 9.0及以下版本,只允許setTimeout有兩個參數,不支持更多的參數。這時有三種解決方法。第一種是在一個匿名函數里面,讓回調函數帶參數運行,再把匿名函數輸入setTimeout。
```javascript
setTimeout(function() {
myFunc("one", "two", "three");
}, 1000);
```
上面代碼中,myFunc是真正要推遲執行的函數,有三個參數。如果直接放入setTimeout,低版本的IE不能帶參數,所以可以放在一個匿名函數。
第二種解決方法是使用bind方法,把多余的參數綁定在回調函數上面,生成一個新的函數輸入setTimeout。
```javascript
setTimeout(function(arg1){}.bind(undefined, 10), 1000);
```
上面代碼中,bind方法第一個參數是undefined,表示將原函數的this綁定全局作用域,第二個參數是要傳入原函數的參數。它運行后會返回一個新函數,該函數不帶參數。
第三種解決方法是自定義setTimeout,使用apply方法將參數輸入回調函數。
```html
<!--[if lte IE 9]><script>
(function(f){
window.setTimeout =f(window.setTimeout);
window.setInterval =f(window.setInterval);
})(function(f){return function(c,t){
var a=[].slice.call(arguments,2);return f(function(){c.apply(this,a)},t)}
});
</script><![endif]-->
```
除了參數問題,setTimeout還有一個需要注意的地方:如果被setTimeout推遲執行的回調函數是某個對象的方法,那么該方法中的this關鍵字將指向全局環境,而不是定義時所在的那個對象。
```javascript
var x = 1;
var o = {
x: 2,
y: function(){
console.log(this.x);
}
};
setTimeout(o.y,1000);
// 1
```
上面代碼輸出的是1,而不是2,這表示`o.y`的this所指向的已經不是o,而是全局環境了。
再看一個不容易發現錯誤的例子。
```javascript
function User(login) {
this.login = login;
this.sayHi = function() {
console.log(this.login);
}
}
var user = new User('John');
setTimeout(user.sayHi, 1000);
```
上面代碼只會顯示undefined,因為等到user.sayHi執行時,它是在全局對象中執行,所以this.login取不到值。
為了防止出現這個問題,一種解決方法是將user.sayHi放在函數中執行。
```javascript
setTimeout(function() {
user.sayHi();
}, 1000);
```
上面代碼中,sayHi是在user作用域內執行,而不是在全局作用域內執行,所以能夠顯示正確的值。
另一種解決方法是,使用bind方法,將綁定sayHi綁定在user上面。
```javascript
setTimeout(user.sayHi.bind(user), 1000);
```
HTML 5標準規定,setTimeout的最短時間間隔是4毫秒。為了節電,對于那些不處于當前窗口的頁面,瀏覽器會將時間間隔擴大到1000毫秒。另外,如果筆記本電腦處于電池供電狀態,Chrome和IE 9以上的版本,會將時間間隔切換到系統定時器,大約是15.6毫秒。
## setInterval()
`setInterval`函數的用法與`setTimeout`完全一致,區別僅僅在于`setInterval`指定某個任務每隔一段時間就執行一次,也就是無限次的定時執行。
```html
<input type="button" onclick="clearInterval(timer)" value="stop">
<script>
var i = 1
var timer = setInterval(function() {
console.log(2);
}, 1000);
</script>
```
上面代碼表示每隔1000毫秒就輸出一個2,直到用戶點擊了停止按鈕。
與`setTimeout`一樣,除了前兩個參數,`setInterval`方法還可以接受更多的參數,它們會傳入回調函數,下面是一個例子。
```javascript
function f(){
for (var i=0;i<arguments.length;i++){
console.log(arguments[i]);
}
}
setInterval(f, 1000, "Hello World");
// Hello World
// Hello World
// Hello World
// ...
```
如果網頁不在瀏覽器的當前窗口(或tab),許多瀏覽器限制setInteral指定的反復運行的任務最多每秒執行一次。
下面是一個通過`setInterval`方法實現網頁動畫的例子。
```javascript
var div = document.getElementById('someDiv');
var opacity = 1;
var fader = setInterval(function() {
opacity -= 0.1;
if (opacity >= 0) {
div.style.opacity = opacity;
} else {
clearInterval(fader);
}
}, 100);
```
上面代碼每隔100毫秒,設置一次`div`元素的透明度,直至其完全透明為止。
`setInterval`的一個常見用途是實現輪詢。下面是一個輪詢URL的Hash值是否發生變化的例子。
```javascript
var hash = window.location.hash;
var hashWatcher = setInterval(function() {
if (window.location.hash != hash) {
updatePage();
}
}, 1000);
```
setInterval指定的是“開始執行”之間的間隔,并不考慮每次任務執行本身所消耗的時間。因此實際上,兩次執行之間的間隔會小于指定的時間。比如,setInterval指定每100ms執行一次,每次執行需要5ms,那么第一次執行結束后95毫秒,第二次執行就會開始。如果某次執行耗時特別長,比如需要105毫秒,那么它結束后,下一次執行就會立即開始。
為了確保兩次執行之間有固定的間隔,可以不用setInterval,而是每次執行結束后,使用setTimeout指定下一次執行的具體時間。
```javascript
var i = 1;
var timer = setTimeout(function() {
alert(i++);
timer = setTimeout(arguments.callee, 2000);
}, 2000);
```
上面代碼可以確保,下一個對話框總是在關閉上一個對話框之后2000毫秒彈出。
根據這種思路,可以自己部署一個函數,實現間隔時間確定的setInterval的效果。
```javascript
function interval(func, wait){
var interv = function(){
func.call(null);
setTimeout(interv, wait);
};
setTimeout(interv, wait);
}
interval(function(){
console.log(2);
},1000);
```
上面代碼部署了一個interval函數,用循環調用setTimeout模擬了setInterval。
HTML 5標準規定,setInterval的最短間隔時間是10毫秒,也就是說,小于10毫秒的時間間隔會被調整到10毫秒。
## clearTimeout(),clearInterval()
setTimeout和setInterval函數,都返回一個表示計數器編號的整數值,將該整數傳入clearTimeout和clearInterval函數,就可以取消對應的定時器。
```javascript
var id1 = setTimeout(f,1000);
var id2 = setInterval(f,1000);
clearTimeout(id1);
clearInterval(id2);
```
setTimeout和setInterval返回的整數值是連續的,也就是說,第二個setTimeout方法返回的整數值,將比第一個的整數值大1。利用這一點,可以寫一個函數,取消當前所有的setTimeout。
```javascript
(function() {
var gid = setInterval(clearAllTimeouts, 0);
function clearAllTimeouts() {
var id = setTimeout(function() {}, 0);
while (id > 0) {
if (id !== gid) {
clearTimeout(id);
}
id--;
}
}
})();
```
運行上面代碼后,實際上再設置任何setTimeout都無效了。
下面是一個clearTimeout實際應用的例子。有些網站會實時將用戶在文本框的輸入,通過Ajax方法傳回服務器,jQuery的寫法如下。
```javascript
$('textarea').on('keydown', ajaxAction);
```
這樣寫有一個很大的缺點,就是如果用戶連續擊鍵,就會連續觸發keydown事件,造成大量的Ajax通信。這是不必要的,而且很可能會發生性能問題。正確的做法應該是,設置一個門檻值,表示兩次Ajax通信的最小間隔時間。如果在設定的時間內,發生新的keydown事件,則不觸發Ajax通信,并且重新開始計時。如果過了指定時間,沒有發生新的keydown事件,將進行Ajax通信將數據發送出去。
這種做法叫做debounce(防抖動)方法,用來返回一個新函數。只有當兩次觸發之間的時間間隔大于事先設定的值,這個新函數才會運行實際的任務。假定兩次Ajax通信的間隔不小于2500毫秒,上面的代碼可以改寫成下面這樣。
```javascript
$('textarea').on('keydown', debounce(ajaxAction, 2500))
```
利用setTimeout和clearTimeout,可以實現debounce方法。該方法用于防止某個函數在短時間內被密集調用,具體來說,debounce方法返回一個新版的該函數,這個新版函數調用后,只有在指定時間內沒有新的調用,才會執行,否則就重新計時。
```javascript
function debounce(fn, delay){
var timer = null; // 聲明計時器
return function(){
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function(){
fn.apply(context, args);
}, delay);
};
}
// 用法示例
var todoChanges = _.debounce(batchLog, 1000);
Object.observe(models.todo, todoChanges);
```
現實中,最好不要設置太多個setTimeout和setInterval,它們耗費CPU。比較理想的做法是,將要推遲執行的代碼都放在一個函數里,然后只對這個函數使用setTimeout或setInterval。
## 運行機制
setTimeout和setInterval的運行機制是,將指定的代碼移出本次執行,等到下一輪Event Loop時,再檢查是否到了指定時間。如果到了,就執行對應的代碼;如果不到,就等到再下一輪Event Loop時重新判斷。這意味著,setTimeout指定的代碼,必須等到本次執行的所有代碼都執行完,才會執行。
每一輪Event Loop時,都會將“任務隊列”中需要執行的任務,一次執行完。setTimeout和setInterval都是把任務添加到“任務隊列”的尾部。因此,它們實際上要等到當前腳本的所有同步任務執行完,然后再等到本次Event Loop的“任務隊列”的所有任務執行完,才會開始執行。由于前面的任務到底需要多少時間執行完,是不確定的,所以沒有辦法保證,setTimeout和setInterval指定的任務,一定會按照預定時間執行。
```javascript
setTimeout(someTask,100);
veryLongTask();
```
上面代碼的setTimeout,指定100毫秒以后運行一個任務。但是,如果后面立即運行的任務(當前腳本的同步任務))非常耗時,過了100毫秒還無法結束,那么被推遲運行的someTask就只有等著,等到前面的veryLongTask運行結束,才輪到它執行。
這一點對于setInterval影響尤其大。
```javascript
setInterval(function(){
console.log(2);
},1000);
(function (){
sleeping(3000);
})();
```
上面的第一行語句要求每隔1000毫秒,就輸出一個2。但是,第二行語句需要3000毫秒才能完成,請問會發生什么結果?
結果就是等到第二行語句運行完成以后,立刻連續輸出三個2,然后開始每隔1000毫秒,輸出一個2。也就是說,setIntervel具有累積效應,如果某個操作特別耗時,超過了setInterval的時間間隔,排在后面的操作會被累積起來,然后在很短的時間內連續觸發,這可能或造成性能問題(比如集中發出Ajax請求)。
為了進一步理解JavaScript的單線程模型,請看下面這段偽代碼。
```javascript
function init(){
{ 耗時5ms的某個操作 }
觸發mouseClickEvent事件
{ 耗時5ms的某個操作 }
setInterval(timerTask,10);
{ 耗時5ms的某個操作 }
}
function handleMouseClick(){
耗時8ms的某個操作
}
function timerTask(){
耗時2ms的某個操作
}
```
請問調用init函數后,這段代碼的運行順序是怎樣的?
- **0-15ms**:運行init函數。
- **15-23ms**:運行handleMouseClick函數。請注意,這個函數是在5ms時觸發的,應該在那個時候就立即運行,但是由于單線程的關系,必須等到init函數完成之后再運行。
- **23-25ms**:運行timerTask函數。這個函數是在10ms時觸發的,規定每10ms運行一次,即在20ms、30ms、40ms等時候運行。由于20ms時,JavaScript線程還有任務在運行,因此必須延遲到前面任務完成時再運行。
- **30-32ms**:運行timerTask函數。
- **40-42ms**:運行timerTask函數。
## setTimeout(f,0)
### 含義
`setTimeout`的作用是將代碼推遲到指定時間執行,如果指定時間為`0`,即`setTimeout(f, 0)`,那么會立刻執行嗎?
答案是不會。因為上一段說過,必須要等到當前腳本的同步任務和“任務隊列”中已有的事件,全部處理完以后,才會執行`setTimeout`指定的任務。也就是說,setTimeout的真正作用是,在“消息隊列”的現有消息的后面再添加一個消息,規定在指定時間執行某段代碼。`setTimeout`添加的事件,會在下一次`Event Loop`執行。
`setTimeout(f, 0)`將第二個參數設為`0`,作用是讓`f`在現有的任務(腳本的同步任務和“消息隊列”指定的任務)一結束就立刻執行。也就是說,`setTimeout(f, 0)`的作用是,盡可能早地執行指定的任務。而并不是會立刻就執行這個任務。
```javascript
setTimeout(function () {
console.log('你好!');
}, 0);
```
上面代碼的含義是,盡可能早地顯示“你好!”。
`setTimeout(f, 0)`指定的任務,最早也要到下一次Event Loop才會執行。請看下面的例子。
```javascript
setTimeout(function() {
console.log("Timeout");
}, 0);
function a(x) {
console.log("a() 開始運行");
b(x);
console.log("a() 結束運行");
}
function b(y) {
console.log("b() 開始運行");
console.log("傳入的值為" + y);
console.log("b() 結束運行");
}
console.log("當前任務開始");
a(42);
console.log("當前任務結束");
// 當前任務開始
// a() 開始運行
// b() 開始運行
// 傳入的值為42
// b() 結束運行
// a() 結束運行
// 當前任務結束
// Timeout
```
上面代碼說明,`setTimeout(f, 0)`必須要等到當前腳本的所有同步任務結束后才會執行。
即使消息隊列是空的,0毫秒實際上也是達不到的。根據[HTML 5標準](http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#timers),`setTimeOut`推遲執行的時間,最少是4毫秒。如果小于這個值,會被自動增加到4。這是為了防止多個`setTimeout(f, 0)`語句連續執行,造成性能問題。
另一方面,瀏覽器內部使用32位帶符號的整數,來儲存推遲執行的時間。這意味著`setTimeout`最多只能推遲執行2147483647毫秒(24.8天),超過這個時間會發生溢出,導致回調函數將在當前任務隊列結束后立即執行,即等同于`setTimeout(f, 0)`的效果。
### 應用
setTimeout(f,0)有幾個非常重要的用途。它的一大應用是,可以調整事件的發生順序。比如,網頁開發中,某個事件先發生在子元素,然后冒泡到父元素,即子元素的事件回調函數,會早于父元素的事件回調函數觸發。如果,我們先讓父元素的事件回調函數先發生,就要用到setTimeout(f, 0)。
```javascript
var input = document.getElementsByTagName('input[type=button]')[0];
input.onclick = function A() {
setTimeout(function B() {
input.value +=' input';
}, 0)
};
document.body.onclick = function C() {
input.value += ' body'
};
```
上面代碼在點擊按鈕后,先觸發回調函數A,然后觸發函數C。在函數A中,setTimeout將函數B推遲到下一輪Loop執行,這樣就起到了,先觸發父元素的回調函數C的目的了。
用戶自定義的回調函數,通常在瀏覽器的默認動作之前觸發。比如,用戶在輸入框輸入文本,keypress事件會在瀏覽器接收文本之前觸發。因此,下面的回調函數是達不到目的的。
```javascript
document.getElementById('input-box').onkeypress = function(event) {
this.value = this.value.toUpperCase();
}
```
上面代碼想在用戶輸入文本后,立即將字符轉為大寫。但是實際上,它只能將上一個字符轉為大寫,因為瀏覽器此時還沒接收到文本,所以`this.value`取不到最新輸入的那個字符。只有用setTimeout改寫,上面的代碼才能發揮作用。
```javascript
document.getElementById('my-ok').onkeypress = function() {
var self = this;
setTimeout(function() {
self.value = self.value.toUpperCase();
}, 0);
}
```
上面代碼將代碼放入setTimeout之中,就能使得它在瀏覽器接收到文本之后觸發。
由于setTimeout(f,0)實際上意味著,將任務放到瀏覽器最早可得的空閑時段執行,所以那些計算量大、耗時長的任務,常常會被放到幾個小部分,分別放到setTimeout(f,0)里面執行。
```javascript
var div = document.getElementsByTagName('div')[0];
// 寫法一
for (var i = 0xA00000; i < 0xFFFFFF; i++) {
div.style.backgroundColor = '#' + i.toString(16);
}
// 寫法二
var timer;
var i=0x100000;
function func() {
timer = setTimeout(func, 0);
div.style.backgroundColor = '#' + i.toString(16);
if (i++ == 0xFFFFFF) clearTimeout(timer);
}
timer = setTimeout(func, 0);
```
上面代碼有兩種寫法,都是改變一個網頁元素的背景色。寫法一會造成瀏覽器“堵塞”,因為JavaScript執行速度遠高于DOM,會造成大量DOM操作“堆積”,而寫法二就不會,這就是`setTimeout(f, 0)`的好處。
另一個使用這種技巧的例子是代碼高亮的處理。如果代碼塊很大,一次性處理,可能會對性能造成很大的壓力,那么將其分成一個個小塊,一次處理一塊,比如寫成`setTimeout(highlightNext, 50)`的樣子,性能壓力就會減輕。
## 正常任務與微任務
正常情況下,JavaScript的任務是同步執行的,即執行完前一個任務,然后執行后一個任務。只有遇到異步任務的情況下,執行順序才會改變。
這時,需要區分兩種任務:正常任務(task)與微任務(microtask)。它們的區別在于,“正常任務”在下一輪Event Loop執行,“微任務”在本輪Event Loop的所有任務結束后執行。
```javascript
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
Promise.resolve().then(function() {
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
// 1
// 5
// 3
// 4
// 2
```
上面代碼的執行結果說明,`setTimeout(fn, 0)`在`Promise.resolve`之后執行。
這是因為`setTimeout`語句指定的是“正常任務”,即不會在當前的Event Loop執行。而Promise會將它的回調函數,在狀態改變后的那一輪Event Loop指定為微任務。所以,3和4輸出在5之后、2之前。
除了`setTimeout`,正常任務還包括各種事件(比如鼠標單擊事件)的回調函數。微任務目前主要就是Promise。
<h2 id="6.3">window對象</h2>
## 概述
JavaScript的所有對象都存在于一個運行環境之中,這個運行環境本身也是對象,稱為“頂層對象”。這就是說,JavaScript的所有對象,都是“頂層對象”的下屬。不同的運行環境有不同的“頂層對象”,在瀏覽器環境中,這個頂層對象就是`window`對象(`w`為小寫)。
所有瀏覽器環境的全局變量,都是`window`對象的屬性。
```javascript
var a = 1;
window.a // 1
```
上面代碼中,變量`a`是一個全局變量,但是實質上它是`window`對象的屬性。聲明一個全局變量,就是為`window`對象的同名屬性賦值。
可以簡單理解成,`window`就是指當前的瀏覽器窗口。
從語言設計的角度看,所有變量都是`window`對象的屬性,其實不是很合理。因為`window`對象有自己的實體含義,不適合當作最高一層的頂層對象。這個設計失誤與JavaScript語言匆忙的設計過程有關,最早的設想是語言內置的對象越少越好,這樣可以提高瀏覽器的性能。因此,語言設計者Brendan Eich就把`window`對象當作頂層對象,所有未聲明就賦值的變量都自動變成`window`對象的屬性。這種設計使得編譯階段無法檢測未聲明變量,但到了今天已經沒有辦法糾正了。
## 窗口的大小和位置
瀏覽器提供一系列屬性,用來獲取瀏覽器窗口的大小和位置。
(1)window.screenX,window.screenY
`window.screenX`和`window.screenY`屬性,返回瀏覽器窗口左上角相對于當前屏幕左上角(`(0, 0)`)的水平距離和垂直距離,單位為像素。
(2)window.innerHeight,window.innerWidth
`window.innerHeight`和`window.innerWidth`屬性,返回網頁在當前窗口中可見部分的高度和寬度,即“視口”(viewport),單位為像素。
當用戶放大網頁的時候(比如將網頁從100%的大小放大為200%),這兩個屬性會變小。因為這時網頁的像素大小不變,只是每個像素占據的屏幕空間變大了,因為可見部分(視口)就變小了。
注意,這兩個屬性值包括滾動條的高度和寬度。
(3)window.outerHeight,window.outerWidth
`window.outerHeight`和`window.outerWidth`屬性返回瀏覽器窗口的高度和寬度,包括瀏覽器菜單和邊框,單位為像素。
(4)window.pageXOffset屬性,window.pageYOffset屬性
`window.pageXOffset`屬性返回頁面的水平滾動距離,`window.pageYOffset`屬性返回頁面的垂直滾動距離,單位都為像素。
## window對象的屬性
### window.closed
`window.closed`屬性返回一個布爾值,表示指定窗口是否關閉,通常用來檢查通過腳本新建的窗口。
```javascript
popup.closed // false
```
上面代碼檢查跳出窗口是否關閉。
### window.opener
`window.opener`屬性返回打開當前窗口的父窗口。如果當前窗口沒有父窗口,則返回`null`。
```javascript
var windowA = window.opener;
```
通過`opener`屬性,可以獲得父窗口的的全局變量和方法,比如`windowA.window.propertyName`和`windowA.window.functionName()`。
該屬性只適用于兩個窗口屬于同源的情況(參見《同源政策》一節),且其中一個窗口由另一個打開。
### window.name
`window.name`屬性用于設置當前瀏覽器窗口的名字。
```javascript
window.name = 'Hello World!';
console.log(window.name)
// "Hello World!"
```
各個瀏覽器對這個值的儲存容量有所不同,但是一般來說,可以高達幾MB。
它有一個重要特點,就是只要是本窗口打開的網頁,都能讀寫該屬性,不管這些網頁是否屬于同一個網站。所以,可以把值存放在該屬性內,然后讓另一個網頁讀取,從而實現跨域通信(詳見《同源政策》一節)。
該屬性只能保存字符串,且當瀏覽器窗口關閉后,所保存的值就會消失。因此局限性比較大,但是與iframe窗口通信時,非常有用。
### window.location
`window.location`返回一個`location`對象,用于獲取窗口當前的URL信息。它等同于`document.location`對象。
```javascript
window.location === document.location // true
```
## 框架窗口
`window.frames`屬性返回一個類似數組的對象,成員為頁面內所有框架窗口,包括`frame`元素和`iframe`元素。`window.frames[0]`表示頁面中第一個框架窗口,`window.frames['someName']`則是根據框架窗口的`name`屬性的值(不是`id`屬性),返回該窗口。另外,通過`document.getElementById()`方法也可以引用指定的框架窗口。
```javascript
var frame = document.getElementById('theFrame');
var frameWindow = frame.contentWindow;
// 等同于 frame.contentWindow.document
var frameDoc = frame.contentDocument;
// 獲取子窗口的變量和屬性
frameWindow.function()
```
`window.length`屬性返回當前頁面中所有框架窗口總數。
```javascript
window.frames.length === window.length // true
```
`window.frames.length`與`window.length`應該是相等的。
由于傳統的`frame`窗口已經不建議使用了,這里主要介紹`iframe`窗口。
需要注意的是,`window.frames`的每個成員對應的是框架內的窗口(即框架的`window`對象)。如果要獲取每個框架內部的DOM樹,需要使用`window.frames[0].document`的寫法。
```javascript
var iframe = window.getElementsByTagName('iframe')[0];
var iframe_title = iframe.contentWindow.title;
```
上面代碼用于獲取`iframe`頁面的標題。
`iframe`元素遵守同源政策,只有當父頁面與框架頁面來自同一個域名,兩者之間才可以用腳本通信,否則只有使用window.postMessage方法。
`iframe`窗口內部,使用`window.parent`引用父窗口。如果當前頁面沒有父窗口,則`window.parent`屬性返回自身。因此,可以通過`window.parent`是否等于`window.self`,判斷當前窗口是否為`iframe`窗口。
```javascript
if (window.parent != window.self) {
// 當前窗口是子窗口
}
```
## navigator對象
Window對象的navigator屬性,指向一個包含瀏覽器相關信息的對象。
**(1)navigator.userAgent屬性**
navigator.userAgent屬性返回瀏覽器的User-Agent字符串,用來標示瀏覽器的種類。下面是Chrome瀏覽器的User-Agent。
```javascript
navigator.userAgent
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"
```
通過userAgent屬性識別瀏覽器,不是一個好辦法。因為必須考慮所有的情況(不同的瀏覽器,不同的版本),非常麻煩,而且無法保證未來的適用性,更何況各種上網設備層出不窮,難以窮盡。所以,現在一般不再識別瀏覽器了,而是使用“功能識別”方法,即逐一測試當前瀏覽器是否支持要用到的JavaScript功能。
不過,通過userAgent可以大致準確地識別手機瀏覽器,方法就是測試是否包含“mobi”字符串。
```javascript
var ua = navigator.userAgent.toLowerCase();
if (/mobi/i.test(ua)) {
// 手機瀏覽器
} else {
// 非手機瀏覽器
}
```
如果想要識別所有移動設備的瀏覽器,可以測試更多的特征字符串。
```javascript
/mobi|android|touch|mini/i.test(ua)
```
**(2)navigator.plugins屬性**
navigator.plugins屬性返回一個類似數組的對象,成員是瀏覽器安裝的插件,比如Flash、ActiveX等。
## window.screen對象
`window.screen`對象包含了顯示設備的信息。
`screen.height`和`screen.width`兩個屬性,一般用來了解設備的分辨率。
```javascript
// 顯示設備的高度,單位為像素
screen.height // 1920
// 顯示設備的寬度,單位為像素
screen.width // 1080
```
上面代碼顯示,某設備的分辨率是1920x1080。
除非調整顯示器的分辨率,否則這兩個值可以看作常量,不會發生變化。顯示器的分辨率與瀏覽器設置無關,縮放網頁并不會改變分辨率。
下面是根據屏幕分辨率,將用戶導向不同網頁的代碼。
```javascript
if ((screen.width <= 800) && (screen.height <= 600)) {
window.location.replace('small.html');
} else {
window.location.replace('wide.html');
}
```
`screen.availHeight`和`screen.availWidth`屬性返回屏幕可用的高度和寬度,單位為像素。它們的值為屏幕的實際大小減去操作系統某些功能占據的空間,比如系統的任務欄。
`screen.colorDepth`屬性返回屏幕的顏色深度,一般為16(表示16-bit)或24(表示24-bit)。
## window對象的方法
### window.moveTo(),window.moveBy()
`window.moveTo`方法用于移動瀏覽器窗口到指定位置。它接受兩個參數,分別是窗口左上角距離屏幕左上角的水平距離和垂直距離,單位為像素。
```javascript
window.moveTo(100, 200)
```
上面代碼將窗口移動到屏幕`(100, 200)`的位置。
`window.moveBy`方法將窗口移動到一個相對位置。它接受兩個參數,分布是窗口左上角向右移動的水平距離和向下移動的垂直距離,單位為像素。
```javascript
window.moveBy(25, 50)
```
上面代碼將窗口向右移動25像素、向下移動50像素。
### window.open(), window.close()
`window.open`方法用于新建另一個瀏覽器窗口,并且返回該窗口對象。
```javascript
var popup = window.open('somefile.html');
```
`open`方法的第一個參數是新窗口打開的網址,此外還可以加上第二個參數,表示新窗口的名字,以及第三個參數用來指定新窗口的參數,形式是一個逗號分隔的`property=value`字符串。
下面是一個例子。
```javascript
var popup = window.open(
'somepage.html',
'DefinitionsWindows',
'height=200,width=200,location=no,resizable=yes,scrollbars=yes'
);
```
注意,如果在第三個參數中設置了一部分參數,其他沒有被設置的`yes/no`參數都會被設成No,只有`titlebar`和關閉按鈕除外(它們的值默認為yes)。
`open`方法返回新窗口的引用。
```javascript
var windowB = window.open('windowB.html', 'WindowB');
windowB.window.name // "WindowB"
```
由于`open`這個方法很容易被濫用,許多瀏覽器默認都不允許腳本新建窗口。因此,有必要檢查一下打開新窗口是否成功。
```javascript
if (popup === null) {
// 新建窗口失敗
}
```
`window.close`方法用于關閉當前窗口,一般用來關閉`window.open`方法新建的窗口。
```javascript
popup.close()
```
`window.closed`屬性用于檢查當前窗口是否被關閉了。
```javascript
if ((popup !== null) && !popup.closed) {
// 窗口仍然打開著
}
```
### window.print()
`print`方法會跳出打印對話框,同用戶點擊菜單里面的“打印”命令效果相同。
頁面上的打印按鈕代碼如下。
```javascript
document.getElementById('printLink').onclick = function() {
window.print();
}
```
非桌面設備(比如手機)可能沒有打印功能,這時可以這樣判斷。
```javascript
if (typeof window.print === 'function') {
// 支持打印功能
}
```
### URL的編碼/解碼方法
JavaScript提供四個URL的編碼/解碼方法。
- decodeURI()
- decodeURIComponent()
- encodeURI()
- encodeURIComponent()
### window.getComputedStyle方法
getComputedStyle方法接受一個HTML元素作為參數,返回一個包含該HTML元素的最終樣式信息的對象。詳見《DOM》一章的CSS章節。
### window.matchMedia方法
window.matchMedia方法用來檢查CSS的mediaQuery語句。詳見《DOM》一章的CSS章節。
### window.focus()
`focus`方法會激活指定當前窗口,使其獲得焦點。
```javascript
if ((popup !== null) && !popup.closed) {
popup.focus();
}
```
上面代碼先檢查`popup`窗口是否依然存在,確認后激活該窗口。
當前窗口獲得焦點時,會觸發`focus`事件;當前窗口失去焦點時,會觸發`blur`事件。
## window對象的事件
### window.onerror
瀏覽器腳本發生錯誤時,會觸發window對象的error事件。我們可以通過`window.onerror`屬性對該事件指定回調函數。
```javascript
window.onerror = function (message, filename, lineno, colno, error) {
console.log("出錯了!--> %s", error.stack);
};
```
error事件的回調函數,一共可以有五個參數,它們的含義依次如下。
- 出錯信息
- 出錯腳本的網址
- 行號
- 列號
- 錯誤對象
老式瀏覽器只支持前三個參數。
需要注意的是,如果腳本網址與網頁網址不在同一個域(比如使用了CDN),瀏覽器根本不會提供詳細的出錯信息,只會提示出錯,錯誤類型是“Script error.”,行號為0,其他信息都沒有。這是瀏覽器防止向外部腳本泄漏信息。一個解決方法是在腳本所在的服務器,設置Access-Control-Allow-Origin的HTTP頭信息。
```bash
Access-Control-Allow-Origin:*
```
然后,在網頁的script標簽中設置crossorigin屬性。
```html
<script crossorigin="anonymous" src="//example.com/file.js"></script>
```
上面代碼的`crossorigin="anonymous"`表示,讀取文件不需要身份信息,即不需要cookie和HTTP認證信息。如果設為`crossorigin="use-credentials"`,就表示瀏覽器會上傳cookie和HTTP認證信息,同時還需要服務器端打開HTTP頭信息Access-Control-Allow-Credentials。
并不是所有的錯誤,都會觸發JavaScript的error事件(即讓JavaScript報錯),只限于以下三類事件。
- JavaScript語言錯誤
- JavaScript腳本文件不存在
- 圖像文件不存在
以下兩類事件不會觸發JavaScript的error事件。
- CSS文件不存在
- iframe文件不存在
## alert(),prompt(),confirm()
`alert()`、`prompt()`、`confirm()`都是瀏覽器與用戶互動的全局方法。它們會彈出不同的對話框,要求用戶做出回應。
需要注意的是,`alert()`、`prompt()`、`confirm()`這三個方法彈出的對話框,都是瀏覽器統一規定的式樣,是無法定制的。
`alert`方法彈出的對話框,只有一個“確定”按鈕,往往用來通知用戶某些信息。
```javascript
// 格式
alert(message);
// 實例
alert('Hello World');
```
用戶只有點擊“確定”按鈕,對話框才會消失。在對話框彈出期間,瀏覽器窗口處于凍結狀態,如果不點“確定”按鈕,用戶什么也干不了。
`prompt`方法彈出的對話框,在提示文字的下方,還有一個輸入框,要求用戶輸入信息,并有“確定”和“取消”兩個按鈕。它往往用來獲取用戶輸入的數據。
```javascript
// 格式
var result = prompt(text[, default]);
// 實例
var result = prompt('您的年齡?', 25)
```
上面代碼會跳出一個對話框,文字提示為“您的年齡?”,要求用戶在對話框中輸入自己的年齡(默認顯示25)。
`alert`方法的參數只能是字符串,沒法使用CSS樣式,但是可以用`\n`指定換行。
```javascript
alert('本條提示\n分成兩行');
```
`prompt`方法的返回值是一個字符串(有可能為空)或者`null`,具體分成三種情況。
1. 用戶輸入信息,并點擊“確定”,則用戶輸入的信息就是返回值。
2. 用戶沒有輸入信息,直接點擊“確定”,則輸入框的默認值就是返回值。
3. 用戶點擊了“取消”(或者按了Esc按鈕),則返回值是`null`。
`prompt`方法的第二個參數是可選的,但是如果不提供的話,IE瀏覽器會在輸入框中顯示`undefined`。因此,最好總是提供第二個參數,作為輸入框的默認值。
`confirm`方法彈出的對話框,除了提示信息之外,只有“確定”和“取消”兩個按鈕,往往用來征詢用戶的意見。
```javascript
// 格式
var result = confirm(message);
// 實例
var result = confirm("你最近好嗎?");
```
上面代碼彈出一個對話框,上面只有一行文字“你最近好嗎?”,用戶選擇點擊“確定”或“取消”。
`confirm`方法返回一個布爾值,如果用戶點擊“確定”,則返回`true`;如果用戶點擊“取消”,則返回`false`。
```javascript
var okay = confirm('Please confirm this message.');
if (okay) {
// 用戶按下“確定”
} else {
// 用戶按下“取消”
}
```
`confirm`的一個用途是,當用戶離開當前頁面時,彈出一個對話框,問用戶是否真的要離開。
```javascript
window.onunload = function() {
return confirm('你確定要離開當面頁面嗎?');
}
```
<h2 id="6.4">history對象</h2>
## 概述
瀏覽器窗口有一個`history`對象,用來保存瀏覽歷史。
比如,當前窗口先后訪問了三個地址,那么`history`對象就包括三項,`history.length`屬性等于3。
```javascript
history.length // 3
```
`history`對象提供了一系列方法,允許在瀏覽歷史之間移動。
- `back()`:移動到上一個訪問頁面,等同于瀏覽器的后退鍵。
- `forward()`:移動到下一個訪問頁面,等同于瀏覽器的前進鍵。
- `go()`:接受一個整數作為參數,移動到該整數指定的頁面,比如`go(1)`相當于`forward()`,`go(-1)`相當于`back()`。
```javascript
history.back();
history.forward();
history.go(-2);
```
如果移動的位置超出了訪問歷史的邊界,以上三個方法并不報錯,而是默默的失敗。
以下命令相當于刷新當前頁面。
```javascript
history.go(0);
```
常見的“返回上一頁”鏈接,代碼如下。
```javascript
document.getElementById('backLink').onclick = function () {
window.history.back();
}
```
注意,返回上一頁時,頁面通常是從瀏覽器緩存之中加載,而不是重新要求服務器發送新的網頁。
## history.pushState(),history.replaceState()
HTML5為history對象添加了兩個新方法,history.pushState() 和 history.replaceState(),用來在瀏覽歷史中添加和修改記錄。所有主流瀏覽器都支持該方法(包括IE10)。
```javascript
if (!!(window.history && history.pushState)){
// 支持History API
} else {
// 不支持
}
```
上面代碼可以用來檢查,當前瀏覽器是否支持History API。如果不支持的話,可以考慮使用Polyfill庫[History.js]( https://github.com/browserstate/history.js/)。
history.pushState方法接受三個參數,依次為:
- **state**:一個與指定網址相關的狀態對象,popstate事件觸發時,該對象會傳入回調函數。如果不需要這個對象,此處可以填null。
- **title**:新頁面的標題,但是所有瀏覽器目前都忽略這個值,因此這里可以填null。
- **url**:新的網址,必須與當前頁面處在同一個域。瀏覽器的地址欄將顯示這個網址。
假定當前網址是`example.com/1.html`,我們使用pushState方法在瀏覽記錄(history對象)中添加一個新記錄。
```javascript
var stateObj = { foo: "bar" };
history.pushState(stateObj, "page 2", "2.html");
```
添加上面這個新記錄后,瀏覽器地址欄立刻顯示`example.com/2.html`,但并不會跳轉到2.html,甚至也不會檢查2.html是否存在,它只是成為瀏覽歷史中的最新記錄。假定這時你訪問了google.com,然后點擊了倒退按鈕,頁面的url將顯示2.html,但是內容還是原來的1.html。你再點擊一次倒退按鈕,url將顯示1.html,內容不變。
> 注意,pushState方法不會觸發頁面刷新。
如果 pushState 的url參數,設置了一個當前網頁的#號值(即hash),并不會觸發hashchange事件。如果設置了一個非同域的網址,則會報錯。
```javascript
// 報錯
history.pushState(null, null, 'https://twitter.com/hello');
```
上面代碼中,pushState想要插入一個非同域的網址,導致報錯。這樣設計的目的是,防止惡意代碼讓用戶以為他們是在另一個網站上。
`history.replaceState`方法的參數與`pushState`方法一模一樣,區別是它修改瀏覽歷史中當前頁面的值。下面的例子假定當前網頁是example.com/example.html。
```javascript
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // url顯示為http://example.com/example.html?page=1
history.back(); // url顯示為http://example.com/example.html
history.go(2); // url顯示為http://example.com/example.html?page=3
```
## history.state屬性
history.state屬性保存當前頁面的state對象。
```javascript
history.pushState({page: 1}, "title 1", "?page=1");
history.state
// { page: 1 }
```
## popstate事件
每當同一個文檔的瀏覽歷史(即history對象)出現變化時,就會觸發popstate事件。需要注意的是,僅僅調用pushState方法或replaceState方法 ,并不會觸發該事件,只有用戶點擊瀏覽器倒退按鈕和前進按鈕,或者使用JavaScript調用back、forward、go方法時才會觸發。另外,該事件只針對同一個文檔,如果瀏覽歷史的切換,導致加載不同的文檔,該事件也不會觸發。
使用的時候,可以為popstate事件指定回調函數。這個回調函數的參數是一個event事件對象,它的state屬性指向pushState和replaceState方法為當前url所提供的狀態對象(即這兩個方法的第一個參數)。
```javascript
window.onpopstate = function(event) {
console.log("location: " + document.location);
console.log("state: " + JSON.stringify(event.state));
};
// 或者
window.addEventListener('popstate', function(event) {
console.log("location: " + document.location);
console.log("state: " + JSON.stringify(event.state));
});
```
上面代碼中的event.state,就是通過pushState和replaceState方法,為當前url綁定的state對象。
這個state對象也可以直接通過history對象讀取。
```javascript
var currentState = history.state;
```
另外,需要注意的是,當頁面第一次加載的時候,在onload事件發生后,Chrome和Safari瀏覽器(Webkit核心)會觸發popstate事件,而Firefox和IE瀏覽器不會。
## URLSearchParams API
URLSearchParams API用于處理URL之中的查詢字符串,即問號之后的部分。沒有部署這個API的瀏覽器,可以用[url-search-params](url-search-params)這個墊片庫。
```javascript
var paramsString = 'q=URLUtils.searchParams&topic=api'
var searchParams = new URLSearchParams(paramsString);
```
URLSearchParams有以下方法,用來操作某個參數。
- `has()`:返回一個布爾值,表示是否具有某個參數
- `get()`:返回指定參數的第一個值
- `getAll()`:返回一個數組,成員是指定參數的所有值
- `set()`:設置指定參數
- `delete()`:刪除指定參數
- `append()`:在查詢字符串之中,追加一個鍵值對
- `toString()`:返回整個查詢字符串
```javascript
var paramsString = "q=URLUtils.searchParams&topic=api"
var searchParams = new URLSearchParams(paramsString);
searchParams.has('topic') // true
searchParams.get('topic') // "api"
searchParams.getAll('topic') // ["api"]
searchParams.get('foo') // null,注意Firefox返回空字符串
searchParams.set('foo', 2);
searchParams.get('foo') // 2
searchParams.append('topic', 'webdev');
searchParams.toString() // "q=URLUtils.searchParams&topic=api&foo=2&topic=webdev"
searchParams.append('foo', 3);
searchParams.getAll('foo') // [2, 3]
searchParams.delete('topic');
searchParams.toString() // "q=URLUtils.searchParams&foo=2&foo=3"
```
URLSearchParams還有三個方法,用來遍歷所有參數。
- `key()`:遍歷所有參數名
- `values()`:遍歷所有參數值
- `entries()`:遍歷所有參數的鍵值對
上面三個方法返回的都是Iterator對象。
```javascript
var searchParams = new URLSearchParams('key1=value1&key2=value2');
for(var key of searchParams.keys()) {
console.log(key);
}
// key1
// key2
for(var value of searchParams.values()) {
console.log(value);
}
// value1
// value2
for(var pair of searchParams.entries()) {
console.log(pair[0]+ ', '+ pair[1]);
}
// key1, value1
// key2, value2
```
在Chrome瀏覽器之中,`URLSearchParams`實例本身就是Iterator對象,與`entries`方法返回值相同。所以,可以寫成下面的樣子。
```javascript
for (var p of searchParams) {
console.log(p);
}
```
下面是一個替換當前URL的例子。
```javascript
// URL: https://example.com?version=1.0
var params = new URLSearchParams(location.search.slice(1));
params.set('version', 2.0);
window.history.replaceState({}, '', `${location.pathname}?${params}`);
// URL: https://example.com?version=2.0
```
`URLSearchParams`實例可以當作POST數據發送,所有數據都會URL編碼。
```javascript
let params = new URLSearchParams();
params.append('api_key', '1234567890');
fetch('https://example.com/api', {
method: 'POST',
body: params
}).then(...)
```
DOM的`a`元素節點的`searchParams`屬性,就是一個`URLSearchParams`實例。
```javascript
var a = document.createElement('a');
a.href = 'https://example.com?filter=api';
a.searchParams.get('filter') // "api"
```
`URLSearchParams`還可以與`URL`接口結合使用。
```javascript
var url = new URL(location);
var foo = url.searchParams.get('foo') || 'somedefault';
```
<h2 id="6.5">Cookie</h2>
## 概述
Cookie是服務器保存在瀏覽器的一小段文本信息,每個Cookie的大小一般不能超過4KB。瀏覽器每次向服務器發出請求,就會自動附上這段信息。
Cookie保存以下幾方面的信息。
- Cookie的名字
- Cookie的值
- 到期時間
- 所屬域名(默認是當前域名)
- 生效的路徑(默認是當前網址)
舉例來說,如果當前URL是`www.example.com`,那么Cookie的路徑就是根目錄`/`。這意味著,這個Cookie對該域名的根路徑和它的所有子路徑都有效。如果路徑設為`/forums`,那么這個Cookie只有在訪問`www.example.com/forums`及其子路徑時才有效。
瀏覽器可以設置不接受Cookie,也可以設置不向服務器發送Cookie。`window.navigator.cookieEnabled`屬性返回一個布爾值,表示瀏覽器是否打開Cookie功能。
`document.cookie`屬性返回當前網頁的Cookie。
```javascript
// 讀取當前網頁的所有cookie
var allCookies = document.cookie;
```
由于`document.cookie`返回的是分號分隔的所有Cookie,所以必須手動還原,才能取出每一個Cookie的值。
```javascript
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
// cookies[i] name=value形式的單個Cookie
}
```
`document.cookie`屬性是可寫的,可以通過它為當前網站添加Cookie。
```javascript
document.cookie = 'fontSize=14';
```
Cookie的值必須寫成`key=value`的形式。注意,等號兩邊不能有空格。另外,寫入Cookie的時候,必須對分號、逗號和空格進行轉義(它們都不允許作為Cookie的值),這可以用`encodeURIComponent`方法達到。
但是,`document.cookie`一次只能寫入一個Cookie,而且寫入并不是覆蓋,而是添加。
```javascript
document.cookie = 'test1=hello';
document.cookie = 'test2=world';
document.cookie
// test1=hello;test2=world
```
`document.cookie`屬性讀寫行為的差異(一次可以讀出全部Cookie,但是只能寫入一個Cookie),與服務器與瀏覽器之間的Cookie通信格式有關。瀏覽器向服務器發送Cookie的時候,是一行將所有Cookie全部發送。
```http
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: cookie_name1=cookie_value1; cookie_name2=cookie_value2
Accept: */*
```
上面的頭信息中,`Cookie`字段是瀏覽器向服務器發送的Cookie。
服務器告訴瀏覽器需要儲存Cookie的時候,則是分行指定。
```http
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: cookie_name1=cookie_value1
Set-Cookie: cookie_name2=cookie_value2; expires=Sun, 16 Jul 3567 06:23:41 GMT
```
上面的頭信息中,`Set-Cookie`字段是服務器寫入瀏覽器的Cookie,一行一個。
如果仔細看瀏覽器向服務器發送的Cookie,就會意識到,Cookie協議存在問題。對于服務器來說,有兩點是無法知道的。
- Cookie的各種屬性,比如何時過期。
- 哪個域名設置的Cookie,因為Cookie可能是一級域名設的,也可能是任意一個二級域名設的。
## Cookie的屬性
除了Cookie本身的內容,還有一些可選的屬性也是可以寫入的,它們都必須以分號開頭。
```http
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
```
上面的`Set-Cookie`字段,用分號分隔多個屬性。它們的含義如下。
(1)value屬性
`value`屬性是必需的,它是一個鍵值對,用于指定Cookie的值。
(2)expires屬性
`expires`屬性用于指定Cookie過期時間。它的格式采用`Date.toUTCString()`的格式。
如果不設置該屬性,或者設為`null`,Cookie只在當前會話(session)有效,瀏覽器窗口一旦關閉,當前Session結束,該Cookie就會被刪除。
瀏覽器根據本地時間,決定Cookie是否過期,由于本地時間是不精確的,所以沒有辦法保證Cookie一定會在服務器指定的時間過期。
(3)domain屬性
`domain`屬性指定Cookie所在的域名,比如`example.com`或`.example.com`(這種寫法將對所有子域名生效)、`subdomain.example.com`。
如果未指定,默認為設定該Cookie的域名。所指定的域名必須是當前發送Cookie的域名的一部分,比如當前訪問的域名是`example.com`,就不能將其設為`google.com`。只有訪問的域名匹配domain屬性,Cookie才會發送到服務器。
(4)path屬性
`path`屬性用來指定路徑,必須是絕對路徑(比如`/`、`/mydir`),如果未指定,默認為請求該Cookie的網頁路徑。
只有`path`屬性匹配向服務器發送的路徑,Cokie才會發送。這里的匹配不是絕對匹配,而是從根路徑開始,只要`path`屬性匹配發送路徑的一部分,就可以發送。比如,`path`屬性等于`/blog`,則發送路徑是`/blog`或者`/blogroll`,Cookie都會發送。`path`屬性生效的前提是`domain`屬性匹配。
(5)secure
`secure`屬性用來指定Cookie只能在加密協議HTTPS下發送到服務器。
該屬性只是一個開關,不需要指定值。如果通信是HTTPS協議,該開關自動打開。
(6)max-age
`max-age`屬性用來指定Cookie有效期,比如`60 * 60 * 24 * 365`(即一年31536e3秒)。
(7)HttpOnly
`HttpOnly`屬性用于設置該Cookie不能被JavaScript讀取,詳見下文的說明。
以上屬性可以同時設置一個或多個,也沒有次序的要求。如果服務器想改變一個早先設置的Cookie,必須同時滿足四個條件:Cookie的`key`、`domain`、`path`和`secure`都匹配。也就是說,如果原始的Cookie是用如下的`Set-Cookie`設置的。
```http
Set-Cookie: key1=value1; domain=example.com; path=/blog
```
改變上面這個cookie的值,就必須使用同樣的`Set-Cookie`。
```http
Set-Cookie: key1=value2; domain=example.com; path=/blog
```
只要有一個屬性不同,就會生成一個全新的Cookie,而不是替換掉原來那個Cookie。
```http
Set-Cookie: key1=value2; domain=example.com; path=/
```
上面的命令設置了一個全新的同名Cookie,但是`path`屬性不一樣。下一次訪問`example.com/blog`的時候,瀏覽器將向服務器發送兩個同名的Cookie。
```http
Cookie: key1=value1; key1=value2
```
上面代碼的兩個Cookie是同名的,匹配越精確的Cookie排在越前面。
瀏覽器設置這些屬性的寫法如下。
```javascript
document.cookie = 'fontSize=14; '
+ 'expires=' + someDate.toGMTString() + '; '
+ 'path=/subdirectory; '
+ 'domain=*.example.com';
```
另外,這些屬性只能用來設置Cookie。一旦設置完成,就沒有辦法讀取這些屬性的值。
刪除一個Cookie的簡便方法,就是設置`expires`屬性等于0,或者等于一個過去的日期。
```javascript
document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';
```
上面代碼中,名為`fontSize`的Cookie的值為空,過期時間設為1970年1月1月零點,就等同于刪除了這個Cookie。
## Cookie的限制
瀏覽器對Cookie數量的限制,規定不一樣。目前,Firefox是每個域名最多設置50個Cookie,而Safari和Chrome沒有域名數量的限制。
所有Cookie的累加長度限制為4KB。超過這個長度的Cookie,將被忽略,不會被設置。
由于Cookie可能存在數量限制,有時為了規避限制,可以將cookie設置成下面的形式。
```http
name=a=b&c=d&e=f&g=h
```
上面代碼實際上是設置了一個Cookie,但是這個Cookie內部使用`&`符號,設置了多部分的內容。因此,讀取這個Cookie的時候,就要自行解析,得到多個鍵值對。這樣就規避了cookie的數量限制。
## 同源政策
瀏覽器的同源政策規定,兩個網址只要域名相同和端口相同,就可以共享Cookie。
注意,這里不要求協議相同。也就是說,`http://example.com`設置的Cookie,可以被`https://example.com`讀取。
## HTTP-Only Cookie
設置cookie的時候,如果服務器加上了`HTTPOnly`屬性,則這個Cookie無法被JavaScript讀取(即`document.cookie`不會返回這個Cookie的值),只用于向服務器發送。
```http
Set-Cookie: key=value; HttpOnly
```
上面的這個Cookie將無法用JavaScript獲取。進行AJAX操作時,`XMLHttpRequest`對象也無法包括這個Cookie。這主要是為了防止XSS攻擊盜取Cookie。
<h2 id="6.6">Web Storage:瀏覽器端數據儲存機制</h2>
## 概述
這個API的作用是,使得網頁可以在瀏覽器端儲存數據。它分成兩類:sessionStorage和localStorage。
sessionStorage保存的數據用于瀏覽器的一次會話,當會話結束(通常是該窗口關閉),數據被清空;localStorage保存的數據長期存在,下一次訪問該網站的時候,網頁可以直接讀取以前保存的數據。除了保存期限的長短不同,這兩個對象的屬性和方法完全一樣。
它們很像cookie機制的強化版,能夠動用大得多的存儲空間。目前,每個域名的存儲上限視瀏覽器而定,Chrome是2.5MB,Firefox和Opera是5MB,IE是10MB。其中,Firefox的存儲空間由一級域名決定,而其他瀏覽器沒有這個限制。也就是說,在Firefox中,`a.example.com`和`b.example.com`共享5MB的存儲空間。另外,與Cookie一樣,它們也受同域限制。某個網頁存入的數據,只有同域下的網頁才能讀取。
通過檢查window對象是否包含sessionStorage和localStorage屬性,可以確定瀏覽器是否支持這兩個對象。
```javascript
function checkStorageSupport() {
// sessionStorage
if (window.sessionStorage) {
return true;
} else {
return false;
}
// localStorage
if (window.localStorage) {
return true;
} else {
return false;
}
}
```
## 操作方法
### 存入/讀取數據
sessionStorage和localStorage保存的數據,都以“鍵值對”的形式存在。也就是說,每一項數據都有一個鍵名和對應的值。所有的數據都是以文本格式保存。
存入數據使用setItem方法。它接受兩個參數,第一個是鍵名,第二個是保存的數據。
```javascript
sessionStorage.setItem("key","value");
localStorage.setItem("key","value");
```
讀取數據使用getItem方法。它只有一個參數,就是鍵名。
```javascript
var valueSession = sessionStorage.getItem("key");
var valueLocal = localStorage.getItem("key");
```
### 清除數據
removeItem方法用于清除某個鍵名對應的數據。
```javascript
sessionStorage.removeItem('key');
localStorage.removeItem('key');
```
clear方法用于清除所有保存的數據。
```javascript
sessionStorage.clear();
localStorage.clear();
```
### 遍歷操作
利用length屬性和key方法,可以遍歷所有的鍵。
```javascript
for(var i = 0; i < localStorage.length; i++){
console.log(localStorage.key(i));
}
```
其中的key方法,根據位置(從0開始)獲得鍵值。
```javascript
localStorage.key(1);
```
## storage事件
當儲存的數據發生變化時,會觸發storage事件。我們可以指定這個事件的回調函數。
```javascript
window.addEventListener("storage",onStorageChange);
```
回調函數接受一個event對象作為參數。這個event對象的key屬性,保存發生變化的鍵名。
```javascript
function onStorageChange(e) {
console.log(e.key);
}
```
除了key屬性,event對象的屬性還有三個:
- oldValue:更新前的值。如果該鍵為新增加,則這個屬性為null。
- newValue:更新后的值。如果該鍵被刪除,則這個屬性為null。
- url:原始觸發storage事件的那個網頁的網址。
值得特別注意的是,該事件不在導致數據變化的當前頁面觸發。如果瀏覽器同時打開一個域名下面的多個頁面,當其中的一個頁面改變sessionStorage或localStorage的數據時,其他所有頁面的storage事件會被觸發,而原始頁面并不觸發storage事件。可以通過這種機制,實現多個窗口之間的通信。所有瀏覽器之中,只有IE瀏覽器除外,它會在所有頁面觸發storage事件。
<h2 id="6.7">同源政策</h2>
瀏覽器安全的基石是”同源政策“([same-origin policy](https://en.wikipedia.org/wiki/Same-origin_policy))。很多開發者都知道這一點,但了解得不全面。
本節詳細介紹”同源政策“的各個方面,以及如何規避它。

## 概述
### 含義
1995年,同源政策由 Netscape 公司引入瀏覽器。目前,所有瀏覽器都實行這個政策。
最初,它的含義是指,A網頁設置的 Cookie,B網頁不能打開,除非這兩個網頁“同源”。所謂“同源”指的是”三個相同“。
> - 協議相同
> - 域名相同
> - 端口相同
舉例來說,`http://www.example.com/dir/page.html`這個網址,協議是`http://`,域名是`www.example.com`,端口是`80`(默認端口可以省略)。它的同源情況如下。
- `http://www.example.com/dir2/other.html`:同源
- `http://example.com/dir/other.html`:不同源(域名不同)
- `http://v2.www.example.com/dir/other.html`:不同源(域名不同)
- `http://www.example.com:81/dir/other.html`:不同源(端口不同)
- `https://www.example.com/dir/page.html`:不同源(協議不同)
### 目的
同源政策的目的,是為了保證用戶信息的安全,防止惡意的網站竊取數據。
設想這樣一種情況:A網站是一家銀行,用戶登錄以后,又去瀏覽其他網站。如果其他網站可以讀取A網站的 Cookie,會發生什么?
很顯然,如果 Cookie 包含隱私(比如存款總額),這些信息就會泄漏。更可怕的是,Cookie 往往用來保存用戶的登錄狀態,如果用戶沒有退出登錄,其他網站就可以冒充用戶,為所欲為。因為瀏覽器同時還規定,提交表單不受同源政策的限制。
由此可見,”同源政策“是必需的,否則 Cookie 可以共享,互聯網就毫無安全可言了。
### 限制范圍
隨著互聯網的發展,“同源政策”越來越嚴格。目前,如果非同源,共有三種行為受到限制。
> (1) Cookie、LocalStorage 和 IndexDB 無法讀取。
>
> (2) DOM 無法獲得。
>
> (3) AJAX 請求不能發送。
雖然這些限制是必要的,但是有時很不方便,合理的用途也受到影響。下面,我將詳細介紹,如何規避上面三種限制。
## Cookie
Cookie 是服務器寫入瀏覽器的一小段信息,只有同源的網頁才能共享。但是,兩個網頁一級域名相同,只是二級域名不同,瀏覽器允許通過設置`document.domain`共享 Cookie。
舉例來說,A網頁是`http://w1.example.com/a.html`,B網頁是`http://w2.example.com/b.html`,那么只要設置相同的`document.domain`,兩個網頁就可以共享Cookie。
```javascript
document.domain = 'example.com';
```
現在,A網頁通過腳本設置一個 Cookie。
```javascript
document.cookie = "test1=hello";
```
B網頁就可以讀到這個 Cookie。
```javascript
var allCookie = document.cookie;
```
注意,這種方法只適用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 無法通過這種方法,規避同源政策,而要使用下文介紹的PostMessage API。
另外,服務器也可以在設置Cookie的時候,指定Cookie的所屬域名為一級域名,比如`.example.com`。
```http
Set-Cookie: key=value; domain=.example.com; path=/
```
這樣的話,二級域名和三級域名不用做任何設置,都可以讀取這個Cookie。
## iframe
如果兩個網頁不同源,就無法拿到對方的DOM。典型的例子是`iframe`窗口和`window.open`方法打開的窗口,它們與父窗口無法通信。
比如,父窗口運行下面的命令,如果`iframe`窗口不是同源,就會報錯。
```javascript
document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
```
上面命令中,父窗口想獲取子窗口的DOM,因為跨源導致報錯。
反之亦然,子窗口獲取主窗口的DOM也會報錯。
```javascript
window.parent.document.body
// 報錯
```
如果兩個窗口一級域名相同,只是二級域名不同,那么設置上一節介紹的`document.domain`屬性,就可以規避同源政策,拿到DOM。
對于完全不同源的網站,目前有三種方法,可以解決跨域窗口的通信問題。
> - 片段識別符(fragment identifier)
> - window.name
> - 跨文檔通信API(Cross-document messaging)
### 片段識別符
片段標識符(fragment identifier)指的是,URL的`#`號后面的部分,比如`http://example.com/x.html#fragment`的`#fragment`。如果只是改變片段標識符,頁面不會重新刷新。
父窗口可以把信息,寫入子窗口的片段標識符。
```javascript
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
```
子窗口通過監聽`hashchange`事件得到通知。
```javascript
window.onhashchange = checkMessage;
function checkMessage() {
var message = window.location.hash;
// ...
}
```
同樣的,子窗口也可以改變父窗口的片段標識符。
```javascript
parent.location.href= target + “#” + hash;
```
### window.name
瀏覽器窗口有`window.name`屬性。這個屬性的最大特點是,無論是否同源,只要在同一個窗口里,前一個網頁設置了這個屬性,后一個網頁可以讀取它。
父窗口先打開一個子窗口,載入一個不同源的網頁,該網頁將信息寫入`window.name`屬性。
```javascript
window.name = data;
```
接著,子窗口跳回一個與主窗口同域的網址。
```javascript
location = 'http://parent.url.com/xxx.html';
```
然后,主窗口就可以讀取子窗口的`window.name`了。
```javascript
var data = document.getElementById('myFrame').contentWindow.name;
```
這種方法的優點是,`window.name`容量很大,可以放置非常長的字符串;缺點是必須監聽子窗口`window.name`屬性的變化,影響網頁性能。
### window.postMessage
上面兩種方法都屬于破解,HTML5為了解決這個問題,引入了一個全新的API:跨文檔通信 API(Cross-document messaging)。
這個API為`window`對象新增了一個`window.postMessage`方法,允許跨窗口通信,不論這兩個窗口是否同源。
舉例來說,父窗口`aaa.com`向子窗口`bbb.com`發消息,調用`postMessage`方法就可以了。
```javascript
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
```
`postMessage`方法的第一個參數是具體的信息內容,第二個參數是接收消息的窗口的源(origin),即“協議 + 域名 + 端口”。也可以設為`*`,表示不限制域名,向所有窗口發送。
子窗口向父窗口發送消息的寫法類似。
```javascript
window.opener.postMessage('Nice to see you', 'http://aaa.com');
```
父窗口和子窗口都可以通過`message`事件,監聽對方的消息。
```javascript
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
```
`message`事件的事件對象`event`,提供以下三個屬性。
> - `event.source`:發送消息的窗口
> - `event.origin`: 消息發向的網址
> - `event.data`: 消息內容
下面的例子是,子窗口通過`event.source`屬性引用父窗口,然后發送消息。
```javascript
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
```
上面代碼有幾個地方需要注意。首先,`receiveMessage`函數里面沒有過濾信息的來源,任意網址發來的信息都會被處理。其次,`postMessage`方法中指定的目標窗口的網址是一個星號,表示該信息可以向任意網址發送。通常來說,這兩種做法是不推薦的,因為不夠安全,可能會被惡意利用。
`event.origin`屬性可以過濾不是發給本窗口的消息。
```javascript
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
if (event.origin !== 'http://aaa.com') return;
if (event.data === 'Hello World') {
event.source.postMessage('Hello', event.origin);
} else {
console.log(event.data);
}
}
```
### LocalStorage
通過`window.postMessage`,讀寫其他窗口的 LocalStorage 也成為了可能。
下面是一個例子,主窗口寫入iframe子窗口的`localStorage`。
```javascript
window.onmessage = function(e) {
if (e.origin !== 'http://bbb.com') {
return;
}
var payload = JSON.parse(e.data);
localStorage.setItem(payload.key, JSON.stringify(payload.data));
};
```
上面代碼中,子窗口將父窗口發來的消息,寫入自己的LocalStorage。
父窗口發送消息的代碼如下。
```javascript
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com');
```
加強版的子窗口接收消息的代碼如下。
```javascript
window.onmessage = function(e) {
if (e.origin !== 'http://bbb.com') return;
var payload = JSON.parse(e.data);
switch (payload.method) {
case 'set':
localStorage.setItem(payload.key, JSON.stringify(payload.data));
break;
case 'get':
var parent = window.parent;
var data = localStorage.getItem(payload.key);
parent.postMessage(data, 'http://aaa.com');
break;
case 'remove':
localStorage.removeItem(payload.key);
break;
}
};
```
加強版的父窗口發送消息代碼如下。
```javascript
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入對象
win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com');
// 讀取對象
win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
window.onmessage = function(e) {
if (e.origin != 'http://aaa.com') return;
// "Jack"
console.log(JSON.parse(e.data).name);
};
```
## AJAX
同源政策規定,AJAX請求只能發給同源的網址,否則就報錯。
除了架設服務器代理(瀏覽器請求同源服務器,再由后者請求外部服務),有三種方法規避這個限制。
> - JSONP
> - WebSocket
> - CORS
### JSONP
JSONP是服務器與客戶端跨源通信的常用方法。最大特點就是簡單適用,老式瀏覽器全部支持,服務器改造非常小。
它的基本思想是,網頁通過添加一個`<script>`元素,向服務器請求JSON數據,這種做法不受同源政策限制;服務器收到請求后,將數據放在一個指定名字的回調函數里傳回來。
首先,網頁動態插入`<script>`元素,由它向跨源網址發出請求。
```javascript
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
```
上面代碼通過動態添加`<script>`元素,向服務器`example.com`發出請求。注意,該請求的查詢字符串有一個`callback`參數,用來指定回調函數的名字,這對于JSONP是必需的。
服務器收到這個請求以后,會將數據放在回調函數的參數位置返回。
```javascript
foo({
"ip": "8.8.8.8"
});
```
由于`<script>`元素請求的腳本,直接作為代碼運行。這時,只要瀏覽器定義了`foo`函數,該函數就會立即調用。作為參數的JSON數據被視為JavaScript對象,而不是字符串,因此避免了使用`JSON.parse`的步驟。
### WebSocket
WebSocket是一種通信協議,使用`ws://`(非加密)和`wss://`(加密)作為協議前綴。該協議不實行同源政策,只要服務器支持,就可以通過它進行跨源通信。
下面是一個例子,瀏覽器發出的WebSocket請求的頭信息(摘自[維基百科](https://en.wikipedia.org/wiki/WebSocket))。
```http
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
```
上面代碼中,有一個字段是`Origin`,表示該請求的請求源(origin),即發自哪個域名。
正是因為有了`Origin`這個字段,所以WebSocket才沒有實行同源政策。因為服務器可以根據這個字段,判斷是否許可本次通信。如果該域名在白名單內,服務器就會做出如下回應。
```http
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
```
### CORS
CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫。它是W3C標準,是跨源AJAX請求的根本解決方法。相比JSONP只能發`GET`請求,CORS允許任何類型的請求。
下一節將詳細介紹,如何通過CORS完成跨源AJAX請求。
<h2 id="6.8">AJAX</h2>
瀏覽器與服務器之間,采用HTTP協議通信。用戶在瀏覽器地址欄鍵入一個網址,或者通過網頁表單向服務器提交內容,這時瀏覽器就會向服務器發出HTTP請求。
1999年,微軟公司發布IE瀏覽器5.0版,第一次引入新功能:允許JavaScript腳本向服務器發起HTTP請求。這個功能當時并沒有引起注意,直到2004年Gmail發布和2005年Google Map發布,才引起廣泛重視。2005年2月,AJAX這個詞第一次正式提出,指圍繞這個功能進行開發的一整套做法。從此,AJAX成為腳本發起HTTP通信的代名詞,W3C也在2006年發布了它的國際標準。
具體來說,AJAX包括以下幾個步驟。
1. 創建AJAX對象
1. 發出HTTP請求
1. 接收服務器傳回的數據
1. 更新網頁數據
概括起來,就是一句話,AJAX通過原生的`XMLHttpRequest`對象發出HTTP請求,得到服務器返回的數據后,再進行處理。
AJAX可以是同步請求,也可以是異步請求。但是,大多數情況下,特指異步請求。因為同步的Ajax請求,對瀏覽器有”堵塞效應“。
## XMLHttpRequest對象
`XMLHttpRequest`對象用來在瀏覽器與服務器之間傳送數據。
```javascript
var ajax = new XMLHttpRequest();
ajax.open('GET', 'http://www.example.com/page.php', true);
```
上面代碼向指定的服務器網址,發出GET請求。
然后,AJAX指定回調函數,監聽通信狀態(`readyState`屬性)的變化。
```javascript
ajax.onreadystatechange = handleStateChange;
```
一旦拿到服務器返回的數據,AJAX不會刷新整個網頁,而是只更新相關部分,從而不打斷用戶正在做的事情。
注意,AJAX只能向同源網址(協議、域名、端口都相同)發出HTTP請求,如果發出跨源請求,就會報錯(詳見《同源政策》和《CORS機制》兩節)。
雖然名字里面有`XML`,但是實際上,XMLHttpRequest可以報送各種數據,包括字符串和二進制,而且除了HTTP,它還支持通過其他協議傳送(比如File和FTP)。
下面是`XMLHttpRequest`對象的典型用法。
```javascript
var xhr = new XMLHttpRequest();
// 指定通信過程中狀態改變時的回調函數
xhr.onreadystatechange = function(){
// 通信成功時,狀態值為4
if (xhr.readyState === 4){
if (xhr.status === 200){
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
};
// open方式用于指定HTTP動詞、請求的網址、是否異步
xhr.open('GET', '/endpoint', true);
// 發送HTTP請求
xhr.send(null);
```
`open`方法的第三個參數是一個布爾值,表示是否為異步請求。如果設為`false`,就表示這個請求是同步的,下面是一個例子。
```javascript
var request = new XMLHttpRequest();
request.open('GET', '/bar/foo.txt', false);
request.send(null);
if (request.status === 200) {
console.log(request.responseText);
}
```
## XMLHttpRequest實例的屬性
### readyState
`readyState`是一個只讀屬性,用一個整數和對應的常量,表示XMLHttpRequest請求當前所處的狀態。
- 0,對應常量`UNSENT`,表示XMLHttpRequest實例已經生成,但是`open()`方法還沒有被調用。
- 1,對應常量`OPENED`,表示`send()`方法還沒有被調用,仍然可以使用`setRequestHeader()`,設定HTTP請求的頭信息。
- 2,對應常量`HEADERS_RECEIVED`,表示`send()`方法已經執行,并且頭信息和狀態碼已經收到。
- 3,對應常量`LOADING`,表示正在接收服務器傳來的body部分的數據,如果`responseType`屬性是`text`或者空字符串,`responseText`就會包含已經收到的部分信息。
- 4,對應常量`DONE`,表示服務器數據已經完全接收,或者本次接收已經失敗了。
在通信過程中,每當發生狀態變化的時候,`readyState`屬性的值就會發生改變。這個值每一次變化,都會觸發`readyStateChange`事件。
```javascript
if (ajax.readyState == 4) {
// Handle the response.
} else {
// Show the 'Loading...' message or do nothing.
}
```
上面代碼表示,只有`readyState`變為4時,才算確認請求已經成功,其他值都表示請求還在進行中。
### onreadystatechange
`onreadystatechange`屬性指向一個回調函數,當`readystatechange`事件發生的時候,這個回調函數就會調用,并且XMLHttpRequest實例的`readyState`屬性也會發生變化。
另外,如果使用`abort()`方法,終止XMLHttpRequest請求,`onreadystatechange`回調函數也會被調用。
```javascript
var xmlhttp = new XMLHttpRequest();
xmlhttp.open( 'GET', 'http://example.com' , true );
xmlhttp.onreadystatechange = function () {
if ( XMLHttpRequest.DONE != xmlhttp.readyState ) {
return;
}
if ( 200 != xmlhttp.status ) {
return;
}
console.log( xmlhttp.responseText );
};
xmlhttp.send();
```
### response
`response`屬性為只讀,返回接收到的數據體(即body部分)。它的類型可以是ArrayBuffer、Blob、Document、JSON對象、或者一個字符串,這由`XMLHttpRequest.responseType`屬性的值決定。
如果本次請求沒有成功或者數據不完整,該屬性就會等于`null`。
### responseType
`responseType`屬性用來指定服務器返回數據(`xhr.response`)的類型。
- "":字符串(默認值)
- "arraybuffer":ArrayBuffer對象
- "blob":Blob對象
- "document":Document對象
- "json":JSON對象
- "text":字符串
text類型適合大多數情況,而且直接處理文本也比較方便,document類型適合返回XML文檔的情況,blob類型適合讀取二進制數據,比如圖片文件。
```javascript
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var blob = new Blob([this.response], {type: 'image/png'});
// 或者
var blob = oReq.response;
}
};
xhr.send();
```
如果將這個屬性設為ArrayBuffer,就可以按照數組的方式處理二進制數據。
```javascript
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
var uInt8Array = new Uint8Array(this.response);
for (var i = 0, len = binStr.length; i < len; ++i) {
// var byte = uInt8Array[i];
}
};
xhr.send();
```
如果將這個屬性設為“json”,支持JSON的瀏覽器(Firefox>9,chrome>30),就會自動對返回數據調用JSON.parse() 方法。也就是說,你從xhr.response屬性(注意,不是xhr.responseText屬性)得到的不是文本,而是一個JSON對象。
XHR2支持Ajax的返回類型為文檔,即xhr.responseType="document" 。這意味著,對于那些打開CORS的網站,我們可以直接用Ajax抓取網頁,然后不用解析HTML字符串,直接對XHR回應進行DOM操作。
### responseText
`responseText`屬性返回從服務器接收到的字符串,該屬性為只讀。如果本次請求沒有成功或者數據不完整,該屬性就會等于`null`。
如果服務器返回的數據格式是JSON,就可以使用`responseText`屬性。
```javascript
var data = ajax.responseText;
data = JSON.parse(data);
```
### responseXML
`responseXML`屬性返回從服務器接收到的Document對象,該屬性為只讀。如果本次請求沒有成功,或者數據不完整,或者不能被解析為XML或HTML,該屬性等于null。
返回的數據會被直接解析為DOM對象。
```javascript
/* 返回的XML文件如下
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<book>
<chapter id="1">(Re-)Introducing JavaScript</chapter>
<chapter id="2">JavaScript in Action</chapter>
</book>
*/
var data = ajax.responseXML;
var chapters = data.getElementsByTagName('chapter');
```
如果服務器返回的數據,沒有明示`Content-Type`頭信息等于`text/xml`,可以使用`overrideMimeType()`方法,指定XMLHttpRequest對象將返回的數據解析為XML。
### status
`status`屬性為只讀屬性,表示本次請求所得到的HTTP狀態碼,它是一個整數。一般來說,如果通信成功的話,這個狀態碼是200。
- 200, OK,訪問正常
- 301, Moved Permanently,永久移動
- 304, Not Modified,未修改
- 307, Temporary Redirect,暫時重定向
- 401, Unauthorized,未授權
- 403, Forbidden,禁止訪問
- 404, Not Found,未發現指定網址
- 500, Internal Server Error,服務器發生錯誤
基本上,只有2xx和304的狀態碼,表示服務器返回是正常狀態。
```javascript
if (ajax.readyState == 4) {
if ( (ajax.status >= 200 && ajax.status < 300)
|| (ajax.status == 304) ) {
// Handle the response.
} else {
// Status error!
}
}
```
### statusText
`statusText`屬性為只讀屬性,返回一個字符串,表示服務器發送的狀態提示。不同于`status`屬性,該屬性包含整個狀態信息,比如”200 OK“。
### timeout
`timeout`屬性等于一個整數,表示多少毫秒后,如果請求仍然沒有得到結果,就會自動終止。如果該屬性等于0,就表示沒有時間限制。
```javascript
var xhr = new XMLHttpRequest();
xhr.ontimeout = function () {
console.error("The request for " + url + " timed out.");
};
xhr.onload = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback.apply(xhr, args);
} else {
console.error(xhr.statusText);
}
}
};
xhr.open("GET", url, true);
xhr.timeout = timeout;
xhr.send(null);
}
```
### 事件監聽接口
XMLHttpRequest第一版,只能對`onreadystatechange`這一個事件指定回調函數。該事件對所有情況作出響應。 XMLHttpRequest第二版允許對更多的事件指定回調函數。
- onloadstart 請求發出
- onprogress 正在發送和加載數據
- onabort 請求被中止,比如用戶調用了`abort()`方法
- onerror 請求失敗
- onload 請求成功完成
- ontimeout 用戶指定的時限到期,請求還未完成
- onloadend 請求完成,不管成果或失敗
```javascript
xhr.onload = function() {
var responseText = xhr.responseText;
console.log(responseText);
// process the response.
};
xhr.onerror = function() {
console.log('There was an error!');
};
```
注意,如果發生網絡錯誤(比如服務器無法連通),`onerror`事件無法獲取報錯信息,所以只能顯示報錯。
### withCredentials
`withCredentials`屬性是一個布爾值,表示跨域請求時,用戶信息(比如Cookie和認證的HTTP頭信息)是否會包含在請求之中,默認為`false`。即向`example.com`發出跨域請求時,不會發送`example.com`設置在本機上的Cookie(如果有的話)。
如果你需要通過跨域AJAX發送Cookie,需要打開`withCredentials`。
```javascript
xhr.withCredentials = true;
```
為了讓這個屬性生效,服務器必須顯式返回`Access-Control-Allow-Credentials`這個頭信息。
```javascript
Access-Control-Allow-Credentials: true
```
`.withCredentials`屬性打開的話,不僅會發送Cookie,還會設置遠程主機指定的Cookie。注意,此時你的腳本還是遵守同源政策,無法 從`document.cookie`或者HTTP回應的頭信息之中,讀取這些Cookie。
## XMLHttpRequest實例的方法
### abort()
`abort`方法用來終止已經發出的HTTP請求。
```javascript
ajax.open('GET', 'http://www.example.com/page.php', true);
var ajaxAbortTimer = setTimeout(function() {
if (ajax) {
ajax.abort();
ajax = null;
}
}, 5000);
```
上面代碼在發出5秒之后,終止一個AJAX請求。
### getAllResponseHeaders()
`getAllResponseHeaders`方法返回服務器發來的所有HTTP頭信息。格式為字符串,每個頭信息之間使用`CRLF`分隔,如果沒有受到服務器回應,該屬性返回`null`。
### getResponseHeader()
`getResponseHeader`方法返回HTTP頭信息指定字段的值,如果還沒有收到服務器回應或者指定字段不存在,則該屬性為`null`。
```html
function getHeaderTime () {
console.log(this.getResponseHeader("Last-Modified"));
}
var oReq = new XMLHttpRequest();
oReq.open("HEAD", "yourpage.html");
oReq.onload = getHeaderTime;
oReq.send();
```
如果有多個字段同名,則它們的值會被連接為一個字符串,每個字段之間使用”逗號+空格“分隔。
### open()
`XMLHttpRequest`對象的`open`方法用于指定發送HTTP請求的參數,它的使用格式如下,一共可以接受五個參數。
```javascript
void open(
string method,
string url,
optional boolean async,
optional string user,
optional string password
);
```
- `method`:表示HTTP動詞,比如“GET”、“POST”、“PUT”和“DELETE”。
- `url`: 表示請求發送的網址。
- `async`: 格式為布爾值,默認為`true`,表示請求是否為異步。如果設為`false`,則`send()`方法只有等到收到服務器返回的結果,才會有返回值。
- `user`:表示用于認證的用戶名,默認為空字符串。
- `password`:表示用于認證的密碼,默認為空字符串。
如果對使用過`open()`方法的請求,再次使用這個方法,等同于調用`abort()`。
下面發送POST請求的例子。
```javascript
xhr.open('POST', encodeURI('someURL'));
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {};
xhr.send(encodeURI('dataString'));
```
上面方法中,open方法向指定URL發出POST請求,send方法送出實際的數據。
下面是一個同步AJAX請求的例子。
```javascript
var request = new XMLHttpRequest();
request.open('GET', '/bar/foo.txt', false);
request.send(null);
if (request.status === 200) {
console.log(request.responseText);
}
```
### send()
`send`方法用于實際發出HTTP請求。如果不帶參數,就表示HTTP請求只包含頭信息,也就是只有一個URL,典型例子就是GET請求;如果帶有參數,就表示除了頭信息,還帶有包含具體數據的信息體,典型例子就是POST請求。
```javascript
ajax.open('GET'
, 'http://www.example.com/somepage.php?id=' + encodeURIComponent(id)
, true
);
// 等同于
var data = 'id=' + encodeURIComponent(id));
ajax.open('GET', 'http://www.example.com/somepage.php', true);
ajax.send(data);
```
上面代碼中,`GET`請求的參數,可以作為查詢字符串附加在URL后面,也可以作為`send`方法的參數。
下面是發送POST請求的例子。
```javascript
var data = 'email='
+ encodeURIComponent(email)
+ '&password='
+ encodeURIComponent(password);
ajax.open('POST', 'http://www.example.com/somepage.php', true);
ajax.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
ajax.send(data);
```
如果請求是異步的(默認為異步),該方法在發出請求后會立即返回。如果請求為同步,該方法只有等到收到服務器回應后,才會返回。
注意,所有XMLHttpRequest的監聽事件,都必須在`send()`方法調用之前設定。
`send`方法的參數就是發送的數據。多種格式的數據,都可以作為它的參數。
```javascript
void send();
void send(ArrayBufferView data);
void send(Blob data);
void send(Document data);
void send(String data);
void send(FormData data);
```
如果發送`Document`數據,在發送之前,數據會先被串行化。
發送二進制數據,最好使用`ArrayBufferView`或`Blob`對象,這使得通過Ajax上傳文件成為可能。
下面是一個上傳`ArrayBuffer`對象的例子。
```javascript
function sendArrayBuffer() {
var xhr = new XMLHttpRequest();
var uInt8Array = new Uint8Array([1, 2, 3]);
xhr.open('POST', '/server', true);
xhr.onload = function(e) { ... };
xhr.send(uInt8Array.buffer);
}
```
FormData類型可以用于構造表單數據。
```javascript
var formData = new FormData();
formData.append('username', '張三');
formData.append('email', 'zhangsan@example.com');
formData.append('birthDate', 1940);
var xhr = new XMLHttpRequest();
xhr.open("POST", "/register");
xhr.send(formData);
```
上面的代碼構造了一個`formData`對象,然后使用send方法發送。它的效果與點擊下面表單的submit按鈕是一樣的。
```html
<form id='registration' name='registration' action='/register'>
<input type='text' name='username' value='張三'>
<input type='email' name='email' value='zhangsan@example.com'>
<input type='number' name='birthDate' value='1940'>
<input type='submit' onclick='return sendForm(this.form);'>
</form>
```
FormData也可以將現有表單構造生成。
```javascript
var formElement = document.querySelector("form");
var request = new XMLHttpRequest();
request.open("POST", "submitform.php");
request.send(new FormData(formElement));
```
FormData對象還可以對現有表單添加數據,這為我們操作表單提供了極大的靈活性。
```javascript
function sendForm(form) {
var formData = new FormData(form);
formData.append('csrf', 'e69a18d7db1286040586e6da1950128c');
var xhr = new XMLHttpRequest();
xhr.open('POST', form.action, true);
xhr.onload = function(e) {
// ...
};
xhr.send(formData);
return false;
}
var form = document.querySelector('#registration');
sendForm(form);
```
FormData對象也能用來模擬File控件,進行文件上傳。
```javascript
function uploadFiles(url, files) {
var formData = new FormData();
for (var i = 0, file; file = files[i]; ++i) {
formData.append(file.name, file); // 可加入第三個參數,表示文件名
}
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.onload = function(e) { ... };
xhr.send(formData); // multipart/form-data
}
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
uploadFiles('/server', this.files);
}, false);
```
FormData也可以加入JavaScript生成的文件。
```javascript
// 添加JavaScript生成的文件
var content = '<a id="a"><b id="b">hey!</b></a>';
var blob = new Blob([content], { type: "text/xml"});
formData.append("webmasterfile", blob);
```
### setRequestHeader()
`setRequestHeader`方法用于設置HTTP頭信息。該方法必須在`open()`之后、`send()`之前調用。如果該方法多次調用,設定同一個字段,則每一次調用的值會被合并成一個單一的值發送。
```javascript
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Content-Length', JSON.stringify(data).length);
xhr.send(JSON.stringify(data));
```
上面代碼首先設置頭信息`Content-Type`,表示發送JSON格式的數據;然后設置`Content-Length`,表示數據長度;最后發送JSON數據。
### overrideMimeType()
該方法用來指定服務器返回數據的MIME類型。該方法必須在`send()`之前調用。
傳統上,如果希望從服務器取回二進制數據,就要使用這個方法,人為將數據類型偽裝成文本數據。
```javascript
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
// 強制將MIME改為文本類型
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.onreadystatechange = function(e) {
if (this.readyState == 4 && this.status == 200) {
var binStr = this.responseText;
for (var i = 0, len = binStr.length; i < len; ++i) {
var c = binStr.charCodeAt(i);
var byte = c & 0xff; // 去除高位字節,留下低位字節
}
}
};
xhr.send();
```
上面代碼中,因為傳回來的是二進制數據,首先用`xhr.overrideMimeType`方法強制改變它的MIME類型,偽裝成文本數據。字符集必需指定為“x-user-defined”,如果是其他字符集,瀏覽器內部會強制轉碼,將其保存成UTF-16的形式。字符集“x-user-defined”其實也會發生轉碼,瀏覽器會在每個字節前面再加上一個字節(0xF700-0xF7ff),因此后面要對每個字符進行一次與運算(&),將高位的8個位去除,只留下低位的8個位,由此逐一讀出原文件二進制數據的每個字節。
這種方法很麻煩,在XMLHttpRequest版本升級以后,一般采用指定`responseType`的方法。
```javascript
var xhr = new XMLHttpRequest();
xhr.onload = function(e) {
var arraybuffer = xhr.response;
// ...
}
xhr.open("GET", url);
xhr.responseType = "arraybuffer";
xhr.send();
```
## XMLHttpRequest實例的事件
### readyStateChange事件
`readyState`屬性的值發生改變,就會觸發readyStateChange事件。
我們可以通過`onReadyStateChange`屬性,指定這個事件的回調函數,對不同狀態進行不同處理。尤其是當狀態變為4的時候,表示通信成功,這時回調函數就可以處理服務器傳送回來的數據。
### progress事件
上傳文件時,XMLHTTPRequest對象的upload屬性有一個progress,會不斷返回上傳的進度。
假定網頁上有一個progress元素。
```http
<progress min="0" max="100" value="0">0% complete</progress>
```
文件上傳時,對upload屬性指定progress事件回調函數,即可獲得上傳的進度。
```javascript
function upload(blobOrFile) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/server', true);
xhr.onload = function(e) { ... };
// Listen to the upload progress.
var progressBar = document.querySelector('progress');
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
progressBar.value = (e.loaded / e.total) * 100;
progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
}
};
xhr.send(blobOrFile);
}
upload(new Blob(['hello world'], {type: 'text/plain'}));
```
### load事件、error事件、abort事件
load事件表示服務器傳來的數據接收完畢,error事件表示請求出錯,abort事件表示請求被中斷。
```javascript
var xhr = new XMLHttpRequest();
xhr.addEventListener("progress", updateProgress);
xhr.addEventListener("load", transferComplete);
xhr.addEventListener("error", transferFailed);
xhr.addEventListener("abort", transferCanceled);
xhr.open();
function updateProgress (oEvent) {
if (oEvent.lengthComputable) {
var percentComplete = oEvent.loaded / oEvent.total;
// ...
} else {
// 回應的總數據量未知,導致無法計算百分比
}
}
function transferComplete(evt) {
console.log("The transfer is complete.");
}
function transferFailed(evt) {
console.log("An error occurred while transferring the file.");
}
function transferCanceled(evt) {
console.log("The transfer has been canceled by the user.");
}
```
### loadend事件
`abort`、`load`和`error`這三個事件,會伴隨一個`loadend`事件,表示請求結束,但不知道其是否成功。
```javascript
req.addEventListener("loadend", loadEnd);
function loadEnd(e) {
alert("請求結束(不知道是否成功)");
}
```
## 文件上傳
HTML網頁的`<form>`元素能夠以四種格式,向服務器發送數據。
- 使用`POST`方法,將`enctype`屬性設為`application/x-www-form-urlencoded`,這是默認方法。
```html
<form action="register.php" method="post" onsubmit="AJAXSubmit(this); return false;">
</form>
```
- 使用`POST`方法,將`enctype`屬性設為`text/plain`。
```html
<form action="register.php" method="post" enctype="text/plain" onsubmit="AJAXSubmit(this); return false;">
</form>
```
- 使用`POST`方法,將`enctype`屬性設為`multipart/form-data`。
```html
<form action="register.php" method="post" enctype="multipart/form-data" onsubmit="AJAXSubmit(this); return false;">
</form>
```
- 使用`GET`方法,`enctype`屬性將被忽略。
```html
<form action="register.php" method="get" onsubmit="AJAXSubmit(this); return false;">
</form>
```
某個表單有兩個字段,分別是`foo`和`baz`,其中`foo`字段的值等于`bar`,`baz`字段的值一個分為兩行的字符串。上面四種方法,都可以將這個表單發送到服務器。
第一種方法是默認方法,POST發送,Encoding type為application/x-www-form-urlencoded。
```http
Content-Type: application/x-www-form-urlencoded
foo=bar&baz=The+first+line.%0D%0AThe+second+line.%0D%0A
```
第二種方法是POST發送,Encoding type為text/plain。
```javascript
Content-Type: text/plain
foo=bar
baz=The first line.
The second line.
```
第三種方法是POST發送,Encoding type為multipart/form-data。
```http
Content-Type: multipart/form-data; boundary=---------------------------314911788813839
-----------------------------314911788813839
Content-Disposition: form-data; name="foo"
bar
-----------------------------314911788813839
Content-Disposition: form-data; name="baz"
The first line.
The second line.
-----------------------------314911788813839--
```
第四種方法是GET請求。
```http
?foo=bar&baz=The%20first%20line.%0AThe%20second%20line.
```
通常,我們使用file控件實現文件上傳。
```html
<form id="file-form" action="handler.php" method="POST">
<input type="file" id="file-select" name="photos[]" multiple/>
<button type="submit" id="upload-button">上傳</button>
</form>
```
上面HTML代碼中,file控件的multiple屬性,指定可以一次選擇多個文件;如果沒有這個屬性,則一次只能選擇一個文件。
file對象的files屬性,返回一個FileList對象,包含了用戶選中的文件。
```javascript
var fileSelect = document.getElementById('file-select');
var files = fileSelect.files;
```
然后,新建一個FormData對象的實例,用來模擬發送到服務器的表單數據,把選中的文件添加到這個對象上面。
```javascript
var formData = new FormData();
for (var i = 0; i < files.length; i++) {
var file = files[i];
if (!file.type.match('image.*')) {
continue;
}
formData.append('photos[]', file, file.name);
}
```
上面代碼中的FormData對象的append方法,除了可以添加文件,還可以添加二進制對象(Blob)或者字符串。
```javascript
// Files
formData.append(name, file, filename);
// Blobs
formData.append(name, blob, filename);
// Strings
formData.append(name, value);
```
append方法的第一個參數是表單的控件名,第二個參數是實際的值,第三個參數是可選的,通常是文件名。
最后,使用Ajax方法向服務器上傳文件。
```javascript
var xhr = new XMLHttpRequest();
xhr.open('POST', 'handler.php', true);
xhr.onload = function () {
if (xhr.status !== 200) {
alert('An error occurred!');
}
};
xhr.send(formData);
```
目前,各大瀏覽器(包括IE 10)都支持Ajax上傳文件。
除了使用FormData接口上傳,也可以直接使用File API上傳。
```javascript
var file = document.getElementById('test-input').files[0];
var xhr = new XMLHttpRequest();
xhr.open('POST', 'myserver/uploads');
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
```
可以看到,上面這種寫法比FormData的寫法,要簡單很多。
## Fetch API
### 基本用法
Ajax操作所用的XMLHttpRequest對象,已經有十多年的歷史,它的API設計并不是很好,輸入、輸出、狀態都在同一個接口管理,容易寫出非常混亂的代碼。Fetch API是一種新規范,用來取代XMLHttpRequest對象。它主要有兩個特點,一是簡化接口,將API分散在幾個不同的對象上,二是返回Promise對象,避免了嵌套的回調函數。
檢查瀏覽器是否部署了這個API的代碼如下。
```javascript
if (fetch in window){
// 支持
} else {
// 不支持
}
```
下面是一個Fetch API的簡單例子。
```javascript
var URL = 'http://some/path';
fetch(URL).then(function(response) {
return response.json();
}).then(function(json) {
someOperator(json);
});
```
上面代碼向服務器請求JSON文件,獲取后再做進一步處理。
下面比較XMLHttpRequest寫法與Fetch寫法的不同。
```javascript
function reqListener() {
var data = JSON.parse(this.responseText);
console.log(data);
}
function reqError(err) {
console.log('Fetch Error :-S', err);
}
var oReq = new XMLHttpRequest();
oReq.onload = reqListener;
oReq.onerror = reqError;
oReq.open('get', './api/some.json', true);
oReq.send();
```
同樣的操作用Fetch實現如下。
```javascript
fetch('./api/some.json')
.then(function(response) {
if (response.status !== 200) {
console.log('請求失敗,狀態碼:' + response.status);
return;
}
response.json().then(function(data) {
console.log(data);
});
}).catch(function(err) {
console.log('出錯:', err);
});
```
上面代碼中,因為HTTP請求返回的response對象是一個Stream對象,所以需要使用`response.json`方法轉為JSON格式,不過這個方法返回的是一個Promise對象。
### fetch()
fetch方法的第一個參數可以是URL字符串,也可以是后文要講到的Request對象實例。Fetch方法返回一個Promise對象,并將一個response對象傳給回調函數。
response對象還有一個ok屬性,如果返回的狀態碼在200到299之間(即請求成功),這個屬性為true,否則為false。因此,上面的代碼可以寫成下面這樣。
```javascript
fetch("./api/some.json").then(function(response) {
if (response.ok) {
response.json().then(function(data) {
console.log(data);
});
} else {
console.log("請求失敗,狀態碼為", response.status);
}
}, function(err) {
console.log("出錯:", err);
});
```
response對象除了json方法,還包含了HTTP回應的元數據。
```javascript
fetch('users.json').then(function(response) {
console.log(response.headers.get('Content-Type'));
console.log(response.headers.get('Date'));
console.log(response.status);
console.log(response.statusText);
console.log(response.type);
console.log(response.url);
});
```
上面代碼中,response對象有很多屬性,其中的`response.type`屬性比較特別,表示HTTP回應的類型,它有以下三個值。
- basic:正常的同域請求
- cors:CORS機制下的跨域請求
- opaque:非CORS機制下的跨域請求,這時無法讀取返回的數據,也無法判斷是否請求成功
如果需要在CORS機制下發出跨域請求,需要指明狀態。
```javascript
fetch('http://some-site.com/cors-enabled/some.json', {mode: 'cors'})
.then(function(response) {
return response.text();
})
.then(function(text) {
console.log('Request successful', text);
})
.catch(function(error) {
log('Request failed', error)
});
```
除了指定模式,fetch方法的第二個參數還可以用來配置其他值,比如指定cookie連同HTTP請求一起發出。
```javascript
fetch(url, {
credentials: 'include'
})
```
發出POST請求的寫法如下。
```javascript
fetch("http://www.example.org/submit.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "firstName=Nikhil&favColor=blue&password=easytoguess"
}).then(function(res) {
if (res.ok) {
console.log("Perfect! Your settings are saved.");
} else if (res.status == 401) {
console.log("Oops! You are not authorized.");
}
}, function(e) {
console.log("Error submitting form!");
});
```
目前,還有一些XMLHttpRequest對象可以做到,但是Fetch API還沒做到的地方,比如中途中斷HTTP請求,以及獲取HTTP請求的進度。這些不足與Fetch返回的是Promise對象有關。
### Headers
Fetch API引入三個新的對象(也是構造函數):Headers, Request 和 Response。其中,Headers對象用來構造/讀取HTTP數據包的頭信息。
```javascript
var content = "Hello World";
var reqHeaders = new Headers();
reqHeaders.append("Content-Type", "text/plain");
reqHeaders.append("Content-Length", content.length.toString());
reqHeaders.append("X-Custom-Header", "ProcessThisImmediately");
```
Headers對象的實例,除了使用append方法添加屬性,也可以直接通過構造函數一次性生成。
```javascript
reqHeaders = new Headers({
"Content-Type": "text/plain",
"Content-Length": content.length.toString(),
"X-Custom-Header": "ProcessThisImmediately",
});
```
Headers對象實例還提供了一些工具方法。
```javascript
reqHeaders.has("Content-Type") // true
reqHeaders.has("Set-Cookie") // false
reqHeaders.set("Content-Type", "text/html")
reqHeaders.append("X-Custom-Header", "AnotherValue")
reqHeaders.get("Content-Length") // 11
reqHeaders.getAll("X-Custom-Header") // ["ProcessThisImmediately", "AnotherValue"]
reqHeaders.delete("X-Custom-Header")
reqHeaders.getAll("X-Custom-Header") // []
```
生成Header實例以后,可以將它作為第二個參數,傳入Request方法。
```javascript
var headers = new Headers();
headers.append('Accept', 'application/json');
var request = new Request(URL, {headers: headers});
fetch(request).then(function(response) {
console.log(response.headers);
});
```
同樣地,Headers實例可以用來構造Response方法。
```javascript
var headers = new Headers({
'Content-Type': 'application/json',
'Cache-Control': 'max-age=3600'
});
var response = new Response(
JSON.stringify({photos: {photo: []}}),
{'status': 200, headers: headers}
);
response.json().then(function(json) {
insertPhotos(json);
});
```
上面代碼中,構造了一個HTTP回應。目前,瀏覽器構造HTTP回應沒有太大用處,但是隨著Service Worker的部署,不久瀏覽器就可以向Service Worker發出HTTP回應。
### Request對象
Request對象用來構造HTTP請求。
```javascript
var req = new Request("/index.html");
req.method // "GET"
req.url // "http://example.com/index.html"
```
Request對象的第二個參數,表示配置對象。
```javascript
var uploadReq = new Request("/uploadImage", {
method: "POST",
headers: {
"Content-Type": "image/png",
},
body: "image data"
});
```
上面代碼指定Request對象使用POST方法發出,并指定HTTP頭信息和信息體。
下面是另一個例子。
```javascript
var req = new Request(URL, {method: 'GET', cache: 'reload'});
fetch(req).then(function(response) {
return response.json();
}).then(function(json) {
someOperator(json);
});
```
上面代碼中,指定請求方法為GET,并且要求瀏覽器不得緩存response。
Request對象實例有兩個屬性是只讀的,不能手動設置。一個是referrer屬性,表示請求的來源,由瀏覽器設置,有可能是空字符串。另一個是context屬性,表示請求發出的上下文,如果是image,表示是從img標簽發出,如果是worker,表示是從worker腳本發出,如果是fetch,表示是從fetch函數發出的。
Request對象實例的mode屬性,用來設置是否跨域,合法的值有以下三種:same-origin、no-cors(默認值)、cors。當設置為same-origin時,只能向同域的URL發出請求,否則會報錯。
```javascript
var arbitraryUrl = document.getElementById("url-input").value;
fetch(arbitraryUrl, { mode: "same-origin" }).then(function(res) {
console.log("Response succeeded?", res.ok);
}, function(e) {
console.log("Please enter a same-origin URL!");
});
```
上面代碼中,如果用戶輸入的URL不是同域的,將會報錯,否則就會發出請求。
如果mode屬性為no-cors,就與默認的瀏覽器行為沒有不同,類似script標簽加載外部腳本文件、img標簽加載外部圖片。如果mode屬性為cors,就可以向部署了CORS機制的服務器,發出跨域請求。
```javascript
var u = new URLSearchParams();
u.append('method', 'flickr.interestingness.getList');
u.append('api_key', '<insert api key here>');
u.append('format', 'json');
u.append('nojsoncallback', '1');
var apiCall = fetch('https://api.flickr.com/services/rest?' + u);
apiCall.then(function(response) {
return response.json().then(function(json) {
// photo is a list of photos.
return json.photos.photo;
});
}).then(function(photos) {
photos.forEach(function(photo) {
console.log(photo.title);
});
});
```
上面代碼是向Flickr API發出圖片請求的例子。
Request對象的一個很有用的功能,是在其他Request實例的基礎上,生成新的Request實例。
```javascript
var postReq = new Request(req, {method: 'POST'});
```
### Response
fetch方法返回Response對象實例,它有以下屬性。
- status:整數值,表示狀態碼(比如200)
- statusText:字符串,表示狀態信息,默認是“OK”
- ok:布爾值,表示狀態碼是否在200-299的范圍內
- headers:Headers對象,表示HTTP回應的頭信息
- url:字符串,表示HTTP請求的網址
- type:字符串,合法的值有五個basic、cors、default、error、opaque。basic表示正常的同域請求;cors表示CORS機制的跨域請求;error表示網絡出錯,無法取得信息,status屬性為0,headers屬性為空,并且導致fetch函數返回Promise對象被拒絕;opaque表示非CORS機制的跨域請求,受到嚴格限制。
Response對象還有兩個靜態方法。
- Response.error() 返回一個type屬性為error的Response對象實例
- Response.redirect(url, status) 返回的Response對象實例會重定向到另一個URL
### body屬性
Request對象和Response對象都有body屬性,表示請求的內容。body屬性可能是以下的數據類型。
- ArrayBuffer
- ArrayBufferView (Uint8Array等)
- Blob/File
- string
- URLSearchParams
- FormData
```javascript
var form = new FormData(document.getElementById('login-form'));
fetch("/login", {
method: "POST",
body: form
})
```
上面代碼中,Request對象的body屬性為表單數據。
Request對象和Response對象都提供以下方法,用來讀取body。
- arrayBuffer()
- blob()
- json()
- text()
- formData()
注意,上面這些方法都只能使用一次,第二次使用就會報錯,也就是說,body屬性只能讀取一次。Request對象和Response對象都有bodyUsed屬性,返回一個布爾值,表示body是否被讀取過。
```javascript
var res = new Response("one time use");
console.log(res.bodyUsed); // false
res.text().then(function(v) {
console.log(res.bodyUsed); // true
});
console.log(res.bodyUsed); // true
res.text().catch(function(e) {
console.log("Tried to read already consumed Response");
});
```
上面代碼中,第二次通過text方法讀取Response對象實例的body時,就會報錯。
這是因為body屬性是一個stream對象,數據只能單向傳送一次。這樣的設計是為了允許JavaScript處理視頻、音頻這樣的大型文件。
如果希望多次使用body屬性,可以使用Response對象和Request對象的clone方法。它必須在body還沒有讀取前調用,返回一個前的body,也就是說,需要使用幾次body,就要調用幾次clone方法。
```javascript
addEventListener('fetch', function(evt) {
var sheep = new Response("Dolly");
console.log(sheep.bodyUsed); // false
var clone = sheep.clone();
console.log(clone.bodyUsed); // false
clone.text();
console.log(sheep.bodyUsed); // false
console.log(clone.bodyUsed); // true
evt.respondWith(cache.add(sheep.clone()).then(function(e) {
return sheep;
});
});
```
<h2 id="6.9">CORS通信</h2>
CORS是一個W3C標準,全稱是“跨域資源共享”(Cross-origin resource sharing)。
它允許瀏覽器向跨源服務器,發出[`XMLHttpRequest`](http://www.ruanyifeng.com/blog/2012/09/xmlhttprequest_level_2.html)請求,從而克服了AJAX只能[同源](http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html)使用的限制。
本文詳細介紹CORS的內部機制。
## 簡介
CORS需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10。
整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對于開發者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。
因此,實現CORS通信的關鍵是服務器。只要服務器實現了CORS接口,就可以跨源通信。
## 兩種請求
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
只要同時滿足以下兩大條件,就屬于簡單請求。
(1)請求方法是以下三種方法之一。
> - HEAD
> - GET
> - POST
(2)HTTP的頭信息不超出以下幾種字段。
> - Accept
> - Accept-Language
> - Content-Language
> - Last-Event-ID
> - Content-Type:只限于三個值`application/x-www-form-urlencoded`、`multipart/form-data`、`text/plain`
凡是不同時滿足上面兩個條件,就屬于非簡單請求。
瀏覽器對這兩種請求的處理,是不一樣的。
## 簡單請求
### 基本流程
對于簡單請求,瀏覽器直接發出CORS請求。具體來說,就是在頭信息之中,增加一個`Origin`字段。
下面是一個例子,瀏覽器發現這次跨源AJAX請求是簡單請求,就自動在頭信息之中,添加一個`Origin`字段。
```http
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
```
上面的頭信息中,`Origin`字段用來說明,本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。
如果`Origin`指定的源,不在許可范圍內,服務器會返回一個正常的HTTP回應。瀏覽器發現,這個回應的頭信息沒有包含`Access-Control-Allow-Origin`字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被`XMLHttpRequest`的`onerror`回調函數捕獲。注意,這種錯誤無法通過狀態碼識別,因為HTTP回應的狀態碼有可能是200。
如果`Origin`指定的域名在許可范圍內,服務器返回的響應,會多出幾個頭信息字段。
```http
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
```
上面的頭信息之中,有三個與CORS請求相關的字段,都以`Access-Control-`開頭。
**(1)`Access-Control-Allow-Origin`**
該字段是必須的。它的值要么是請求時`Origin`字段的值,要么是一個`*`,表示接受任意域名的請求。
**(2)`Access-Control-Allow-Credentials`**
該字段可選。它的值是一個布爾值,表示是否允許發送Cookie。默認情況下,Cookie不包括在CORS請求之中。設為`true`,即表示服務器明確許可,Cookie可以包含在請求中,一起發給服務器。這個值也只能設為`true`,如果服務器不要瀏覽器發送Cookie,刪除該字段即可。
**(3)`Access-Control-Expose-Headers`**
該字段可選。CORS請求時,`XMLHttpRequest`對象的`getResponseHeader()`方法只能拿到6個基本字段:`Cache-Control`、`Content-Language`、`Content-Type`、`Expires`、`Last-Modified`、`Pragma`。如果想拿到其他字段,就必須在`Access-Control-Expose-Headers`里面指定。上面的例子指定,`getResponseHeader('FooBar')`可以返回`FooBar`字段的值。
### withCredentials 屬性
上面說到,CORS請求默認不包含Cookie信息(以及HTTP認證信息等)。如果需要包含Cookie信息,一方面要服務器同意,指定`Access-Control-Allow-Credentials`字段。
```http
Access-Control-Allow-Credentials: true
```
另一方面,開發者必須在AJAX請求中打開`withCredentials`屬性。
```javascript
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
```
否則,即使服務器同意發送Cookie,瀏覽器也不會發送。或者,服務器要求設置Cookie,瀏覽器也不會處理。
但是,如果省略`withCredentials`設置,有的瀏覽器還是會一起發送Cookie。這時,可以顯式關閉`withCredentials`。
```javascript
xhr.withCredentials = false;
```
需要注意的是,如果要發送Cookie,`Access-Control-Allow-Origin`就不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用服務器域名設置的Cookie才會上傳,其他域名的Cookie并不會上傳,且(跨源)原網頁代碼中的`document.cookie`也無法讀取服務器域名下的Cookie。
## 非簡單請求
### 預檢請求
非簡單請求是那種對服務器有特殊要求的請求,比如請求方法是`PUT`或`DELETE`,或者`Content-Type`字段的類型是`application/json`。
非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為”預檢“請求(preflight)。
瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答復,瀏覽器才會發出正式的`XMLHttpRequest`請求,否則就報錯。
下面是一段瀏覽器的JavaScript腳本。
```javascript
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
```
上面代碼中,HTTP請求的方法是`PUT`,并且發送一個自定義頭信息`X-Custom-Header`。
瀏覽器發現,這是一個非簡單請求,就自動發出一個”預檢“請求,要求服務器確認可以這樣請求。下面是這個“預檢”請求的HTTP頭信息。
```http
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
```
“預檢”請求用的請求方法是`OPTIONS`,表示這個請求是用來詢問的。頭信息里面,關鍵字段是`Origin`,表示請求來自哪個源。
除了`Origin`字段,“預檢”請求的頭信息包括兩個特殊字段。
**(1)`Access-Control-Request-Method`**
該字段是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是`PUT`。
**(2)`Access-Control-Request-Headers`**
該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段,上例是`X-Custom-Header`。
### 預檢請求的回應
服務器收到“預檢”請求以后,檢查了`Origin`、`Access-Control-Request-Method`和`Access-Control-Request-Headers`字段以后,確認允許跨源請求,就可以做出回應。
```http
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
```
上面的HTTP回應中,關鍵的是`Access-Control-Allow-Origin`字段,表示`http://api.bob.com`可以請求數據。該字段也可以設為星號,表示同意任意跨源請求。
```http
Access-Control-Allow-Origin: *
```
如果服務器否定了”預檢“請求,會返回一個正常的HTTP回應,但是沒有任何CORS相關的頭信息字段。這時,瀏覽器就會認定,服務器不同意預檢請求,因此觸發一個錯誤,被`XMLHttpRequest`對象的`onerror`回調函數捕獲。控制臺會打印出如下的報錯信息。
```bash
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
```
服務器回應的其他CORS相關字段如下。
```http
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Headers: true
Access-Control-Max-Age: 1728000
```
**(1)`Access-Control-Allow-Methods`**
該字段必需,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請求的那個方法。這是為了避免多次“預檢”請求。
**(2)`Access-Control-Allow-Headers`**
如果瀏覽器請求包括`Access-Control-Request-Headers`字段,則`Access-Control-Allow-Headers`字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限于瀏覽器在”預檢“中請求的字段。
**(3)`Access-Control-Allow-Credentials`**
該字段與簡單請求時的含義相同。
**(4)`Access-Control-Max-Age`**
該字段可選,用來指定本次預檢請求的有效期,單位為秒。上面結果中,有效期是20天(1728000秒),即允許緩存該條回應1728000秒(即20天),在此期間,不用發出另一條預檢請求。
### 瀏覽器的正常請求和回應
一旦服務器通過了“預檢”請求,以后每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個`Origin`頭信息字段。服務器的回應,也都會有一個`Access-Control-Allow-Origin`頭信息字段。
下面是“預檢”請求之后,瀏覽器的正常CORS請求。
```http
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
```
上面頭信息的`Origin`字段是瀏覽器自動添加的。
下面是服務器正常的回應。
```http
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
```
上面頭信息中,`Access-Control-Allow-Origin`字段是每次回應都必定包含的。
## 與JSONP的比較
CORS與JSONP的使用目的相同,但是比JSONP更強大。
JSONP只支持`GET`請求,CORS支持所有類型的HTTP請求。JSONP的優勢在于支持老式瀏覽器,以及可以向不支持CORS的網站請求數據。
<h2 id="6.10">IndexedDB:瀏覽器端數據庫</h2>
## 概述
隨著瀏覽器的處理能力不斷增強,越來越多的網站開始考慮,將大量數據儲存在客戶端,這樣可以減少用戶等待從服務器獲取數據的時間。
現有的瀏覽器端數據儲存方案,都不適合儲存大量數據:cookie不超過4KB,且每次請求都會發送回服務器端;Window.name屬性缺乏安全性,且沒有統一的標準;localStorage在2.5MB到10MB之間(各家瀏覽器不同)。所以,需要一種新的解決方案,這就是IndexedDB誕生的背景。
通俗地說,IndexedDB就是瀏覽器端數據庫,可以被網頁腳本程序創建和操作。它允許儲存大量數據,提供查找接口,還能建立索引。這些都是localStorage所不具備的。就數據庫類型而言,IndexedDB不屬于關系型數據庫(不支持SQL查詢語句),更接近NoSQL數據庫。
IndexedDB具有以下特點。
**(1)鍵值對儲存。** IndexedDB內部采用對象倉庫(object store)存放數據。所有類型的數據都可以直接存入,包括JavaScript對象。在對象倉庫中,數據以“鍵值對”的形式保存,每一個數據都有對應的鍵名,鍵名是獨一無二的,不能有重復,否則會拋出一個錯誤。
**(2)異步。** IndexedDB操作時不會鎖死瀏覽器,用戶依然可以進行其他操作,這與localStorage形成對比,后者的操作是同步的。異步設計是為了防止大量數據的讀寫,拖慢網頁的表現。
**(3)支持事務。** IndexedDB支持事務(transaction),這意味著一系列操作步驟之中,只要有一步失敗,整個事務就都取消,數據庫回到事務發生之前的狀態,不存在只改寫一部分數據的情況。
**(4)同域限制** IndexedDB也受到同域限制,每一個數據庫對應創建該數據庫的域名。來自不同域名的網頁,只能訪問自身域名下的數據庫,而不能訪問其他域名下的數據庫。
**(5)儲存空間大** IndexedDB的儲存空間比localStorage大得多,一般來說不少于250MB。IE的儲存上限是250MB,Chrome和Opera是剩余空間的某個百分比,Firefox則沒有上限。
**(6)支持二進制儲存。** IndexedDB不僅可以儲存字符串,還可以儲存二進制數據。
目前,Chrome 27+、Firefox 21+、Opera 15+和IE 10+支持這個API,但是Safari完全不支持。
下面的代碼用來檢查瀏覽器是否支持這個API。
```javascript
if("indexedDB" in window) {
// 支持
} else {
// 不支持
}
```
## indexedDB.open方法
瀏覽器原生提供indexedDB對象,作為開發者的操作接口。indexedDB.open方法用于打開數據庫。
```javascript
var openRequest = indexedDB.open("test",1);
```
open方法的第一個參數是數據庫名稱,格式為字符串,不可省略;第二個參數是數據庫版本,是一個大于0的正整數(0將報錯)。上面代碼表示,打開一個名為test、版本為1的數據庫。如果該數據庫不存在,則會新建該數據庫。如果省略第二個參數,則會自動創建版本為1的該數據庫。
打開數據庫的結果是,有可能觸發4種事件。
- **success**:打開成功。
- **error**:打開失敗。
- **upgradeneeded**:第一次打開該數據庫,或者數據庫版本發生變化。
- **blocked**:上一次的數據庫連接還未關閉。
第一次打開數據庫時,會先觸發upgradeneeded事件,然后觸發success事件。
根據不同的需要,對上面4種事件設立回調函數。
```javascript
var openRequest = indexedDB.open("test",1);
var db;
openRequest.onupgradeneeded = function(e) {
console.log("Upgrading...");
}
openRequest.onsuccess = function(e) {
console.log("Success!");
db = e.target.result;
}
openRequest.onerror = function(e) {
console.log("Error");
console.dir(e);
}
```
上面代碼有兩個地方需要注意。首先,open方法返回的是一個對象(IDBOpenDBRequest),回調函數定義在這個對象上面。其次,回調函數接受一個事件對象event作為參數,它的target.result屬性就指向打開的IndexedDB數據庫。
## indexedDB實例對象的方法
獲得數據庫實例以后,就可以用實例對象的方法操作數據庫。
### createObjectStore方法
createObjectStore方法用于創建存放數據的“對象倉庫”(object store),類似于傳統關系型數據庫的表格。
```javascript
db.createObjectStore("firstOS");
```
上面代碼創建了一個名為firstOS的對象倉庫,如果該對象倉庫已經存在,就會拋出一個錯誤。為了避免出錯,需要用到下文的objectStoreNames屬性,檢查已有哪些對象倉庫。
createObjectStore方法還可以接受第二個對象參數,用來設置“對象倉庫”的屬性。
```javascript
db.createObjectStore("test", { keyPath: "email" });
db.createObjectStore("test2", { autoIncrement: true });
```
上面代碼中的keyPath屬性表示,所存入對象的email屬性用作每條記錄的鍵名(由于鍵名不能重復,所以存入之前必須保證數據的email屬性值都是不一樣的),默認值為null;autoIncrement屬性表示,是否使用自動遞增的整數作為鍵名(第一個數據為1,第二個數據為2,以此類推),默認為false。一般來說,keyPath和autoIncrement屬性只要使用一個就夠了,如果兩個同時使用,表示鍵名為遞增的整數,且對象不得缺少指定屬性。
### objectStoreNames屬性
objectStoreNames屬性返回一個DOMStringList對象,里面包含了當前數據庫所有“對象倉庫”的名稱。可以使用DOMStringList對象的contains方法,檢查數據庫是否包含某個“對象倉庫”。
```javascript
if(!db.objectStoreNames.contains("firstOS")) {
db.createObjectStore("firstOS");
}
```
上面代碼先判斷某個“對象倉庫”是否存在,如果不存在就創建該對象倉庫。
### transaction方法
transaction方法用于創建一個數據庫事務。向數據庫添加數據之前,必須先創建數據庫事務。
```javascript
var t = db.transaction(["firstOS"],"readwrite");
```
transaction方法接受兩個參數:第一個參數是一個數組,里面是所涉及的對象倉庫,通常是只有一個;第二個參數是一個表示操作類型的字符串。目前,操作類型只有兩種:readonly(只讀)和readwrite(讀寫)。添加數據使用readwrite,讀取數據使用readonly。
transaction方法返回一個事務對象,該對象的objectStore方法用于獲取指定的對象倉庫。
```javascript
var t = db.transaction(["firstOS"],"readwrite");
var store = t.objectStore("firstOS");
```
transaction方法有三個事件,可以用來定義回調函數。
- **abort**:事務中斷。
- **complete**:事務完成。
- **error**:事務出錯。
```javascript
var transaction = db.transaction(["note"], "readonly");
transaction.oncomplete = function(event) {
// some code
};
```
事務對象有以下方法,用于操作數據。
**(1)添加數據:add方法**
獲取對象倉庫以后,就可以用add方法往里面添加數據了。
```javascript
var store = t.objectStore("firstOS");
var o = {p: 123};
var request = store.add(o,1);
```
add方法的第一個參數是所要添加的數據,第二個參數是這條數據對應的鍵名(key),上面代碼將對象o的鍵名設為1。如果在創建數據倉庫時,對鍵名做了設置,這里也可以不指定鍵名。
add方法是異步的,有自己的success和error事件,可以對這兩個事件指定回調函數。
```javascript
var request = store.add(o,1);
request.onerror = function(e) {
console.log("Error",e.target.error.name);
// error handler
}
request.onsuccess = function(e) {
console.log("數據添加成功!");
}
```
**(2)讀取數據:get方法**
讀取數據使用get方法,它的參數是數據的鍵名。
```javascript
var t = db.transaction(["test"], "readonly");
var store = t.objectStore("test");
var ob = store.get(x);
```
get方法也是異步的,會觸發自己的success和error事件,可以對它們指定回調函數。
```javascript
var ob = store.get(x);
ob.onsuccess = function(e) {
// ...
}
```
從創建事務到讀取數據,所有操作方法也可以寫成下面這樣鏈式形式。
```javascript
db.transaction(["test"], "readonly")
.objectStore("test")
.get(X)
.onsuccess = function(e){}
```
**(3)更新記錄:put方法**
put方法的用法與add方法相近。
```javascript
var o = { p:456 };
var request = store.put(o, 1);
```
**(4)刪除記錄:delete方法**
刪除記錄使用delete方法。
```javascript
var t = db.transaction(["people"], "readwrite");
var request = t.objectStore("people").delete(thisId);
```
delete方法的參數是數據的鍵名。另外,delete也是一個異步操作,可以為它指定回調函數。
**(5)遍歷數據:openCursor方法**
如果想要遍歷數據,就要openCursor方法,它在當前對象倉庫里面建立一個讀取光標(cursor)。
```javascript
var t = db.transaction(["test"], "readonly");
var store = t.objectStore("test");
var cursor = store.openCursor();
```
openCursor方法也是異步的,有自己的success和error事件,可以對它們指定回調函數。
```javascript
cursor.onsuccess = function(e) {
var res = e.target.result;
if(res) {
console.log("Key", res.key);
console.dir("Data", res.value);
res.continue();
}
}
```
回調函數接受一個事件對象作為參數,該對象的target.result屬性指向當前數據對象。當前數據對象的key和value分別返回鍵名和鍵值(即實際存入的數據)。continue方法將光標移到下一個數據對象,如果當前數據對象已經是最后一個數據了,則光標指向null。
openCursor方法還可以接受第二個參數,表示遍歷方向,默認值為next,其他可能的值為prev、nextunique和prevunique。后兩個值表示如果遇到重復值,會自動跳過。
### createIndex方法
createIndex方法用于創建索引。
假定對象倉庫中的數據對象都是下面person類型的。
```javascript
var person = {
name:name,
email:email,
created:new Date()
}
```
可以指定這個數據對象的某個屬性來建立索引。
```javascript
var store = db.createObjectStore("people", { autoIncrement:true });
store.createIndex("name","name", {unique:false});
store.createIndex("email","email", {unique:true});
```
createIndex方法接受三個參數,第一個是索引名稱,第二個是建立索引的屬性名,第三個是參數對象,用來設置索引特性。unique表示索引所在的屬性是否有唯一值,上面代碼表示name屬性不是唯一值,email屬性是唯一值。
### index方法
有了索引以后,就可以針對索引所在的屬性讀取數據。index方法用于從對象倉庫返回指定的索引。
```javascript
var t = db.transaction(["people"],"readonly");
var store = t.objectStore("people");
var index = store.index("name");
var request = index.get(name);
```
上面代碼打開對象倉庫以后,先用index方法指定索引在name屬性上面,然后用get方法讀取某個name屬性所在的數據。如果沒有指定索引的那一行代碼,get方法只能按照鍵名讀取數據,而不能按照name屬性讀取數據。需要注意的是,這時get方法有可能取回多個數據對象,因為name屬性沒有唯一值。
另外,get是異步方法,讀取成功以后,只能在success事件的回調函數中處理數據。
## IDBKeyRange對象
索引的有用之處,還在于可以指定讀取數據的范圍。這需要用到瀏覽器原生的IDBKeyRange對象。
IDBKeyRange對象的作用是生成一個表示范圍的Range對象。生成方法有四種:
- **lowerBound方法**:指定范圍的下限。
- **upperBound方法**:指定范圍的上限。
- **bound方法**:指定范圍的上下限。
- **only方法**:指定范圍中只有一個值。
下面是一些代碼實例:
```javascript
// All keys ≤ x
var r1 = IDBKeyRange.upperBound(x);
// All keys < x
var r2 = IDBKeyRange.upperBound(x, true);
// All keys ≥ y
var r3 = IDBKeyRange.lowerBound(y);
// All keys > y
var r4 = IDBKeyRange.lowerBound(y, true);
// All keys ≥ x && ≤ y
var r5 = IDBKeyRange.bound(x, y);
// All keys > x &&< y
var r6 = IDBKeyRange.bound(x, y, true, true);
// All keys > x && ≤ y
var r7 = IDBKeyRange.bound(x, y, true, false);
// All keys ≥ x &&< y
var r8 = IDBKeyRange.bound(x, y, false, true);
// The key = z
var r9 = IDBKeyRange.only(z);
```
前三個方法(lowerBound、upperBound和bound)默認包括端點值,可以傳入一個布爾值,修改這個屬性。
生成Range對象以后,將它作為參數輸入openCursor方法,就可以在所設定的范圍內讀取數據。
```javascript
var t = db.transaction(["people"],"readonly");
var store = t.objectStore("people");
var index = store.index("name");
var range = IDBKeyRange.bound('B', 'D');
index.openCursor(range).onsuccess = function(e) {
var cursor = e.target.result;
if(cursor) {
console.log(cursor.key + ":");
for(var field in cursor.value) {
console.log(cursor.value[field]);
}
cursor.continue();
}
}
```
<h2 id="6.11">Web Notifications API</h2>
## 概述
Notification API是瀏覽器的通知接口,用于在用戶的桌面(而不是網頁上)顯示通知信息,桌面電腦和手機都適用,比如通知用戶收到了一封Email。具體的實現形式由瀏覽器自行部署,對于手機來說,一般顯示在頂部的通知欄。
如果網頁代碼調用這個API,瀏覽器會詢問用戶是否接受。只有在用戶同意的情況下,通知信息才會顯示。
下面的代碼用于檢查瀏覽器是否支持這個API。
```javascript
if (window.Notification) {
// 支持
} else {
// 不支持
}
```
目前,Chrome和Firefox在桌面端部署了這個API,Firefox和Blackberry在手機端部署了這個API。
```javascript
if(window.Notification && Notification.permission !== "denied") {
Notification.requestPermission(function(status) {
var n = new Notification('通知標題', { body: '這里是通知內容!' });
});
}
```
上面代碼檢查當前瀏覽器是否支持Notification對象,并且當前用戶準許使用該對象,然后調用Notification.requestPermission方法,向用戶彈出一條通知。
## Notification對象的屬性和方法
### Notification.permission
Notification.permission屬性,用于讀取用戶給予的權限,它是一個只讀屬性,它有三種狀態。
- default:用戶還沒有做出任何許可,因此不會彈出通知。
- granted:用戶明確同意接收通知。
- denied:用戶明確拒絕接收通知。
### Notification.requestPermission()
Notification.requestPermission方法用于讓用戶做出選擇,到底是否接收通知。它的參數是一個回調函數,該函數可以接收用戶授權狀態作為參數。
```javascript
Notification.requestPermission(function (status) {
if (status === "granted") {
var n = new Notification("Hi!");
} else {
alert("Hi!");
}
});
```
上面代碼表示,如果用戶拒絕接收通知,可以用alert方法代替。
## Notification實例對象
### Notification構造函數
Notification對象作為構造函數使用時,用來生成一條通知。
```javascript
var notification = new Notification(title, options);
```
Notification構造函數的title屬性是必須的,用來指定通知的標題,格式為字符串。options屬性是可選的,格式為一個對象,用來設定各種設置。該對象的屬性如下:
- dir:文字方向,可能的值為auto、ltr(從左到右)和rtl(從右到左),一般是繼承瀏覽器的設置。
- lang:使用的語種,比如en-US、zh-CN。
- body:通知內容,格式為字符串,用來進一步說明通知的目的。。
- tag:通知的ID,格式為字符串。一組相同tag的通知,不會同時顯示,只會在用戶關閉前一個通知后,在原位置顯示。
- icon:圖表的URL,用來顯示在通知上。
上面這些屬性,都是可讀寫的。
下面是一個生成Notification實例對象的例子。
```javascript
var notification = new Notification('收到新郵件', {
body: '您總共有3封未讀郵件。'
});
notification.title // "收到新郵件"
notification.body // "您總共有3封未讀郵件。"
```
### 實例對象的事件
Notification實例會觸發以下事件。
- show:通知顯示給用戶時觸發。
- click:用戶點擊通知時觸發。
- close:用戶關閉通知時觸發。
- error:通知出錯時觸發(大多數發生在通知無法正確顯示時)。
這些事件有對應的onshow、onclick、onclose、onerror方法,用來指定相應的回調函數。addEventListener方法也可以用來為這些事件指定回調函數。
```javascript
notification.onshow = function() {
console.log('Notification shown');
};
```
### close方法
Notification實例的close方法用于關閉通知。
```javascript
var n = new Notification("Hi!");
// 手動關閉
n.close();
// 自動關閉
n.onshow = function () {
setTimeout(n.close.bind(n), 5000);
}
```
上面代碼說明,并不能從通知的close事件,判斷它是否為用戶手動關閉。
<h2 id="6.12">Performance API</h2>
Performance API用于精確度量、控制、增強瀏覽器的性能表現。這個API為測量網站性能,提供以前沒有辦法做到的精度。
比如,為了得到腳本運行的準確耗時,需要一個高精度時間戳。傳統的做法是使用Date對象的getTime方法。
```javascript
var start = new Date().getTime();
// do something here
var now = new Date().getTime();
var latency = now - start;
console.log("任務運行時間:" + latency);
```
上面這種做法有兩個不足之處。首先,getTime方法(以及Date對象的其他方法)都只能精確到毫秒級別(一秒的千分之一),想要得到更小的時間差別就無能為力了;其次,這種寫法只能獲取代碼運行過程中的時間進度,無法知道一些后臺事件的時間進度,比如瀏覽器用了多少時間從服務器加載網頁。
為了解決這兩個不足之處,ECMAScript 5引入“高精度時間戳”這個API,部署在performance對象上。它的精度可以達到1毫秒的千分之一(1秒的百萬分之一),這對于衡量的程序的細微差別,提高程序運行速度很有好處,而且還可以獲取后臺事件的時間進度。
目前,所有主要瀏覽器都已經支持performance對象,包括Chrome 20+、Firefox 15+、IE 10+、Opera 15+。
## performance.timing對象
performance對象的timing屬性指向一個對象,它包含了各種與瀏覽器性能有關的時間數據,提供瀏覽器處理網頁各個階段的耗時。比如,performance.timing.navigationStart就是瀏覽器處理當前網頁的啟動時間。
```javascript
Date.now() - performance.timing.navigationStart
// 13260687
```
上面代碼表示距離瀏覽器開始處理當前網頁,已經過了13260687毫秒。
下面是另一個例子。
```javascript
var t = performance.timing;
var pageloadtime = t.loadEventStart - t.navigationStart;
var dns = t.domainLookupEnd - t.domainLookupStart;
var tcp = t.connectEnd - t.connectStart;
var ttfb = t.responseStart - t.navigationStart;
```
上面代碼依次得到頁面加載的耗時、域名解析的耗時、TCP連接的耗時、讀取頁面第一個字節之前的耗時。
performance.timing對象包含以下屬性(全部為只讀):
- **navigationStart**:當前瀏覽器窗口的前一個網頁關閉,發生unload事件時的Unix毫秒時間戳。如果沒有前一個網頁,則等于fetchStart屬性。
- **unloadEventStart**:如果前一個網頁與當前網頁屬于同一個域名,則返回前一個網頁的unload事件發生時的Unix毫秒時間戳。如果沒有前一個網頁,或者之前的網頁跳轉不是在同一個域名內,則返回值為0。
- **unloadEventEnd**:如果前一個網頁與當前網頁屬于同一個域名,則返回前一個網頁unload事件的回調函數結束時的Unix毫秒時間戳。如果沒有前一個網頁,或者之前的網頁跳轉不是在同一個域名內,則返回值為0。
- **redirectStart**:返回第一個HTTP跳轉開始時的Unix毫秒時間戳。如果沒有跳轉,或者不是同一個域名內部的跳轉,則返回值為0。
- **redirectEnd**:返回最后一個HTTP跳轉結束時(即跳轉回應的最后一個字節接受完成時)的Unix毫秒時間戳。如果沒有跳轉,或者不是同一個域名內部的跳轉,則返回值為0。
- **fetchStart**:返回瀏覽器準備使用HTTP請求讀取文檔時的Unix毫秒時間戳。該事件在網頁查詢本地緩存之前發生。
- **domainLookupStart**:返回域名查詢開始時的Unix毫秒時間戳。如果使用持久連接,或者信息是從本地緩存獲取的,則返回值等同于fetchStart屬性的值。
- **domainLookupEnd**:返回域名查詢結束時的Unix毫秒時間戳。如果使用持久連接,或者信息是從本地緩存獲取的,則返回值等同于fetchStart屬性的值。
- **connectStart**:返回HTTP請求開始向服務器發送時的Unix毫秒時間戳。如果使用持久連接(persistent connection),則返回值等同于fetchStart屬性的值。
- **connectEnd**:返回瀏覽器與服務器之間的連接建立時的Unix毫秒時間戳。如果建立的是持久連接,則返回值等同于fetchStart屬性的值。連接建立指的是所有握手和認證過程全部結束。
- **secureConnectionStart**:返回瀏覽器與服務器開始安全鏈接的握手時的Unix毫秒時間戳。如果當前網頁不要求安全連接,則返回0。
- **requestStart**:返回瀏覽器向服務器發出HTTP請求時(或開始讀取本地緩存時)的Unix毫秒時間戳。
- **responseStart**:返回瀏覽器從服務器收到(或從本地緩存讀取)第一個字節時的Unix毫秒時間戳。
- **responseEnd**:返回瀏覽器從服務器收到(或從本地緩存讀取)最后一個字節時(如果在此之前HTTP連接已經關閉,則返回關閉時)的Unix毫秒時間戳。
- **domLoading**:返回當前網頁DOM結構開始解析時(即Document.readyState屬性變為“loading”、相應的readystatechange事件觸發時)的Unix毫秒時間戳。
- **domInteractive**:返回當前網頁DOM結構結束解析、開始加載內嵌資源時(即Document.readyState屬性變為“interactive”、相應的readystatechange事件觸發時)的Unix毫秒時間戳。
- **domContentLoadedEventStart**:返回當前網頁DOMContentLoaded事件發生時(即DOM結構解析完畢、所有腳本開始運行時)的Unix毫秒時間戳。
- **domContentLoadedEventEnd**:返回當前網頁所有需要執行的腳本執行完成時的Unix毫秒時間戳。
- **domComplete**:返回當前網頁DOM結構生成時(即Document.readyState屬性變為“complete”,以及相應的readystatechange事件發生時)的Unix毫秒時間戳。
- **loadEventStart**:返回當前網頁load事件的回調函數開始時的Unix毫秒時間戳。如果該事件還沒有發生,返回0。
- **loadEventEnd**:返回當前網頁load事件的回調函數運行結束時的Unix毫秒時間戳。如果該事件還沒有發生,返回0。
根據上面這些屬性,可以計算出網頁加載各個階段的耗時。比如,網頁加載整個過程的耗時的計算方法如下:
```javascript
var t = performance.timing;
var pageLoadTime = t.loadEventEnd - t.navigationStart;
```
## performance.now()
performance.now方法返回當前網頁自從performance.timing.navigationStart到當前時間之間的微秒數(毫秒的千分之一)。也就是說,它的精度可以達到100萬分之一秒。
```javascript
performance.now()
// 23493457.476999998
Date.now() - (performance.timing.navigationStart + performance.now())
// -0.64306640625
```
上面代碼表示,performance.timing.navigationStart加上performance.now(),近似等于Date.now(),也就是說,Date.now()可以替代performance.now()。但是,前者返回的是毫秒,后者返回的是微秒,所以后者的精度比前者高1000倍。
通過兩次調用performance.now方法,可以得到間隔的準確時間,用來衡量某種操作的耗時。
```javascript
var start = performance.now();
doTasks();
var end = performance.now();
console.log('耗時:' + (end - start) + '微秒。');
```
## performance.mark()
mark方法用于為相應的視點做標記。
```javascript
window.performance.mark('mark_fully_loaded');
```
clearMarks方法用于清除標記,如果不加參數,就表示清除所有標記。
```javascript
window.peformance.clearMarks('mark_fully_loaded');
window.performance.clearMarks();
```
## performance.getEntries()
瀏覽器獲取網頁時,會對網頁中每一個對象(腳本文件、樣式表、圖片文件等等)發出一個HTTP請求。performance.getEntries方法以數組形式,返回這些請求的時間統計信息,有多少個請求,返回數組就會有多少個成員。
由于該方法與瀏覽器處理網頁的過程相關,所以只能在瀏覽器中使用。
```javascript
window.performance.getEntries()[0]
// PerformanceResourceTiming {
// responseEnd: 4121.6200000017125,
// responseStart: 4120.0690000005125,
// requestStart: 3315.355000002455,
// ...
// }
```
上面代碼返回第一個HTTP請求(即網頁的HTML源碼)的時間統計信息。該信息以一個高精度時間戳的對象形式返回,每個屬性的單位是微秒(microsecond),即百萬分之一秒。
## performance.navigation對象
除了時間信息,performance還可以提供一些用戶行為信息,主要都存放在performance.navigation對象上面。
它有兩個屬性:
**(1)performance.navigation.type**
該屬性返回一個整數值,表示網頁的加載來源,可能有以下4種情況:
- **0**:網頁通過點擊鏈接、地址欄輸入、表單提交、腳本操作等方式加載,相當于常數performance.navigation.TYPE_NAVIGATENEXT。
- **1**:網頁通過“重新加載”按鈕或者location.reload()方法加載,相當于常數performance.navigation.TYPE_RELOAD。
- **2**:網頁通過“前進”或“后退”按鈕加載,相當于常數performance.navigation.TYPE_BACK_FORWARD。
- **255**:任何其他來源的加載,相當于常數performance.navigation.TYPE_UNDEFINED。
**(2)performance.navigation.redirectCount**
該屬性表示當前網頁經過了多少次重定向跳轉。
<h2 id="6.13">移動設備API</h2>
為了更好地為移動設備服務,HTML 5推出了一系列針對移動設備的API。
## Viewport
Viewport指的是網頁的顯示區域,也就是不借助滾動條的情況下,用戶可以看到的部分網頁大小,中文譯為“視口”。正常情況下,viewport和瀏覽器的顯示窗口是一樣大小的。但是,在移動設備上,兩者可能不是一樣大小。
比如,手機瀏覽器的窗口寬度可能是640像素,這時viewport寬度就是640像素,但是網頁寬度有950像素,正常情況下,瀏覽器會提供橫向滾動條,讓用戶查看窗口容納不下的310個像素。另一種方法則是,將viewport設成950像素,也就是說,瀏覽器的顯示寬度還是640像素,但是網頁的顯示區域達到950像素,整個網頁縮小了,在瀏覽器中可以看清楚全貌。這樣一來,手機瀏覽器就可以看到網頁在桌面瀏覽器上的顯示效果。
viewport縮放規則,需要在HTML網頁的head部分指定。
```html
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
</head>
```
上面代碼指定,viewport的縮放規則是,縮放到當前設備的屏幕寬度(device-width),初始縮放比例(initial-scale)為1倍,禁止用戶縮放(user-scalable)。
viewport 全部屬性如下。
- width: viewport寬度
- height: viewport高度
- initial-scale: 初始縮放比例
- maximum-scale: 最大縮放比例
- minimum-scale: 最小縮放比例
- user-scalable: 是否允許用戶縮放
其他的例子如下。
```html
<meta name = "viewport" content = "width = 320,
initial-scale = 2.3, user-scalable = no">
```
## Geolocation API
Geolocation接口用于獲取用戶的地理位置。它使用的方法基于GPS或者其他機制(比如IP地址、Wifi熱點、手機基站等)。
下面的方法,可以檢查瀏覽器是否支持這個接口。
```javascript
if(navigator.geolocation) {
// 支持
} else {
// 不支持
}
```
這個API的支持情況非常好,所有瀏覽器都支持(包括IE 9+),所以上面的代碼不是很必要。
### getCurrentPosition方法
getCurrentPosition方法,用來獲取用戶的地理位置。使用它需要得到用戶的授權,瀏覽器會跳出一個對話框,詢問用戶是否許可當前頁面獲取他的地理位置。必須考慮兩種情況的回調函數:一種是同意授權,另一種是拒絕授權。如果用戶拒絕授權,會拋出一個錯誤。
```javascript
navigator.geolocation.getCurrentPosition(geoSuccess,geoError);
```
上面代碼指定了處理當前地理位置的兩個回調函數。
**(1)同意授權**
如果用戶同意授權,就會調用geoSuccess。
```javascript
function geoSuccess(event) {
console.log(event.coords.latitude + ', ' + event.coords.longitude);
}
```
geoSuccess的參數是一個event對象。event有兩個屬性:timestamp和coords。timestamp屬性是一個時間戳,返回獲得位置信息的具體時間。coords屬性指向一個對象,包含了用戶的位置信息,主要是以下幾個值:
- **coords.latitude**:緯度
- **coords.longitude**:經度
- **coords.accuracy**:精度
- **coords.altitude**:海拔
- **coords.altitudeAccuracy**:海拔精度(單位:米)
- **coords.heading**:以360度表示的方向
- **coords.speed**:每秒的速度(單位:米)
大多數桌面瀏覽器不提供上面列表的后四個值。
**(2)拒絕授權**
如果用戶拒絕授權,就會調用getCurrentPosition方法指定的第二個回調函數geoError。
```javascript
function geoError(event) {
console.log("Error code " + event.code + ". " + event.message);
}
```
geoError的參數也是一個event對象。event.code屬性表示錯誤類型,有四個值:
- **0**:未知錯誤,瀏覽器沒有提示出錯的原因,相當于常量event.UNKNOWN_ERROR。
- **1**:用戶拒絕授權,相當于常量event.PERMISSION_DENIED。
- **2**:沒有得到位置,GPS或其他定位機制無法定位,相當于常量event.POSITION_UNAVAILABLE。
- **3**:超時,GPS沒有在指定時間內返回結果,相當于常量event.TIMEOUT。
**(3)設置定位行為**
getCurrentPosition方法還可以接受一個對象作為第三個參數,用來設置定位行為。
```javascript
var option = {
enableHighAccuracy : true,
timeout : Infinity,
maximumAge : 0
};
navigator.geolocation.getCurrentPosition(geoSuccess, geoError, option);
```
這個參數對象有三個成員:
- **enableHighAccuracy**:如果設為true,就要求客戶端提供更精確的位置信息,這會導致更長的定位時間和更大的耗電,默認設為false。
- **Timeout**:等待客戶端做出回應的最大毫秒數,默認值為Infinity(無限)。
- **maximumAge**:客戶端可以使用緩存數據的最大毫秒數。如果設為0,客戶端不讀取緩存;如果設為infinity,客戶端只讀取緩存。
### watchPosition方法和clearWatch方法
watchPosition方法可以用來監聽用戶位置的持續改變,使用方法與getCurrentPosition方法一樣。
```javascript
var watchID = navigator.geolocation.watchPosition(geoSuccess,geoError, option);
```
一旦用戶位置發生變化,就會調用回調函數geoSuccess。這個回調函數的事件對象,也包含timestamp和coords屬性。
watchPosition和getCurrentPosition方法的不同之處在于,前者返回一個表示符,后者什么都不返回。watchPosition方法返回的標識符,用于供clearWatch方法取消監聽。
```javascript
navigator.geolocation.clearWatch(watchID);
```
## Vibration API
Vibration接口用于在瀏覽器中發出命令,使得設備振動。顯然,這個API主要針對手機,適用場合是向用戶發出提示或警告,游戲中尤其會大量使用。由于振動操作很耗電,在低電量時最好取消該操作。
使用下面的代碼檢查該接口是否可用。目前,只有Chrome和Firefox的Android平臺最新版本支持它。
```javascript
navigator.vibrate = navigator.vibrate
|| navigator.webkitVibrate
|| navigator.mozVibrate
|| navigator.msVibrate;
if (navigator.vibrate) {
// 支持
}
```
vibrate方法可以使得設備振動,它的參數就是振動持續的毫秒數。
```javascript
navigator.vibrate(1000);
```
上面的代碼使得設備振動1秒鐘。
vibrate方法還可以接受一個數組作為參數,表示振動的模式。偶數位置的數組成員表示振動的毫秒數,奇數位置的數組成員表示等待的毫秒數。
```javascript
navigator.vibrate([500, 300, 100]);
```
上面代碼表示,設備先振動500毫秒,然后等待300毫秒,再接著振動100毫秒。
vibrate是一個非阻塞式的操作,即手機振動的同時,JavaScript代碼繼續向下運行。要停止振動,只有將0毫秒或者一個空數組傳入vibrate方法。
```javascript
navigator.vibrate(0);
navigator.vibrate([]);
```
如果要讓振動一直持續,可以使用setInterval不斷調用vibrate。
```javascript
var vibrateInterval;
function startVibrate(duration) {
navigator.vibrate(duration);
}
function stopVibrate() {
if(vibrateInterval) clearInterval(vibrateInterval);
navigator.vibrate(0);
}
function startPeristentVibrate(duration, interval) {
vibrateInterval = setInterval(function() {
startVibrate(duration);
}, interval);
}
```
## Luminosity API
Luminosity API用于屏幕亮度調節,當移動設備的亮度傳感器感知外部亮度發生顯著變化時,會觸發devicelight事件。目前,只有Firefox部署了這個API。
```javascript
window.addEventListener('devicelight', function(event) {
console.log(event.value + 'lux');
});
```
上面代碼表示,devicelight事件的回調函數,接受一個事件對象作為參數。該對象的value屬性就是亮度的流明值。
這個API的一種應用是,如果亮度變強,網頁可以顯示黑底白字,如果亮度變弱,網頁可以顯示白底黑字。
```javascript
window.addEventListener('devicelight', function(e) {
var lux = e.value;
if(lux < 50) {
document.body.className = 'dim';
}
if(lux >= 50 && lux <= 1000) {
document.body.className = 'normal';
}
if(lux > 1000) {
document.body.className = 'bright';
}
});
```
CSS下一個版本的Media Query可以單獨設置亮度,一旦瀏覽器支持,就可以用來取代Luminosity API。
```css
@media (light-level: dim) {
/* 暗光環境 */
}
@media (light-level: normal) {
/* 正常光環境 */
}
@media (light-level: washed) {
/* 明亮環境 */
}
```
## Orientation API
Orientation API用于檢測手機的擺放方向(豎放或橫放)。
使用下面的代碼檢測瀏覽器是否支持該API。
```javascript
if (window.DeviceOrientationEvent) {
// 支持
} else {
// 不支持
}
```
一旦設備的方向發生變化,會觸發deviceorientation事件,可以對該事件指定回調函數。
```javascript
window.addEventListener("deviceorientation", callback);
```
回調函數接受一個event對象作為參數。
```javascript
function callback(event){
console.log(event.alpha);
console.log(event.beta);
console.log(event.gamma);
}
```
上面代碼中,event事件對象有alpha、beta和gamma三個屬性,它們分別對應手機擺放的三維傾角變化。要理解它們,就要理解手機的方向模型。當手機水平擺放時,使用三個軸標示它的空間位置:x軸代表橫軸、y軸代表豎軸、z軸代表垂直軸。event對象的三個屬性就對應這三根軸的旋轉角度。
- alpha:表示圍繞z軸的旋轉,從0到360度。當設備水平擺放時,頂部指向地球的北極,alpha此時為0。
- beta:表示圍繞x軸的旋轉,從-180度到180度。當設備水平擺放時,beta此時為0。
- gramma:表示圍繞y軸的選擇,從-90到90度。當設備水平擺放時,gramma此時為0。