第2章 在網頁中使用JavaScript
第2章 在網頁中使用JavaScript
與能夠獨立執行的C/C++等傳統語言不同,執行JavaScript程式碼需要HTML網頁環境。在當初開發JavaScript時,Netscape把JavaScript定位為嵌入式Web指令碼語言,這種做法被保留了下來,並被正式納入HTML規範當中。本章將詳細介紹如何在網頁中編寫JavaScript程式碼並執行,同時介紹如何在瀏覽器中進行JavaScript程式碼除錯和錯誤處理。
【學習重點】
▲ 靈活使用<script>標籤
▲ 瞭解JavaScript指令碼存放位置
▲ 會用<script>標籤
▲ 瞭解JavaScript執行順序
▲ 瞭解JavaScript錯誤處理機制(選學)
▲ 瞭解JavaScript程式碼除錯方法(選學)
2.1 使用<script>標籤
通常情況下,在Web頁面中使用JavaScript有兩種方法,一種是在頁面中直接嵌入JavaScript程式碼,另一種是連結外部JavaScript檔案。
在HTML頁面中嵌入JavaScript指令碼需要使用<script>標籤,使用者可以在<script>標籤中直接編寫JavaScript程式碼,或者單獨編寫JavaScript檔案,然後通過<script>標籤匯入。
2.1.1 編寫第一個JavaScript程式
下面通過示例演示如何使用<script>標籤的兩種方式(直接在頁面中嵌入JavaScript程式碼和連結外部JavaScript檔案)。
【示例1】直接在頁面中嵌入JavaScript程式碼。
第1步,新建HTML文件,儲存為test.html。然後在標籤內插入一個<script>標籤。
第2步,為<script>標籤指定type屬性值為"text/JavaScript"。現在的瀏覽器預設<script>標籤的型別為JavaScript指令碼,因此省略type屬性,依然能夠被正確執行,但是考慮到程式碼的相容性,建議定義該屬性。
第3步,直接在<script>標籤內部輸入JavaScript程式碼:
上面JavaScript指令碼先定義了一個hi()函式,該函式被呼叫後會在頁面中顯示字元"Hello,World!"。document表示DOM網頁文件物件,document.write()表示呼叫Document物件的write()方法,在當前網頁原始碼中寫入HTML字串"
Hello,World!
"。呼叫hi()函式,瀏覽器將在頁面中顯示一級標題字元"Hello,World!"。
第4步,儲存網頁文件,在瀏覽器中預覽,顯示效果如圖2-1所示。
圖2-1 第一個JavaScript程式
提示:包含在<script>標籤內的JavaScript程式碼被瀏覽器從上至下依次解釋。
當使用<script>標籤嵌入JavaScript程式碼時,不要在程式碼中的任何地方輸出" script>"字串。例如,瀏覽器在載入下面所示的程式碼時就會產生一個錯誤:
錯誤原因:當瀏覽器解析到字串" script>"時,會結束JavaScript程式碼段的執行。
解決方法:
使用轉義字元把字串" script>“分成兩部分來寫就不會造成瀏覽器的誤解。
【示例2】連結外部JavaScript檔案。
第1步,新建文字檔案,儲存為test.js。注意,副檔名為.js,它表示該文字檔案是JavaScript型別的檔案。
提示:使用<script>標籤包含外部JavaScript檔案時,預設檔案型別為JavaScript,因此.js副檔名不是必需的,瀏覽器不會檢查包含JavaScript的檔案的副檔名。在高階開發中,使用JSP、PHP或其他伺服器端語言動態生成JavaScript程式碼時可以使用任意副檔名,如果不使用.js副檔名,使用者應確保伺服器能返回正確的MIME型別。
第2步,開啟test.js文字檔案,在其中編寫下面程式碼,定義簡單的輸出函式。
在上面程式碼中,alert()表示Window物件的方法,呼叫該方法將彈出一個提示對話方塊,顯示引數字串"Hello,World!”。
第3步,儲存JavaScript檔案,注意與網頁檔案的位置關係。這裡儲存JavaScript檔案位置與呼叫該檔案的網頁檔案位於相同目錄下。
第4步,新建HTML文件,儲存為test1.html。然後在標籤內插入一個<script>標籤。定義src屬性,設定屬性值為指向外部JavaScript檔案的URL字串。程式碼如下:
<script type=“text/JavaScript” src=“test.js”> script>
第5步,在上面<script>標籤下一行繼續插入一個<script>標籤,直接在<script>標籤內部輸入JavaScript程式碼,呼叫外部JavaScript檔案中的hi()函式。
<meta charset="utf-8"> <title>test</title> <<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>" src="test.js"><!--<span id="sensitive" class="keyword-match"-->script> <<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>"> hi(); //呼叫外部Java<span id="sensitive" class="keyword-match">Script</span>檔案的函式 <!--<span id="sensitive" class="keyword-match"-->script>
第6步,儲存網頁文件,在瀏覽器中預覽,顯示效果如圖2-2所示。
圖2-2 呼叫外部函式彈出提示對話方塊
提示:定義src屬性的<script>標籤不應再包含JavaScript程式碼。如果嵌入了程式碼,則只會下載並執行外部JavaScript檔案,嵌入程式碼會被忽略。
<script>標籤的src屬性可以包含來自外部域的JavaScript檔案。例如:
<script type=“text/JavaScript” src=“http://www.sothersite.com/test.js”> script>
這些位於外部域中的程式碼也會被載入和解析。因此在訪問自己不能控制的伺服器上的JavaScript檔案時要小心,防止惡意程式碼,或者防止惡意人員隨時可能替換JavaScript檔案中的程式碼。
【擴充】HTML為<script>定義了6個屬性,簡單說明如下。
☑ async:可選。表示應該立即下載指令碼,但不應妨礙頁面中其他操作,如下載其他資源或等待載入其他指令碼。該功能只對外部JavaScript檔案有效。
☑ charset:可選。表示通過src屬性指定的程式碼的字符集。由於大多數瀏覽器會忽略它的值,因此很少使用。
☑ defer:可選。表示指令碼可以延遲到文件完全被解析和顯示之後再執行。該屬性只對外部JavaScript檔案有效。IE 7及更早版本對嵌入的JavaScript程式碼也支援這個屬性。
☑ language:已廢棄。原來用於表示編寫程式碼使用的指令碼語言,如JavaScript、JavaScript l.2或VBScript。大多數瀏覽器會忽略這個屬性,不建議再使用。
☑ src:可選。表示包含要執行程式碼的外部檔案。
☑ type:可選。可以看成是language的替代屬性,表示編寫程式碼使用的指令碼語言的內容型別(也稱為MIME型別)。雖然text/JavaScript和text/ecmascript已經不被推薦使用,但人們一直習慣使用text/JavaScript。伺服器在傳送JavaScript檔案時使用的MIME型別通常是application/x-JavaScript,但在type中設定這個值可能導致指令碼被忽略。另外,在非IE瀏覽器中還可以使用application/JavaScript和application/ecmascript。考慮到約定俗成和瀏覽器最大限度的相容性,目前在客戶端,type屬性值一般使用text/JavaScript。不過,這個屬性並不是必需的,如果沒有指定這個屬性,則其預設值仍為text/JavaScript。
2.1.2 指令碼位置
所有<script>標籤都會按照它們在HTML中出現的先後順序依次被解析。在不使用defer和async屬性的情況下,只有在解析完前面<script>標籤中的程式碼之後,才會開始解析後面<script>標籤中的程式碼。
【示例1】在預設情況下,所有<script>標籤都應該放在頁面頭部的標籤中。
<meta charset="utf-8"> <title>test</title> <<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>" src="test.js"><!--<span id="sensitive" class="keyword-match"-->script> <<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>"> hi(); <!--<span id="sensitive" class="keyword-match"-->script> <!-- 網頁內容 -->
這樣就可以把所有外部檔案(包括CSS檔案和JavaScript檔案)的引用都放在相同的地方。但是,在文件的標籤中包含所有JavaScript檔案,意味著必須等到全部JavaScript程式碼都被下載、解析和執行完成以後,才能開始呈現頁面的內容。如果頁面需要很多JavaScript程式碼,這樣無疑會導致瀏覽器在呈現頁面時出現明顯的延遲,而延遲期間的瀏覽器視窗中將是一片空白。
【示例2】為了避免延遲問題,現代Web應用程式一般都把全部JavaScript引用放在標籤中頁面的內容後面。
<meta charset="utf-8"> <!-- 網頁內容 --> <<title>test</title> <<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>" src="test.js"><!--<span id="sensitive" class="keyword-match"-->script> <<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>"> hi(); <!--<span id="sensitive" class="keyword-match"-->script> /body>
這樣,在解析包含的JavaScript程式碼之前,頁面的內容將完全呈現在瀏覽器中,同時會感到開啟頁面的速度加快了。
2.1.3 延遲執行指令碼
為了避免指令碼在執行時影響頁面的構造,HTML為<script>標籤定義了defer屬性。defer屬效能夠迫使指令碼被延遲到整個頁面都解析完畢後再執行。因此,在<script>標籤中設定defer屬性,相當於告訴瀏覽器雖然可以立即下載JavaScript程式碼,但延遲執行。
【示例】在下面的示例中,雖然把<script>標籤放在文件的標籤中,但其中包含的指令碼將延遲到瀏覽器遇到標籤後再執行。
<<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>" defer src="test1.js"><!--<span id="sensitive" class="keyword-match"-->script> <<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>" defer src="test2.js"><!--<span id="sensitive" class="keyword-match"-->script> <!-- 網頁內容 -->
HTML5規範要求指令碼按照它們出現的先後順序執行,因此第一個延遲指令碼會先於第二個延遲指令碼執行,而這兩個指令碼會先於DOMContentLoaded事件執行。在實際應用中,延遲指令碼並不一定會按照順序執行,也不一定會在DOMContentLoaded事件觸發前執行,因此最好只包含一個延遲指令碼。
提示:defer屬性只適用於外部指令碼檔案。這一點在HTML5中已經明確規定,因此支援HTML5的實現會忽略給嵌入指令碼設定的defer屬性。IE 4~IE 7還支援對嵌入指令碼的defer屬性,但IE 8及之後版本則完全支援HTML5規定的行為。
IE 4、Firefox 3.5、Safari 5和Chrome是最早支援defer屬性的瀏覽器。其他瀏覽器會忽略這個屬性。因此,把延遲指令碼放在頁面底部仍然是最佳選擇。
注意:在XHTML型別的文件中,defer屬性應該定義為defer=“defer”。
2.1.4 非同步響應指令碼
HTML5為<script>標籤定義了async屬性。這個屬性與defer屬性類似,都用於改變外部指令碼的行為。同樣與defer類似,async只適用於外部指令碼檔案,並告訴瀏覽器立即下載檔案。但與defer不同的是,標記為async的指令碼並不保證按照指定它們的先後順序執行。
【示例】在下面的程式碼中,第二個指令碼檔案test2.js可能會在第一個指令碼檔案test1.js之前執行。因此,使用者要確保兩個檔案之間沒有邏輯順序的關聯,互不依賴是非常重要的。
<<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>" async src="test1.js"><!--<span id="sensitive" class="keyword-match"-->script> <<span id="sensitive" class="keyword-match">script</span> type="text/Java<span id="sensitive" class="keyword-match">Script</span>" async src="test2.js"><!--<span id="sensitive" class="keyword-match"-->script> <!-- 網頁內容 -->
指定async屬性的目的是不讓頁面等待兩個指令碼檔案下載完後再執行,從而非同步載入頁面其他內容。
提示:非同步響應的指令碼一定會在頁面的load事件前執行,但可能會在DOMContentLoaded事件觸發之前或之後執行。非同步指令碼不要在載入期間修改DOM。
支援非同步指令碼的瀏覽器包括Firefox 3.6+、Safari 5和Chrome。在XHTML文件中,要把async屬性設為async=“async”。
2.1.5 在XHTML中使用JavaScript指令碼
XHTML(EXtensible HyperText Markup Language)表示可擴充套件超文字標記語言,是將HTML作為XML的應用而重新定義的一個標準。編寫XHTML程式碼的規則要比編寫HTML嚴格得多,而且直接影響能否在嵌入JavaScript程式碼時使用<script/>標籤。
提示:將頁面的MIME型別指定為application/xhtml+xml.的情況下會觸發XHTML模式,當然並不是所有瀏覽器都支援這種方法。
【示例】下面的程式碼塊雖然在HTML中是有效的,但在XHTML中則是無效的。
在HTML中,有特殊的規則用以確定<script>標籤中的哪些內容可以被解析,但這些特殊的規則在XHTML中不適用。例如,上面程式碼中比較語句a
避免在XHTML中出現類似語法錯誤的方法如下。
方法一,使用相應的HTML轉碼(&1t;)替換程式碼中所有的小於號(<),替換後的程式碼如下:
雖然這樣可以讓程式碼在XHTML中正常執行,但卻導致程式碼不好理解了。
方法二,使用一個CData片段來包含JavaScript程式碼。在XHTML中,CData片段是文件中的一個特殊區域,這個區域中可以包含不需要解析的任意格式的文字內容。因此,在CData片段中就可以使用任意字元,且不會導致語法錯誤。引入CData片段後的JavaScript程式碼塊如下:
在相容XHTML的瀏覽器中,這個方法可以解決問題。但實際上,還有不少瀏覽器不相容XHTML,因而不支援CData片段。這時可以使用JavaScript註釋將CData標記註釋掉。
這樣在所有現代瀏覽器中都可以正常使用,它能通過XHTML驗證,而且能夠相容XHTML之前的瀏覽器。
2.1.6 相容不支援JavaScript的瀏覽器
最早引入<script>標籤時,與傳統HTML的解析規則存在衝突。由於要對這個標籤應用特殊的解析規則,因此在那些不支援JavaScript的瀏覽器中就會導致問題,即會把<script>標籤的內容直接輸出到頁面中,因而會破壞頁面的佈局和外觀。
Netscape與Mosaic協商並提出了一個解決方案,讓不支援<script>標籤的瀏覽器能夠隱藏嵌入的JavaScript程式碼。這個方案就是把JavaScript程式碼包含在一個HTML註釋中。
【示例】下面的程式碼給指令碼加上HTML註釋後,不支援JavaScript的瀏覽器就會忽略<script>標籤中的內容,而那些支援JavaScript的瀏覽器在遇到這種情況時,則必須進一步確認其中是否包含需要解析的JavaScript程式碼。
雖然這種註釋JavaScript程式碼的格式得到了所有瀏覽器的認可,也能被正確解釋,但由於所有瀏覽器都已經支援JavaScript,因此也就沒有必要再使用這種格式了,也不再推薦使用這種方法。
在XHTML模式下,因為指令碼包含在XML註釋中,所以指令碼會被忽略。
2.2 比較嵌入程式碼與連結指令碼
在HTML中嵌入JavaScript程式碼雖然沒有問題,但一般認為最好的做法還是儘可能使用外部檔案來包含JavaScript程式碼。不過,並不存在必須使用外部檔案的硬性規定,但支援使用外部檔案有如下優點。
☑ 可維護性:遍及不同HTML頁面的JavaScript會造成維護問題。但把所有JavaScript檔案都放在一個資料夾中,維護起來就輕鬆多了。而且開發人員因此也能夠在不觸及HTML標記的情況下,集中精力編輯JavaScript程式碼。
☑ 可快取:瀏覽器能夠根據具體的設定快取連結的所有外部JavaScript檔案。也就是說,如果有兩個頁面都使用同一個檔案,那麼這個檔案只需下載一次。因此,最終結果就是能夠加快頁面載入的速度。
☑ 適應未來:通過外部檔案來包含JavaScript無須使用前面提到的XHTML或註釋hack,HTML和XHTML包含外部檔案的語法是相同的。
2.3 使用<noscript>標籤</no
早期瀏覽器不支援JavaScript,為了確保頁面平穩相容,創造了一個<noscript>標籤,用以在不支援JavaScript的瀏覽器中顯示替代的內容。這個元素可以包含能夠出現在文件中的任何HTML標籤,但<script>標籤除外。包含在<noscript>標籤中的內容只有在下列情況下才會顯示出來。</no</no
☑ 瀏覽器不支援指令碼。
☑ 瀏覽器支援指令碼,但指令碼被禁用。
符合上述任何一個條件,瀏覽器都會顯示<noscript>中的內容。而在除此之外的其他情況下,瀏覽器不會呈現<noscript>中的內容。</no</no
【示例】請看下面這個簡單的例子。
這個頁面在指令碼無效的情況下會顯示一行文字:當前瀏覽器不支援JavaScript。而在啟用了指令碼,且支援JavaScript的瀏覽器中會顯示:當前瀏覽器支援JavaScript。
2.4 JavaScript執行順序
JavaScript解釋過程包括兩個階段:預處理(也稱編譯)和執行。在預編譯期,JavaScript直譯器將完成對JavaScript程式碼的預處理操作,把JavaScript程式碼轉換成位元組碼;在執行期,JavaScript直譯器把位元組碼生成二進位制機器碼,並按順序執行,完成程式設計的任務。
提示:預編譯包括詞法分析和語法分析。詞法分析主要對JavaScript指令碼進行逐一分析,檢查指令碼是否符合JavaScript規範,是否存在語法錯誤;語法分析主要是把從程式中收集的資訊儲存到資料結構中,如符號表和語法樹。
☑ 符號表:儲存程式中所有符號的一個表,包括所有的字串、直接量、變數名和函式名等。
☑ 語法樹:構建程式結構的一個樹形表示,並將使用這個樹形結構來生成中間程式碼。
2.4.1 正常執行順序
HTML文件在瀏覽器中的解析過程是:按文件流從上到下逐步解析頁面結構和資訊。JavaScript程式碼作為嵌入的指令碼應該也算作HTML文件的組成部分,所以JavaScript程式碼在裝載時的執行順序也是根據<script>標籤的出現順序來確定的。
【示例1】瀏覽下面文件頁面,會看到程式碼是從上到下逐步被解析的。
<script>
alert(“頂部指令碼”);
script><meta charset="utf-8"> <title>test</title> <<span id="sensitive" class="keyword-match">script</span>> alert("頭部指令碼"); <!--<span id="sensitive" class="keyword-match"-->script> <<span id="sensitive" class="keyword-match">script</span>> alert("頁面指令碼"); <!--<span id="sensitive" class="keyword-match"-->script> <<span id="sensitive" class="keyword-match">script</span>> alert("底部指令碼"); <!--<span id="sensitive" class="keyword-match"-->script>
對於匯入外部的JavaScript檔案指令碼,也將按照其<script>標籤在文件中出現的順序來執行,而且執行過程是文件裝載的一部分。不會因為是外部JavaScript檔案而延期執行。
【示例2】把上面文件中的頭部和主體區域的指令碼移到外部JavaScript檔案中,然後通過src屬性匯入進來。繼續預覽頁面文件,會看到相同的執行順序。
<script>
alert(“頂部指令碼”);
script><meta charset="utf-8"> <title>test</title> <<span id="sensitive" class="keyword-match">script</span> src="head.js"><!--<span id="sensitive" class="keyword-match"-->script> <<span id="sensitive" class="keyword-match">script</span> src="body.js"><!--<span id="sensitive" class="keyword-match"-->script> <<span id="sensitive" class="keyword-match">script</span>> alert("底部指令碼"); <!--<span id="sensitive" class="keyword-match"-->script>
2.4.2 預編譯提前
當JavaScript引擎解析指令碼時,它會在預編譯期對所有宣告的變數和函式預先進行處理。
【示例1】當JavaScript直譯器執行下面指令碼時不會報錯。
由於變數宣告是在預編譯期被處理的,在執行期間對於所有程式碼來說,都是可見的。但是,執行上面程式碼,提示的值是undefined,而不是1。因為變數初始化過程發生在執行期,而不是預編譯期。在執行期,JavaScript直譯器是按著程式碼先後順序進行解析的,如果在前面程式碼行中沒有為變數賦值,則JavaScript直譯器會使用預設值undefined。由於在第二行中為變數a賦值了,所以在第三行程式碼中會提示變數a的值為1,而不是undefined。
【示例2】下面的示例在函式宣告前呼叫函式也是合法的,並能夠被正確解析,所以返回值為1。
【示例3】如果按下面方式定義函式,則JavaScript直譯器會提示語法錯誤。
上面示例中定義的函式僅作為值賦值給變數f。在預編譯期,JavaScript直譯器只能夠為宣告變數f進行處理,而對於變數f的值,只能等到執行期時按順序進行賦值,自然就會出現語法錯誤,提示找不到物件f。
提示:宣告變數和函式可以在文件任意位置,但是良好的習慣應該是在所有JavaScript程式碼之前宣告全域性變數和函式,並對變數進行初始化賦值。在函式內部也是先宣告變數,後引用。
2.4.3 程式碼塊的執行順序
程式碼塊就是使用<script>標籤分隔的程式碼段。
【示例1】下面兩個<script>標籤分別代表兩個JavaScript程式碼塊。
JavaScript直譯器在執行指令碼時,是按塊來執行的。瀏覽器在解析HTML文件流時,如果遇到一個<script>標籤,則JavaScript直譯器會等到這個程式碼塊都載入完後,先對程式碼塊進行預編譯,然後再執行。執行完畢後,瀏覽器會繼續解析下面的HTML文件流,同時JavaScript直譯器也準備好處理下一個程式碼塊。
【示例2】如果在一個JavaScript塊中呼叫後面塊中宣告的變數或函式就會提示語法錯誤。例如,當JavaScript直譯器執行下面程式碼時就會提示語法錯誤,顯示變數a未定義,物件f找不到,如圖2-3所示。
圖2-3 錯誤的程式碼塊順序
提示:JavaScript是按塊執行的,但是不同塊都屬於同一個全域性作用域,塊之間的變數和函式是可以共享的。
2.4.4 事件響應順序
JavaScript響應操作是通過事件驅動的模式來實現的,由於事件發生的不確定性,所以JavaScript事件響應的順序也是不確定的。
【示例】針對2.4.3節第2個示例的錯誤響應,可以把訪問第2塊程式碼中的變數和函式的程式碼放在頁面初始化事件函式中,這樣就不會出現語法錯誤了。
onload事件只有在文件載入完畢才會響應。因此為了執行安全,一般都設計在頁面初始化完畢之後才允許JavaScript程式碼執行,這樣就可以避免因為程式碼載入延遲對JavaScript執行的影響。同時也避開了HTML文件流對於JavaScript執行的限制。
提示:除了頁面初始化事件外,使用者還可以通過各種互動事件來改變JavaScript程式碼的執行順序,如滑鼠事件、鍵盤事件,以及時鐘觸發器等方法。
2.4.5 動態輸出指令碼字串
使用document物件的write()方法輸出JavaScript指令碼時,這些動態輸出的指令碼的執行順序也不同。
【示例1】JavaScript指令碼輸出的程式碼字串會在輸出後馬上被執行。
document.write(’<script type=“text/JavaScript”>’);
document.write(‘f();’);
document.write(‘function f(){’);
document.write(’ alert(1);’);
document.write(’}’);
document.write(’</script>’);
執行程式碼,document.write()方法先把輸出的JavaScript字串寫入到標籤所在的文件位置,瀏覽器在解析完document.write()所在文件內容後,繼續解析document.write()輸出的內容,然後才按順序解析後面的HTML文件。
提示:使用document.write()方法輸出的JavaScript字串必須放在同時被輸出的<script>標籤中,否則JavaScript直譯器因為不能識別這些合法的JavaScript程式碼,而作為普通的字串顯示在頁面文件中。
【示例2】下面的程式碼就會把JavaScript程式碼顯示出來,而不是執行它。
document.write(‘f();’);
document.write(‘function f(){’);
document.write(’ alert(1);’);
document.write(’);’);
提示:使用document.write()方法輸出指令碼並執行存在一定的風險,因為不同JavaScript引擎對其執行順序不同,同時不同瀏覽器在解析時也會出現各種Bug。
2.5 瀏覽器與JavaScript
Web瀏覽器一般包括兩部分:Shell和核心。Shell是指瀏覽器的外殼,如選單、工具欄等,主要提供使用者介面操作、引數設定等,它是呼叫核心來實現各種功能的UI;核心是瀏覽器的核心,是基於標記語言顯示內容的程式或模組。
目前主流瀏覽器包括IE、FireFox、Opera、Safari、Chrome。
2.5.1 瀏覽器核心
瀏覽器核心可以分為兩部分:渲染引擎和JavaScript引擎。它們負責取得網頁內容(HTML、XML、影像等)、整理資訊(如加入CSS等),以及計算網頁的顯示方式,然後輸出顯示。JavaScript引擎負責解析JavaScript指令碼,執行JavaScript程式碼實現網頁的動態效果。
常見的瀏覽器核心包括4種:Trident、Gecko、Presto、Webkit。
Trident又稱MSHTML,是微軟開發的渲染引擎,包含JavaScript引擎JScript,JScript已經深入Windows系統,如Windows Media Play、Windows Explorer、Outlook Express等都使用它。
Gecko是開源的渲染引擎,包括JavaScript引擎SpiderMonkey(Rhino)。主要使用者為Firefox。
Webkit是蘋果公司基於KHTML開發的。它包括Webcore和JavaScriptCore(SquirrelFish、V8)兩個引擎。主要使用者有Safari和Chrome。
Presto是由Opera公司開發的,用於Opera的渲染引擎。Macromedia Dreamweaver (MX版本及以上)和Adobe Creative Suite 2也使用了Presto的核心。
主流瀏覽器所使用的核心分類如下。
☑ Trident核心:IE、MaxThon、TT、The World、360、搜狗瀏覽器等。
☑ Gecko核心:Netscape 6及以上版本、Firefox、MozillaSuite/SeaMonkey等。
☑ Presto核心:Opera 7及以上。
☑ Webkit核心:Safari和Chrome等。
2.5.2 瀏覽器錯誤報告
瀏覽器都具有某種JavaScript錯誤報告機制,但在預設情況下,都會隱藏此類資訊,在基於瀏覽器編寫JavaScript指令碼時,使用者應該啟用瀏覽器的JavaScript報告功能,以便及時收集錯誤資訊。
1.IE
在IE中可以通過設定讓錯誤對話方塊一發生錯誤就顯示出來。為此,要開啟“工具”選單中的“Internet選項”對話方塊,切換到“高階”選項卡,選中“顯示每個指令碼錯誤的通知”核取方塊,如圖2-4所示。單擊“確定”按鈕儲存設定。
圖2-4 設定IE選項
儲存了設定之後,就會變成一有錯誤發生隨即自動顯示出來。另外,如果啟用了指令碼除錯功能(預設是禁用的),那麼在發生錯誤時,不僅會顯示錯誤通知,而且還會看到另一個對話方塊,詢問是否除錯錯誤。要啟動指令碼除錯功能,也可以在“高階”選項卡中取消選中“禁用指令碼除錯”核取方塊。
2.Firefox
在預設情況下,Firefox在JavaScript發生錯誤時不會通過瀏覽器介面給出提示。但它會在後臺將錯誤記錄到錯誤控制檯中。單擊“工具”選單中的“錯誤控制檯”可以顯示錯誤控制檯,如圖2-5所示。錯誤控制檯中實際上還包含與JavaScript、CSS和HTML相關的警告和資訊,可以通過篩選找到錯誤。
圖2-5 Firefox錯誤控制檯
在發生JavaScript錯誤時,Firefox會將其記錄為一個錯誤,包括錯誤訊息、引發錯誤的URL和錯誤所在的行號等資訊。單擊檔名即可以只讀方式開啟發生錯誤的指令碼,發生錯誤的程式碼行會突出顯示。
目前,最流行的Firefox外掛Firebug已經成為開發人員必備的JavaScript糾錯工具。在有JavaScript錯誤發生時,Firebug圖示會顯示錯誤的數量,單擊可以開啟Firebug控制檯,其中顯示有錯誤訊息、錯誤所在的程式碼行(不包含上下文)、錯誤所在的URL以及行號,如圖2-6所示。
圖2-6 Firebug錯誤控制檯
在Firebug中單擊導致錯誤的程式碼行,將在一個新Firebug檢視中開啟整個指令碼,該程式碼行在其中突出顯示。
除了顯示錯誤之外,Firebug還有更多的用處。實際上,它還是針對Firefox的成熟的除錯環境,為除錯JavaScript、CSS、DOM和網路連線錯誤提供了諸多功能。
3.Safari
Windows和Mac OS平臺的Safari在預設情況下都會隱藏全部JavaScript錯誤。為了訪問到這些資訊,必須啟用“開發”選單,單擊“偏好設定”,然後在“高階”選項卡中選中“在選單欄中顯示‘開發’選單”命令。啟用此項設定之後,就會在Safari的選單欄中看到一個“開發”選單,如圖2-7所示。
圖2-7 啟動Safari開發選單
“開發”選單中提供了一些與除錯有關的選項,還有一些選項可以影響當前載入的頁面。單擊“顯示錯誤控制檯”選項,將會看到一組JavaScript及其他錯誤,控制檯中顯示著錯誤訊息、錯誤的URL及錯誤的行號,如圖2-8所示。單擊控制檯中的錯誤訊息,就可以開啟導致錯誤的原始碼。除了被輸出到控制檯之外,JavaScript錯誤不會影響Safari視窗的外觀。
圖2-8 顯示錯誤控制檯
4.Opera
Opera在預設情況下也會隱藏JavaScript錯誤,所有錯誤都會被記錄到錯誤控制檯中。要開啟錯誤控制檯,需要選擇“開發者工具”選單,單擊“Web檢查器”命令,開啟“Web檢查器”,然後選擇Console選項。與Firefox一樣,Opera的錯誤控制檯中也包含了除JavaScript錯誤之外的很多來源,如HTML、CSS、XML、XSLT等的錯誤和報告資訊。要分類檢視不同來源的訊息,可以使用左下角的下拉選擇框,如圖2-9所示。
圖2-9 錯誤控制檯
錯誤訊息中顯示著導致錯誤的URL和錯誤所在的執行緒。有時還會有棧跟蹤資訊。除了錯誤控制檯中顯示的資訊之外,沒有其他途徑可以獲得更多資訊。
5.Chrome
與Safari和Opera一樣,Chrome在預設情況下也會隱藏JavaScript錯誤。所有錯誤都將被記錄到JavaScript控制檯中。要檢視錯誤訊息,選擇“工具”選單中的“JavaScript控制檯”命令即可,如圖2-10所示。
圖2-10 JavaScript控制檯
開啟視窗中包含著有關頁面的資訊和JavaScript控制檯。控制檯中顯示著錯誤訊息、錯誤的URL和錯誤的行號。單擊JavaScript控制檯中的錯誤,就可以定位到導致錯誤的原始碼行。
2.6 JavaScript錯誤處理
JavaScript屬於動態語言,一直沒有固定的開發工具,難於除錯。當指令碼出錯時,瀏覽器通常會給出類似於object expected(缺少物件)這樣的訊息,沒有上下文資訊,讓人摸不著頭腦。ECMAScript第3版致力於解決這個問題,引入try-catch和throw語句以及一些錯誤型別,適當處理錯誤。同時,現在主流瀏覽器中也出現了一些JavaScript除錯程式和工具,當有了語言特性和工具支援之後,使用者就能夠輕鬆實現錯誤處理,並且能夠找到錯誤的根源。
2.6.1 使用try-catch
ECMA-262第3版引入了try-catch語句,作為JavaScript處理異常的一種標準方式。基本語法如下:
上面語法與Java中的try-catch語句是完全相同的。
【示例1】使用者應把所有可能會丟擲錯誤的程式碼都放在try語句塊中,而把那些用於錯誤處理的程式碼放在catch塊中。
如果try塊中的任何程式碼發生了錯誤,就會立即退出程式碼執行過程,然後執行catch塊。此時,catch塊會接收到一個包含錯誤資訊的物件。與在其他語言中不同的是,即使不使用這個錯誤物件,也要給它起個名字。
【示例2】錯誤物件中包含的實際資訊會因瀏覽器而異,但都有一個儲存著錯誤訊息的message屬性。ECMA-262還規定了一個儲存錯誤型別的name屬性,當前所有瀏覽器都支援這個屬性(Opera 9之前的版本不支援這個屬性)。因此,在發生錯誤時,就可以像下面這樣實事求是地顯示瀏覽器給出的訊息。
這個例子在向使用者顯示錯誤訊息時,使用了錯誤物件的message屬性。
提示:message屬性是唯一一個能夠保證所有瀏覽器都支援的屬性。除此之外,IE、Firefox、Safari、Chrome以及Opera都為事件物件新增了其他相關資訊。
例如,IE新增了與message屬性完全相同的description屬性,還新增了儲存著內部錯誤數量的number屬性;Firefox新增了fileName、lineNumber和stack(包含棧跟蹤資訊)屬性;Safari新增了line(表示行號)、sourceId(表示內部錯誤程式碼)和sourceURL屬性。當然,在跨瀏覽器程式設計時,建議只使用message屬性。
【擴充】當try-catch語句中發生錯誤時,瀏覽器會認為錯誤已經被處理了,因而不會報告錯誤。對於那些不要求使用者懂技術,也不需要使用者理解錯誤的Web應用程式。這應該說是個理想的結果。不過try-catch能夠讓我們實現自己的錯誤處理機制。
使用try-catch最適合處理那些無法控制的錯誤。假設在使用一個大型JavaScript庫中的函式,該函式可能會有意無意地丟擲一些錯誤。由於我們不能修改這個庫的原始碼,所以大可將對該函式的呼叫放在try-catch語句當中,萬一有什麼錯誤發生,也好恰當地處理它們。
在明明白白地知道自己的程式碼會發生錯誤時,再使用try-catch語句就不太合適了。例如,如果傳遞給函式的引數是字串而非數值,就會造成函式出錯,那麼就應該先檢查引數的型別,然後再決定如何去做。在這種情況下,不使用try-catch語句。
2.6.2 使用finally
finally子句在try-catch語句中是可選的,但finally子句已經使用,其程式碼無論如何都會執行。無論try或catch語句塊中包含什麼程式碼—甚至return語句,都不會阻止finally子句的執行。
【示例】只要程式碼中包含finally子句,那麼無論try還是catch語句塊中的return語句都將被忽略。因此,在使用finally子句之前,一定要非常清楚想讓程式碼怎麼樣。看下面這個函式。
這個函式在try-catch語句的每一部分都放了一條return語句。表面上看,呼叫這個函式會返回2,因為返回2的return語句位於try語句塊中,而執行該語句又不會出錯。可是,由於最後還有一個finally子句,結果就會導致該return語句被忽略,也就是說,呼叫這個函式只能返回0。如果把finally子句去掉,這個函式將返回2。
提示:如果提供finally子句,則catch子句就成了可選的,IE 7及更早版本中有一個Bug:除非有catch子句,否則finally中的程式碼永遠不會執行。如果考慮相容IE早期版本,應提供一個catch子句,哪怕裡面什麼都不寫,IE 8修復了這個Bug。
2.6.3 錯誤型別
執行程式碼期間可能會發生的錯誤有多種型別。每種錯誤都有對應的錯誤型別,而當錯誤發生時,就會丟擲相應型別的錯誤物件。ECMA-262定義了下列7種錯誤型別:
☑ Error
☑ EvalError
☑ RangeError
☑ ReferenceError
☑ SyntaxError
☑ TypeError
☑ URIError
其中Error是基型別,其他錯誤型別都繼承自該型別。因此,所有錯誤型別共享了一組相同的屬性,錯誤物件中的方法全是預設的物件方法。Error型別的錯誤很少見,如果有也是瀏覽器丟擲的,這個基型別的主要目的是供開發人員丟擲自定義錯誤。
EvalError型別的錯誤會在使用eval()函式而發生異常時被丟擲。
【示例1】如果沒有把eval()當成函式呼叫,就會丟擲該型別錯誤。
在實踐中,瀏覽器不一定會在應該丟擲錯誤時就丟擲EvalError。例如,Firefox 4+和IE 8對第一種情況會丟擲TypeError,而第二種情況會成功執行,不發生錯誤。因此,在實際開發中極少會這樣使用eval(),所以遇到這種錯誤型別的可能性極小。
RangeError型別的錯誤會在數值超出相應範圍時觸發。JavaScript中經常會出現這種範圍錯誤。
【示例2】在定義陣列時,如果指定了陣列不支援的項數,如-20或Number.MAX_VALUE,就會觸發這種錯誤。
在找不到物件的情況下,會發生ReferenceError。
【示例3】在訪問不存在的變數時,就會發生這種錯誤。
var obj=x; //在x並未宣告的情況下丟擲ReferenceErro
SyntaxError表示語法型別錯誤,當把語法錯誤的JavaScript字串傳入eval()函式時,就會導致此類錯誤。例如:
eval(“a ++ b”) //丟擲SyntaxError
如果語法錯誤的程式碼出現在eval()函式之外,則不太可能使用SyntaxError,因為此時的語法錯誤會導致JavaScript程式碼立即停止執行。
TypeError型別在JavaScript中會經常用到,在變數中儲存著意外的型別時,或者在訪問不存在的方法時,都會導致這種錯誤。錯誤的原因雖然多種多樣,但歸根結底還是由於在執行特定於型別的操作時,變數的型別並不符合要求所致。
【示例4】下面來看幾個例子。
最常發生型別錯誤的情況,就是傳遞給函式的引數事先未經檢查,結果傳入型別與預期型別不相符。
在使用encodeURL()或decodeURL()時,如果URL格式不正確,就會導致URLError錯誤。這種錯誤也很少見,因為這兩個函式的容錯性非常高。
利用不同的錯誤型別,可以獲悉更多有關異常的資訊,從而有助於對錯誤作出恰當的處理。
【應用】如果想知道錯誤的型別,可以按下面這樣在try-catch語句的catch語句中使用instanceof操作符。
在跨瀏覽器程式設計中,檢查錯誤型別是確定處理方式的最簡便途徑,包含在message屬性中的錯誤訊息會因瀏覽器而異。
2.6.4 丟擲錯誤
與try-catch語句相配的還有一個throw操作符,用於隨時丟擲自定義錯誤。丟擲錯誤時,必須要給throw操作符指定一個值,這個值是什麼型別沒有要求。
【示例1】下列程式碼都是有效的。
throw 1;
throw"hi"
throw true;
throw {name:“js”};
在遇到throw操作符時,程式碼會立即停止執行。僅當有try-catch語句捕獲到被丟擲的值時,程式碼才會繼續執行。
通過使用某種內建錯誤型別,可以更真實地模擬瀏覽器錯誤。每種錯誤型別的建構函式接收一個引數,即實際的錯誤訊息。
【示例2】下面是一個例子。
throw new Error(“丟擲錯誤”);
這行程式碼丟擲了一個通用錯誤,帶有一條自定義錯誤訊息。瀏覽器會像處理自己生成的錯誤一樣,來處理這行程式碼丟擲的錯誤。即瀏覽器會以常規方式報告這一錯誤,並且會顯示這裡的自定義錯誤訊息。
【示例3】下面程式碼使用其他錯誤型別,也可以模擬出類似的瀏覽器錯誤。
throw new SyntaxError(“SyntaxError”);
throw new TypeError(“TypeError”);
throw new RangeError(“RangeError”);
throw new EvalError(“EvalError”);
throw new URIError(“URIError”);
throw new ReferenceError(“ReferenceError”);
在建立定義錯誤訊息時,最常用的錯誤型別是Error、RangeError、ReferenceError和TypeError。
【應用】利用原型鏈還可以通過繼承Error來建立自定義錯誤型別。此時,需要為新建立的錯誤型別指定name和message屬性。
瀏覽器對待繼承自Error的自定義錯誤型別,就像對待其他錯誤型別一樣。如果要捕獲自己丟擲的錯誤並且把它與瀏覽器錯誤區別對待,建立自定義錯誤是很有用的。
提示:IE只有在丟擲Error物件時才會顯示自定義錯誤訊息。對於其他型別,它都無一例外地顯示exception thrown and not caught(丟擲了異常,且未被捕獲)。
2.6.5 案例:設計丟擲錯誤時機
針對函式執行失敗給出更多資訊,丟擲自定義錯誤是一種很方便的方式。應該在出現某種特定的已知錯誤條件,導致函式無法正常執行時丟擲錯誤。
【示例1】下面函式會在引數不是陣列的情況下失敗。
如果執行這個函式時傳給它一個字串引數,那麼呼叫sort()就會失敗。對此,不同瀏覽器會給出不同的錯誤訊息,但都不是特別明確。
【示例2】在處理上面示例中的這個函式時,通過除錯處理這些錯誤訊息沒有什麼困難。但是,在面對包含數千行JavaScript程式碼的複雜的Web應用程式時,要想查詢錯誤來源就沒有那麼容易了。在這種情況下,帶有適當資訊的自定義錯誤能夠顯著提升程式碼的可維護性。
重寫函式後,如果values引數不是陣列,就會丟擲一個錯誤。錯誤訊息中包含了函式的名稱,以及為什麼會發生錯誤的明確描述。如果一個複雜的Web應用程式發生了這個錯誤,那麼查詢問題的根源也就容易多了。
提示:在開發JavaScript程式碼的過程中,重點關注函式和可能導致函式執行失敗的因素。良好的錯誤處理機制應該可以確保程式碼中只發生自己丟擲的錯誤。
【擴充】何時該丟擲錯誤,而何時該使用try-catch來捕獲錯誤資訊?
一般來說,應用程式架構的較低層次中經常會丟擲錯誤,但這個層次並不會影響當前執行的程式碼,因而錯誤通常得不到真正的處理。如果打算編寫一個要在很多應用程式中使用的JavaScript庫,甚至只編寫一個可能會在應用程式內部多個地方使用的輔助函式,建議使用者在丟擲錯誤時提供詳盡的資訊。然後,即可在應用程式中捕獲並適當地處理這些錯誤。
在程式中,應該捕獲那些確切地知道該如何處理的錯誤。捕獲錯誤的目的在於避免瀏覽器以預設方式處理它們,而丟擲錯誤的目的在於提供錯誤發生具體原因的訊息。
2.6.6 錯誤事件
任何沒有通過try-catch處理的錯誤都會觸發window物件的error事件。這個事件是瀏覽器最早支援的事件之一,IE、Firefox和Chrome為保持向後相容,並沒有對這個事件做任何修改,Opera和Safari不支援error事件。
在任何Web瀏覽器中,onerror事件處理程式都不會建立event物件,但它可以接收3個引數:錯誤訊息、錯誤所在的URL和行號。多數情況下,只有錯誤訊息有用,因為URL只是給出了文件的位置,而行號所指的程式碼行既可能出自嵌入的JavaScript程式碼,也可能出自外部的檔案。
要指定onerror事件處理程式,必須使用如下所示的DOM 0級技術,它沒有遵循DOM 2級事件的標準格式。
只要發生錯誤,無論是不是瀏覽器生成的,都會觸發error事件,並執行這個事件處理程式。然後,瀏覽器預設的機制發揮作用,像往常一樣顯示出錯誤訊息。
【示例1】通過下面方法在事件處理程式中返回false,可以阻止瀏覽器報告錯誤的預設行為。
通過返回false,這個函式實際上就充當了整個文件中的try-catch語句,可以捕獲所有無程式碼處理的執行時錯誤。這個事件處理程式是避免瀏覽器報告錯誤的最後一道防線,理想情況下,只要可能就不應該使用它。只要能夠適當地使用try-catch語句,就不會有錯誤交給瀏覽器,也就不會觸發error事件。
提示:不同瀏覽器在使用error事件處理錯誤時的方式有明顯不同。在IE中,即使發生error事件,程式碼仍然會正常執行,所有變數和資料都將得到保留,因此能在onerror事件處理程式中訪問它們。但在Firefox中,常規程式碼會停止執行,事件發生之前的所有變數和資料都將被銷燬,因此幾乎就無法判斷錯誤了。
【擴充】影像也支援error事件。只要影像的src特性中的URL不能返回可以被識別的影像格式,就會觸發error事件。此時的error事件遵循DOM格式,會返回一個以影像為目標的event物件。
在這個例子中,當載入影像失敗時就會顯示一個警告框。注意,發生error事件時,影像下載過程已經結束,也就是說不能再重新下載了。
2.6.7 錯誤型別
錯誤處理的核心是首先要知道程式碼裡會發生什麼錯誤。由於JavaScript是鬆散型別的,而且也不會驗證函式的引數,因此錯誤只會在程式碼執行期間出現。一般來說,需要關注3種錯誤:
☑ 型別轉換錯誤。
☑ 資料型別錯誤。
☑ 通訊錯誤。
以上錯誤分別會在特定的模式下或者沒有對值進行足夠的檢查的情況下發生。
任何錯誤處理策略中的重點就是確定錯誤是否致命。對於非致命錯誤,可以根據下列一個或多個條件來確定:
☑ 不影響使用者的主要任務。
☑ 隻影響頁面的一部分。
☑ 可以恢復。
☑ 重複相同操作可以消除錯誤。
本質上,非致命錯誤並不是需要關注的問題。沒有必要因為發生了非致命錯誤而對使用者給出提示,可以把頁面中受到影響的區域替換掉,例如替換成說明相應功能無法使用的訊息。但是,如果因此打斷使用者,那確實沒有必要。
致命錯誤,可以通過以下一個或多個條件來確定:
☑ 應用程式根本無法繼續執行。
☑ 錯誤明顯影響到了使用者的主要操作。
☑ 會導致其他連帶錯誤。
要想採取適當的措施,必須要知道JavaScript在什麼情況下會發生致命錯誤。在發生致命錯誤時,應該立即給使用者傳送一條訊息,告訴無法再繼續執行了。假如必須重新整理頁面才能讓應用程式正常執行,就必須通知使用者,同時給使用者提供一個點選即可重新整理頁面的按鈕。
區分非致命錯誤和致命錯誤的主要依據,就是看它們對使用者的影響。設計良好的程式碼,可以做到應用程式某一部分發生錯誤不會影響另一個毫不相干的部分。例如,My Yahoo!(https://my.yahoo.com/)的個性化主頁包含了很多互不依賴的模組。如果每個模組都需要通過JavaScript呼叫來初始化,那麼使用者可能會看到類似下面這樣的程式碼:
表面上看,這些程式碼沒什麼問題,依次對每個模組呼叫init()方法。問題在於,任何模組的init()方法如果出錯,都會導致陣列中後續的所有模組無法再進行初始化。從邏輯上說,這樣編寫程式碼沒有什麼意義。畢竟每個模組相互之間沒有依賴關係,各自實現不同功能。可能會導致致命錯誤的原因是程式碼的結構。不過使用如下程式碼就可以把所有模組的錯誤變成非致命的:
通過在for迴圈中新增try-catch語句,任何模組初始化時出錯,都不會影響其他模組的初始化。在以上重寫的程式碼中,如果有錯誤發生,相應的錯誤將會得到獨立的處理,並不會影響到使用者的體驗。
2.6.8 案例:記錄錯誤
在開發Web應用程式過程中,應該集中儲存錯誤日誌,以便查詢重要錯誤的原因。例如,資料庫和伺服器錯誤都會定期寫入日誌,而且會按照常用API進行分類。在複雜的Web應用程式中,使用者也應該把JavaScript錯誤回寫到伺服器,並標明它們來自前端。把前後端的錯誤集中起來,能夠極大地方便對資料的分析。
要建立這樣一種JavaScript錯誤記錄系統,首先需要在伺服器上建立一個頁面(或者一個伺服器入口點),用於處理錯誤資料。這個頁面的作用無非就是從查詢字串中取得資料,然後再將資料寫入錯誤日誌中。這個頁面可能會使用如下所示的函式:
在上面程式碼中,logError()函式接收兩個引數:表示嚴重程度的數值或字串,以及錯誤訊息。其中,使用了Image物件來傳送請求,這樣做非常靈活,主要表現在如下幾方面。
☑ 所有瀏覽器都支援Image物件,包括那些不支援XMLHttpRequest物件的瀏覽器。
☑ 可以避免跨域限制。通常都是一臺伺服器要負責處理多臺伺服器的錯誤,而這種情況下使用XMLHttpRequest是不行的。
☑ 在記錄錯誤的過程中出現問題的概率比較低。大多數Ajax通訊都是由JavaScript庫提供的包裝函式來處理的,如果庫程式碼本身有問題,而依賴該庫記錄錯誤,可想而知,錯誤訊息是不可能得到記錄的。
只要是使用try-catch語句,就應該把相應錯誤記錄到日誌中。
在上面程式碼中,一旦模組初始化失敗,就會呼叫logError()函式,其中第一個參數列示錯誤的性質,第二個引數是真正的JavaScript錯誤訊息。記錄到伺服器中的錯誤訊息應該儘可能多地帶有上下文資訊,以便鑑別導致錯誤的真正原因。
2.7 JavaScript程式碼除錯
在JavaScript開發初期,開發人員常在要除錯的程式碼中隨處插入alert()函式。但這種做法一方面比較麻煩(除錯之後還需要清理),另一方面還可能引入新問題,如果把alert()函式遺留在產品程式碼後會帶來很多麻煩。如今,已經有了很多更好的除錯工具,因此就不再建議在除錯中使用alert()了。
2.7.1 使用控制檯
IE、Firefox、Opera、Chrome和Safari都有JavaScript控制檯,可以用來檢視JavaScript錯誤。而且,在這些瀏覽器中都可以通過程式碼向控制檯輸出訊息。對Firefox而言,需要安裝Firebug (http://getfirebug.com/),因為Firefox要使用Firebug的控制檯。對IE、Firefox、Chrome和Safari來說,都可以通過console物件向JavaScript控制檯中寫入訊息,該物件包含下列方法。
☑ error(message):將錯誤訊息記錄到控制檯。
☑ info(message):將資訊性訊息記錄到控制檯。
☑ log(message):將一般訊息記錄到控制檯。
☑ warn(message):將警告訊息記錄到控制檯。
在IE、Firebug、Chrome和Safari中,用來記錄訊息的方法不同,控制檯中顯示的錯誤訊息也不一樣。錯誤訊息帶有紅色圖示,而警告訊息帶有黃色圖示。
【示例1】以下函式展示了使用控制檯輸出訊息的一個示例。
在瀏覽器中執行上面程式碼,則可以在控制檯中看到輸出資訊,如圖2-11所示。
圖2-11 在IE控制檯中寫入資訊
【示例2】不存在一種跨瀏覽器向JavaScript控制檯寫入訊息的機制,但下面的函式可以作為統一的介面。
這個log()函式檢測了哪個JavaScript控制檯介面可用,然後使用相應的介面。可以在任何瀏覽器中安全地使用這個函式,不會導致任何錯誤,例如:
向JavaScript控制檯中寫入訊息可以輔助除錯程式碼,但在釋出應用程式時,還必須要移除所有訊息。在部署應用程式時,可以通過手工或特定的程式碼處理步驟來自動完成清理工作。
提示:記錄訊息要比使用alert()函式更可取,因為警告框會阻斷程式的執行,而在測定非同步處理對時間的影響時,使用警告框會影響結果。
2.7.2 顯示錯誤資訊
另一種輸出除錯訊息的方式,就是在頁面中開闢一小塊區域,用以顯示訊息。這個區域常是一個元素,而該元素可以總是出現在頁面中,但僅用於除錯目的,也可以是一個根據需要動態建立的元素。
【示例】針對2.7.1節的log()函式,可以將log()函式修改為如下所示:
這個修改後的log()函式首先檢測是否已經存在除錯元素,如果沒有則會新建立一個
然後,在頁面中進行測試,則效果如圖2-12所示。
圖2-12 在IE中顯示資訊
提示:與把錯誤訊息記錄到控制檯相似,把錯誤訊息輸出到頁面的程式碼也要在釋出前清除。
2.7.3 丟擲錯誤
如前所述,丟擲錯誤也是一種除錯程式碼的好辦法。如果錯誤訊息很具體,基本上就可以把它當作確定錯誤來源的依據。但這種錯誤訊息必須能夠明確給出導致錯誤的原因,才能省去其他除錯操作。
【示例】看下面的函式。
這個簡單的函式計算兩個數的除法,但如果有一個引數不是數值,它會返回NaN。類似這樣簡單的計算如果返回NaN,就會在Web應用程式中導致問題。對此,可以在計算之前,先檢測每個引數是否都是數值。
在此,如果有一個引數不是數值,就會丟擲錯誤。錯誤訊息中包含了函式的名字,以及導致錯誤的真正原因。瀏覽器只要報告了這個錯誤訊息,我們就可以立即知道錯誤來源及問題的性質。相對來說,這種具體的錯誤訊息要比那些泛泛的瀏覽器錯誤訊息更有用。
對於大型應用程式來說,自定義的錯誤通常都使用assert()函式丟擲。這個函式接收兩個引數,一個是求值結果應該為true的條件,另一個是條件為false時要丟擲的錯誤。以下就是一個非常基本的assert()函式。
可以用這個assert()函式代替某些函式中需要除錯的if語句,以便輸出錯誤訊息。下面是使用這個函式的例子。
可見,使用assert()函式可以減少丟擲錯誤所需的程式碼量,而且也比前面的程式碼更容易看懂。
2.7.4 IE錯誤
多年以來,IE一直都是最難於除錯JavaScript錯誤的瀏覽器。IE給出的錯誤訊息一般很短又語意不詳,而且上下文資訊也很少,有時甚至一點都沒有。但作為使用者最多的瀏覽器,如何看懂給出的錯誤也是最受關注的。下面簡單介紹在IE中常見的難於除錯的JavaScript錯誤。
1.操作終止
在IE 8之前的版本中,存在一個相對於其他瀏覽器而言,最難於除錯的錯誤:操作終止(operation aborted)。在修改尚未載入完成的頁面時,就會發生操作終止錯誤。發生錯誤時,會出現一個模態對話方塊,提示“操作終止。”,單擊“確定”(OK)按鈕,則解除安裝整個頁面,繼而顯示一張空白螢幕,此時要進行除錯非常困難。
【示例1】下面的示例試圖在頁面未完全載入時就對文件結構進行操作,會引發錯誤。
在這個例子中存在的問題:JavaScript程式碼在頁面尚未載入完畢時就要修改document.body,而且<script>元素還不是元素的直接子元素。準確一點說,當<script>節點被包含在某個元素中,而且JavaScript程式碼又要用appendChild()、innerHTML,或其他DOM方法修改該元素的父元素或祖先元素時,將會發生操作終止錯誤,因為它只能修改已經載入完畢的元素。
要避免這個問題,可以等到目標元素載入完畢後再對它進行操作,或者使用其他操作方法。例如,為document.body新增一個絕對定位在頁面上的覆蓋層,就是一種非常常見的操作。通常,開發人員都是使用appendChild()方法來新增這個元素的,但使用insertBefore()方法也很容易。因此,只要修改前面例子中的一行程式碼,就可以避免操作終止錯誤。
在這個例子中,新的
【示例2】除了改變方法之外,還可以把<script>元素從包含元素中移來,直接作為的子元素。
這一次也不會發生錯誤,因為指令碼修改的是它的直接父元素,而不再是間接的祖先元素。
在同樣的情況下,IE 8不再丟擲操作終止錯誤,而是丟擲常規的JavaScript錯誤。不過,雖然瀏覽器丟擲的錯誤不同,但解決方案仍然是一樣的。
2.無效字元
根據語法,JavaScript檔案必須只包含特定的字元。在JavaScript檔案中存在無效字元時,IE會丟擲無效字元(Invalid Character)錯誤。所謂無效字元,就是JavaScript語法中未定義的字元。
例如,有一個很像減號但卻由Unicode值8211表示的字元(\u2013),就不能用作常規的減號(ASCII編碼為45),因為JavaScript語法中沒有定義該字元。這個字元通常是在Word文件中自動插入的。如果JavaScript程式碼是從Word文件中複製到文字編輯器中,然後又在IE中執行的,那麼就可能會遇到無效字元錯誤。其他瀏覽器對無效字元做出的反應與IE類似,Firefox會丟擲非法字元(Illegal Character)錯誤,Safari會報告發生了語法錯誤,而Opera則會報告發生了ReferenceError(引用錯誤),因為它會將無效字元解釋為未定義的識別符號。
3.未找到成員
IE中所有DOM物件都是COM物件,而非原生JavaScript物件的形式實現的。這會導致一些與垃圾收集相關的非常奇怪的行為。IE中的未找到成員(Member not found)錯誤,就是由於垃圾收集例程配合錯誤所直接導致的。
具體來說,如果在物件被銷燬之後,又給該物件賦值,將會導致未找到成員錯誤。而導致這個錯誤的一定是COM物件。發生這個錯誤的最常見情形是使用event物件時。IE中的event物件是window的屬性,該物件在事件發生時建立,在最後一個事件處理程式執行完畢後銷燬。
【示例3】假設在一個閉包中使用了event物件,而該閉包不會立即執行,那麼在將來呼叫它並給event的屬性賦值時,就會導致未找到成員錯誤。
在這段程式碼中,將一個單擊事件處理程式指定給了文件。在事件處理程式中.window.event被儲存在event變數中。然後,傳入setTimeout()中的閉包裡又包含了event變數。當單擊事件處理程式執行完畢後,event物件就會被銷燬,因而閉包中引用物件的成員就成了不存在的。因此,在閉包中給returnValue賦值就會導致未找到成員錯誤。
4.未知執行時錯誤
當使用innerHTML或outerHTML以下列方式指定HTML時,就會發生未知執行時錯誤(Unknown runtime error):一是把塊元素插入到行內元素時,二是訪問表格任意部分(、等)的任意屬性時。
【示例4】從技術角度說,標籤不能包含
span.innerHTML =“Hi”; //這裡span包含元素
在遇到把塊級元素插入到不恰當位置的情況時,其他瀏覽器會嘗試糾正並隱藏錯誤,而IE會提示錯誤。
5.語法錯誤
一般情況下,只要IE報告發生了語法錯誤(Syntax Error),都可以很快找到錯誤的原因,但還有一種原因不是十分明顯的情況需要格外注意。
如果使用者引用了外部的JavaScript檔案,而該檔案最終並沒有返回JavaScript程式碼,IE也會丟擲語法錯誤。例如,<script>元素的src特性指向了一個HTML檔案,就會導致語法錯誤。報告語法錯誤的位置時,通常都會說該錯誤位於指令碼第一行的第一個字元處。Opera和Safari也會報告語法錯誤,但它們會給出導致問題的外部檔案的資訊,IE就不會給出這個資訊,因此就需要自己重複檢查一遍引用的外部JavaScript檔案,但Firefox會忽略這種解析錯誤。
6.系統無法找到指定資源
在使用JavaScript請求某個資源URL,而該URL的長度超過了IE對URL最長不能超過2083個字元的限制時,就會發生這個錯誤。IE不僅限制JavaScript中使用的URL的長度,而且也限制使用者在瀏覽器自身中使用的URL長度(其他瀏覽器對URL的限制沒有這麼嚴格),IE對URL路徑還有一個不能超過2048個字元的限制。
【示例5】下面的程式碼將會導致錯誤。
在這個例子中,XMLHttpRequest物件試圖向一個超出最大長度限制的URL傳送請求。在呼叫open()方法時,就會發生錯誤。避免這個問題的辦法,無非就是通過給查詢字串引數起更短的名字,或者減少不必要的資料,來縮短查詢字串的長度。另外,還可以把請求方法改為POST,通過請求體而不是查詢字串來傳送資料。
相關文章
- [開發教程]第4講:在網頁中使用 Bootstrap網頁boot
- 使用 Beautiful Soup 在 Python 中抓取網頁Python網頁
- 使用openlayers在網頁中展示地理資訊網頁
- Go和JavaScript結合使用:抓取網頁中的影像連結GoJavaScript網頁
- JavaScript 獲取div在頁面中座標JavaScript
- 在網頁設計中如何排版網頁
- IP地址在網頁抓取中的作用網頁
- JavaScript網頁設計案例JavaScript網頁
- 前端黑科技:使用 JavaScript 實現網頁掃碼功能前端JavaScript網頁
- 在 C# 和 JavaScript 之間選擇進行網頁抓取C#JavaScript網頁
- 第85節:Java中的JavaScriptJavaScript
- DSBridge例項-在網頁中展示Native進度網頁
- HTML在網頁設計中是什麼作用?HTML網頁
- 在HTML中使用JavaScriptHTMLJavaScript
- [譯] 在JavaScript中何時使用var、let及constJavaScript
- Java / JavaScript在TensorFlow中的入門使用指南JavaScript
- 爬蟲(6) - 網頁資料解析(2) | BeautifulSoup4在爬蟲中的使用爬蟲網頁
- Python 爬取網頁中JavaScript動態新增的內容(一)Python網頁JavaScript
- Python 爬取網頁中JavaScript動態新增的內容(二)Python網頁JavaScript
- 在 JavaScript 中掌握日期JavaScript
- js 學習之路5:使用js在網頁中新增水印JS網頁
- Java 在PDF中插入頁首、頁尾Java
- 在JavaScript中理解策略模式JavaScript模式
- 在 JavaScript 中如何克隆物件?JavaScript物件
- 【JS 口袋書】第 10 章:使用非同步 JavaScriptJS非同步JavaScript
- 使用結構化克隆在 JavaScript 中進行深度複製JavaScript
- 在 JavaScript 中,什麼時候使用 Map 或勝過 ObjectJavaScriptObject
- 細談在HTML中使用JavaScriptHTMLJavaScript
- 在 Android 使用 QuickJS JavaScript 引擎教程AndroidUIJSJavaScript
- javascript實現網頁截圖匯出方案JavaScript網頁
- [記錄]利用workerman在laravel中實現網頁聊天室Laravel網頁
- 在Go中構建區塊鏈 第7部分:網路Go區塊鏈
- Html 中如何使用javaScriptHTMLJavaScript
- 在JavaScript中this到底指代什麼?JavaScript
- 在Vue3中使用Element-Plus分頁(Pagination )元件Vue元件
- 使用Vue.js在WordPress中建立單頁面應用SPAVue.js
- NodeJS使用PhantomJs抓取網頁NodeJS網頁
- 使用 rem 設計網頁REM網頁