錯誤管理

初級程式人員和專家之間,最顯著的差別之一就是錯誤管理,無論程式人員的技能水準如何,錯誤都會發生,因為使用者會製造很多問題,但是經驗豐富的開發人員在發生錯誤時能夠妥善處理。
有三種避免與處理錯誤的方法:

1捕捉和拋出錯誤

這裡大都採用基本的 if-else 結構監控錯誤,這種方法對簡單的程式碼已經綽綽有餘,但針對更複雜的情況,尤其是物件導向程式設計(OOP)中,還有更佳的系統──異常處理,此機制包含 try 和 catch 敘述以及 throw 和 finally。

捕捉錯誤


try...catch 程式碼區塊的語法如下:
try{ ... }
catch(error){ ... }
          try {
            ...
          } catch(error) {
            //使用錯誤
          }
        
當 try 程式區塊(大括號之間的部份)發生錯誤時,程式流立即轉到 catch 區塊,catch 能夠處理任何程式碼中出現的錯誤;如果 try 區塊中沒有任何錯誤,則完全不執行 catch 區塊。
catch 區塊可以對錯誤作出有關的回應,而錯誤變數也是一個物件,實際上在 OOP 中,try...catch 區塊內的錯誤通常稱作異常(Exception),其為錯誤的物件呈現形式,除非另作說明,物件的具體型別為 Error,這個物件有下列兩個有用的屬性(其他瀏覽器可能提供其他的 Error 物件屬性): 運用上述知識,catch 區塊可記錄錯誤至控制台:
          console.log(error.name + ': ' + error.message + '\n');
        
可以想像 catch 區塊的異常變數為一個函數參數,僅存在於此類程式區塊之中。
          
依型別捕捉
catch 區塊處理 try 區塊中可能發生的所有異常,但是異常也可以按照具體的類型來捕捉,例如,當操作錯誤的物件型別時,標準的 JavaScript 程式碼可能拋出 TypeError 異常,而於數字超出允許值範圍時拋出 RangeError 異常,因為這些問題有顯著的不同,所以可對它們進行單獨的處理,為此建議採用多個 catch 程式區塊,並指出每個區塊要捕捉的異常類型,其內可搭配 instanceof 運算子和條件敘述,以便捕捉具體的異常類型。
            try {
              ...
            } catch (ex if ex instanceof TypeError){
              //使用錯誤
            } catch (ex if ex instanceof RangeError){
              //使用錯誤
            }
          
一旦知道如何建立自己的物件類型後,就能以程式碼攔截自訂的異常類型。

finally 子句


finally 子句是 try...catch 結構的一種補充,它總是出現在最後:
finally{ ... }
          try {
            ...
          } catch(ex) {
            //使用錯誤
          } finally {
            //結束程式碼
          }
        
不管是否發生錯誤,總會執行 finally 區塊中的程式碼,finally 區塊通常用來執行清理工作,無論之前發生過什麼,這些工作都必須進行,例如,finally 子句中的程式可以刪除錯誤處理器,或者為不再需要的 Ajax 物件填入 null 值。
catch 和 finally 區塊都不是必需的,但是必須至少有這兩者之一,如果在 try 區塊編寫程式而不捕捉例外,向使用者回報的異常就無法和 try...catch 敘述的標準錯誤一樣,倘若因為某種理由,不希望在發生異常時做任何事情,則可建立空白的 catch 區塊:
          try {
            ...
          } catch(ex) {
          }
        
參考 MSDN try...catch...finally 陳述式:https://msdn.microsoft.com/zh-tw/library/4yahc5d8(v=vs.94).aspx
參考 W3school JavaScript try/catch/finally Statement:http://www.w3schools.com/jsref/jsref_try_catch.asp

拋出異常


到目前為止的程式碼基於這種思路:問題發生時 JavaScript 會引發異常。也可以自己觸發異常,並由 catch 區塊捕捉,透過 throw 敘述便能做到這一點:
          throw 2;
          throw 'No such HTML element!';
          throw new Error('No such HTML element!');

          var error = {name: 'Division Error', message: 'Cannot divide by zero.'}
          throw error;
        
