<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                <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(輸入/輸出)操作,那么線程的運行大概是下面的樣子。 ![synchronous mode](http://image.beekka.com/blog/201310/2013102002.png) 上圖的綠色部分是程序的運行時間,紅色部分是等待時間。可以看到,由于I/O操作很慢,所以這個線程的大部分運行時間都在空等I/O操作的返回結果。這種運行方式稱為"同步模式"(synchronous I/O)。 如果采用多線程,同時運行多個任務,那很可能就是下面這樣。 ![synchronous mode](http://image.beekka.com/blog/201310/2013102003.png) 上圖表明,多線程不僅占用多倍的系統資源,也閑置多倍的資源,這顯然不合理。 ![asynchronous mode](http://image.beekka.com/blog/201310/2013102004.png) 上圖主線程的綠色部分,還是表示運行時間,而橙色部分表示空閑時間。每當遇到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))。很多開發者都知道這一點,但了解得不全面。 本節詳細介紹”同源政策“的各個方面,以及如何規避它。 ![](http://www.ruanyifeng.com/blogimg/asset/2016/bg2016040801.jpg) ## 概述 ### 含義 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.&#37;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。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看