儘管如此最好是拋出 Error 物件,如此便能編寫一致性的 catch 區塊,以便使用 namemessage 屬性。Error 物件還有一個附加的好處,在某些瀏覽器中該物件還有更多有用的屬性。
try 區塊的程式碼往往在函數呼叫失敗時拋出異常──或許是因為該函數沒有接收到正確的參數,也可透過自己的函數拋出異常:
            function $(id){
              'use strict';
              if(typeof id != 'undefined'){
                return document.getElementById(id);
              }else{
                throw Error('The function requires one argument.');
              }
            }

            try{
              var elem = $();
              elem.innerHTML = 'blah'
            }catch(ex){
              console.log('Could not update element because: ' + ex.message + '\n');
              document.getElementById('btn').innerHTML = 'ex.name: ' + ex.name + '
ex.message: ' + ex.message + ''; }
參考 MSDN throw 陳述式:https://msdn.microsoft.com/zh-tw/library/85fscz6h(v=vs.94).aspx
參考 W3school JavaScript Errors - Throw and Try to Catch:http://www.w3schools.com/js/js_errors.asp

整合


try...catch 有許多良好的常見用法,像是 getXMLHttpRequestObject() 函數,具體地說,這些程式試圖建立一個 ActiveXObject 物件,如果無法產生,則捕捉發生的所有異常。
使用 try 和 catch 敘述更新函數的步驟如下:
  1. 在文字編輯器或 IDE 中開啟 ajax.js
  2. 利用下列內容代替建立新 ActiveXObject 物件的程式:
                  try {
                    ajax = new ActiveXObject('MSXML2.XMLHTTP.3.0');
                  } catch(error) {
                    console.log('Could not create the ActiveXObject: ' + error.message + '\n');
                  }
                
    現在試圖建立 ActiveXObject 的程式碼放在 try 區塊中,如果建立失敗,將捕捉和回報異常,可以擴充此法,使其嘗試產生不同類型的 ActiveXObject,從最新的版本(MSXML2.XMLHTTP.6.0)開始,並且回溯到支援的版本。
  3. 儲存 ajax.js 檔案

2使用斷言

try...catch 結構用於更優雅地處理可能發生的錯誤,而斷言(Assertion)和單元測試的目的是標出不應該發生的錯誤,在程式設計中,斷言是「確定情況就是如此」的程式碼。因此很容易使用,並於開發專案的同時快速地協助偵錯,和單元測試類似,斷言還能大幅減少最終版本程式碼的臭蟲。
JavaScript 沒有預先定義的斷言方法,不過可以自行編寫,或者使用 Firebug 內建的斷言方法。

建立斷言函數


斷言函數可能的定義如下:
          window.onload = function(){
            document.getElementById('submit').onclick = function(){
              //自訂 assert() 函數,就能透過它快速檢查自己編寫的程式碼
              function assert(expression, message){
                if(!expression)
                  throw {name: 'Assertion Exception', message: message};
              }

              var radius = document.getElementById('radius').value;
              document.getElementById('r').innerHTML = radius;

              //斷言 radius 不是未定義的變數
              assert((typeof radius != 'undefined'), 'The radius must be a number.');

              var volume = (4/3) * Math.PI * Math.pow(radius, 3);
              document.getElementById('v').innerHTML = volume;

              //目標是確定 volume 變數是一個數字,所以 isNaN() 應該返回 false
              //為了斷言函數之前使用「!」
              assert(!isNaN(volume), 'The volume is not number');
            }
          }
        
第一個參數是準備評估的運算式:希望斷言哪個條件為真?
第二個參數是運算式不為真實顯示的訊息。

因為 assert() 函數拋出一異常,所以能夠封裝前述程式碼至一段 try...catch 區塊中。
注意斷言不應該出現在任何實際使用的程式中,因為:
最好不要把斷言用於驗證使用者的輸入

Firebug 中的斷言


如果使用 Firebug,則可呼叫其 assert() 方法作為偵錯工具,該方法已是 console 物件的一部份。
          window.onload = function(){
            document.getElementById('submit').onclick = function(){
              var radius = document.getElementById('radius').value;
              document.getElementById('r').innerHTML = radius;
              console.assert((typeof radius == 'number'), 'The radius must be a number.');
              var volume = (4/3) * Math.PI * Math.pow(radius, 3);
              document.getElementById('v').innerHTML = volume;
              console.assert(!isNaN(volume), 'The volume is not number');
            }
          }
        

3單元測試

單元測試(Unit testing)對程式設計而言是相對新穎的概念,不過許多開發人員都奉行這種方法,它能夠讓較大型的應用程式開發更加可靠。單元測試的前提是:定義測試,確認程式碼的特定部分按照需求運作,測試應該盡可能具有不可分割性(明確而簡短),和斷言一樣,單元測試需要檢查程式碼是否依照開發者的意圖運作;單元測試不是用來驗證使用者輸入,或者在網站實際執行期間處理可能發生的問題的手段。

隨著應用程式範圍的擴展以及程式碼增加與修改,我們得持續為新程式編寫測試,同時仍用現有的測試檢查所有的原始程式碼,如此方能確保新程式和修改後的程式碼不會破壞之前正常運作的部分。而且,單元測試通常在一開始就能改善編寫的程式碼,因為是從測試開始思考所有可能性,而不僅僅是預期的結果。
實施單元測試的最佳手段是採用為此目的建立的許多程式庫,像是 JsUnitJasmineSelenium 或是 jQuery、YUI 之類的框架中就有單元測試插件或相關元件。

安裝 jsUnity


jsUnity 是 JsUnit 的一個更新版本,以下以這個單元測試作示範,請先到 jsUnity 網站下載一個單獨的 JavaScript 檔案,並且含括至想要測試的 HTML 頁面:
        <script src="js/jsunity-0.6.js"></script>
      

定義測試


定義一系列測試的最佳方法是建立測試套件(Suite),方法很多,例如定義一個包圍函數:
        var myTests = function(){
          function testThis(){          //於包圍函數內定義代表個別測試的子函數
            ...
          }
        };
      
注意,所有測試函數名稱都必須以「test」開始
在每項測試中使用一個 jsUnity 的斷言方法,jsUnity 沒有簡單的 assert() 方法,但提供更特定的方法: 例如,前述斷言程式碼的 jsUnity 版本如下:
        jsUnity.assertions.assertNotUndefined(myVar);
        jsUnity.assertions.assertTypeOf('number', radius);
        jsUnity.assertions.assertNotNaN(volume);
      
全部的斷言函數都定義在 jsUnity.assertions 中

為了重複執行,單元測試應該盡可能詳盡,而且覆蓋各種可能性,這意味著測試應該確認程式碼正常執行時的正確結果,以及不正常執行時的不同結果。

執行測試


定義測試之後,可以呼叫 run() 方法執行測試:
        var results = jsUnity.run(myTests);
      
執行所有測試之後,result 變數有多個反應結果的屬性 但其結果並不能代表哪些測試通過,哪些測試失敗,因此我們需要定義一個記錄函數。

記錄結果


為了建立一個回報測試結果的記錄函數,所以將函數定義指派給 jsUnity.log 屬性:
        jsUnity.log = function(message){
          //Do something with message
        };
      
該函數的唯一參數是一個字串,此字串根據測試當時所處的階段而異,包含總體結果的回報,可於控制面板顯示這筆訊息,或者動態新增到網頁上。

制定與拆除


最後需要了解一點是如何準備測試,多數情況下,測試預期某些事件的發生,此為測試的必要條件,例如,如果使用者操作之後才觸發函數,便可在準備階段手動呼叫這些函數,準備步驟還能用來調整作用域問題,以便測試必要的變數。
setup() 函數的目的是在測試前執行某些任務,對應的 tearDown() 函數則於測試後執行任務,這兩個函數都可以定義至測試套件中:
        var myTests = function(){
          function setUp(){
            //Do these things
          }
          function tearDown(){
            //Now do these
          }
          function testThis(){
            ...
          }
        }
      

整合


首先測試程式庫的兩個函數:$()──document.getElementById() 的捷徑,以及用來設定元素的 textContent 或 innerText() 的 setText()。為了定義這些測試,必須考慮正常或者不正常的狀況下程式碼應該完成的任務。HTML 頁面只需要兩個段落:
        <p id="output"></p>
        <p id="results"></p>
      
第一個段落用來測試,第二個段落則顯示結果

HTML 頁面還必須含括 utilities.js 腳本(也就是待測的程式碼)、jsUnity 程式庫檔,以及和定義和執行測試的檔案(完成於後續步驟):
          //定義測試套件
          var myTests = function() {                //myTests 物件儲存所有測試
          	'use strict';
            //第一個測試:確認 $() 函數在使用正確的元素 ID 時返回非空值。
          	function testGetElement() {
          		jsUnity.assertions.assertNotNull(U.$('output'));
          	}
            //第二個測試:確認參數為無效元素 ID 時,函數應返回 null
          	function testGetInvalidElement() {
          		jsUnity.assertions.assertNull(U.$('doesNotExist'));
          	}
            //第三個測試:setText() 函數返回一個布林值,表示是否可為指定元素的 textContent 或 innerText 屬性指派內容
            //確認函數接收的參數為一有效的元素和字串時,結果返回 true
          	function testSetText() {
          		jsUnity.assertions.assertTrue(U.setText('output', 'test'));
          	}
            //第四個測試:確認參數為無效的元素 ID 時,結果為 false
          	function testCannotSetText() {
          		jsUnity.assertions.assertFalse(U.setText('doesNotExist', 'test'));
          	}
          };

          //建立 log() 函數,將一包含具體訊息的段落新增到 result 元素上
          jsUnity.log = function(error) {
          	U.$('results').innerHTML += '

' + error + '

'; }; //執行測試,由於記錄函數將自動回報結果,因此沒有必要將測試結果填入一個新變數 jsUnity.run(myTests);
新版本的 jsUnity 屬性跟書上寫的有所不同,需再查對!
DEMO MEMO