表單處理

表單提供與使用者互動的基本介面,以 Web 的應用來說是不可或缺的功能,首先列出常見的表單問題,接著了解各種表單元素類型的用途、常規運算式等。
參考 w3schools JavaScript Forms:http://www.w3schools.com/js/js_validation.asp
參考 w3schools JavaScript HTML Input Examples:http://www.w3schools.com/js/js_input_examples.asp

1一般表單考慮事項

處理表單提交的最佳途徑是為表單本身增加提交事件處理器,提交事件在使用者點擊提交事件或者按下 Enter/Return 鍵(適用於某些瀏覽器)時觸發,因此,監控提交事件是最可靠和最容易的方法。在極少數的情況下,我們可能希望在修改某些表單元素時驗證,但此舉應謹慎,因為 change 事件的發生頻率可能過高,不一定適合於廣泛的驗證。
從歷史經驗得知,開發人員有時候建立沒有提交按鈕的表單,而是在表單元素值(例如選擇下拉選單)變化時提交。必須理解一點:如果這麼做的話,代表未啟用 JavaScript 的使用者就無法提交表單
談到可存取性,表單應該始終存在一個有效的 action 屬性值,當未啟用 JavaScript 時,可讓表單資料通過一種實際的資源,在實際的網站中,建議必須始終建立一個備用的頁面,在使用者未啟用 JavaScript 時處理表單提交。

可以利用 JavaScript 動態修改 action 值:
          document.getElementById('theForm').action = 'otherPage.php';
        
如此一來,就可以在提交表單時,讓啟用 JavaScript 的使用者進入與未啟用 JavaScript 的使用者不同的位置。

為了阻止表單提交到伺服端的腳本,可以透過阻止預設事件行為的標準技術(出現在表單提交事件處理器中):
          if(e.preventDefault){
            e.preventDefault();
          }else{
            e.returnValue = false;
          }
          return false;
        
返回 false 只在使用 DOM Level 0 方法建立事件處理器時有效;其他方法則適用於 DOM Level 2 事件處理器,分別是能使用 addEventListener() 的瀏覽器,以及無法使用的情況(也就是舊版的 IE)。

可藉助 JavaScript 呼叫表單元素的 submit() 方法,強制表單的提交:
          document.getElementById('theForm').submit();
        

最後,在瀏覽器支援 HTML5 的情況下,它將根據輸入類型和指定的屬性執行驗證,這種驗證提供令人滿意的錯誤資訊,如果不使用 HTML5 或者瀏覽器不支援 HTML5,就得改用 DOM 操作和 CSS 提供具有吸引力的內嵌錯誤資訊。

建立錯誤訊息


為了完成所有必要的任務:建立的函數需要兩個參數:1.應用錯誤訊息的表單元素 id 以及2.錯誤訊息本身,接著加上一個包含資訊的 span 元素,並將其附加在目標元素之後。該函數必須為新的 span 指定一個 id 值,以便在重複呼叫時檢查 span 是否存在(如果已存在就沒有必要再產生),其他函數也能夠在合適的時候刪除錯誤訊息,該函數還為表單元素的標籤增加 error 類別。
將新增和刪除錯誤訊息製成一支獨立函數:
          //定義 addErrorMessage() 函數
          function addErrorMessage(id, msg){
            'use strict';
            //取得表單元素
            var elem = document.getElementById(id);
            var newId = id + 'Error';
            var span = document.getElementById(newId);

            //檢查 span 的存在
            if(span){
              span.firstChild.value = msg;                            //即顯示錯誤訊息
            }else{
              span = document.createElement('span');                  //建立一個元素
              span.id = newId;                                        //為該元素指定一個適合的 id
              span.className = 'error';                               //應用 error 類別至 span
              span.appendChild(document.createTextNode(msg));         //在 span 裡放入錯誤訊息
              //新增 span 到 DOM 中
              elem.parentNode.appendChild(span);                      //將新的 span 附加到表單元素的雙親節點內
              elem.previousSibling.className = 'error';               //為元素的標籤指定 error 類別
            }
          }

          //定義 removeErrorMessage() 函數,用來刪除 span 以及對應標籤中的錯誤類別
          function removeErrorMessage(id){
            'use strict';
            var span = document.getElementById(id + 'Error');

            //檢查 span 的存在
            if(span){
              span.previousSibling.previousSibling.className = null;  //從標籤中刪除 error 類別
              span.parentNode.removeChild(span);                      //刪除 span
            }
          }
        

建立工具提示


為了大幅降低使用者錯誤,最好在完成表單的同時,向使用者傳達期望的輸入,如果設計之初考慮到這一點,便可在表單元素的旁邊增加註解,但這種方法可能使頁面看上去很繁雜。
替代方案之一是藉助工具提示,工具提示依照需求顯示和消失,所以每次只會出現一種提示──用於目前正在處理的元素。
從設計和寫程式的角度來看,工具提示類似一種強制回應視窗,但是不會遮住頁面的其餘部分,一開始可用固定的 HTML 建立工具提示:
          

為達到可存取性的目的,最好將工具提示放到對應的元素標籤內,但是 span 在不支援 CSS 的瀏覽器中將被隱藏,所以這樣做反而使得工具提示沒有原來那麼醒目。注意,span 具有 tooltip 的類別,同時應用於 CSS 和 JavaScript:
          label { position: relative; }
          .tooltip {
            display: block;
            visibility: hidden;
            position: absolute;
            left: 30em;
            top: 0;
          }
        

現在 JavaScript 只需要在滑鼠移過對應的表單元素,或者元素取得焦點時改變工具提示的 visibility 屬性,因此建立一函數方便完成任務:
          function addTooltipHandlers(elem){
            //監控「滑鼠移入元素」或「元素在焦點上」時顯示
            document.getElementById('someFormElement').onmouseover = showTooltip;
            document.getElementById('someFormElement').onfocus = showTooltip;

            //監控「滑鼠離開元素」或「元素失去焦點」時隱藏
            document.getElementById('someFormElement').onmouseout = hideTooltip;
            document.getElementById('someFormElement').onblur = hideTooltip;
          }
        

製作 showTooltip() 和 hideTooltip() 函數,它們都以一個事件作為參數,並以此事件動態修改元素的 visibility 屬性:
          function showToolTip(e){
            if (typeof e == 'undefined')  e = window.event;
            var target = e.target || e.srcElement;
            target.previousSibling.lastChild.style.visibility = 'visible';
          }
          function hideToolTip(e){
            if (typeof e == 'undefined')  e = window.event;
            var target = e.target || e.srcElement;
            target.previousSibling.lastChild.style.visibility = 'hidden';
          }
        
這兩個函數的難處式表單元素觸發事件,需要修改的是標籤內的 span,為從表單元素中使用 span,首先引用 previousSibling(就是標籤),接著是 lastChild。

停用提交按鈕


停用提交按鈕最常見的目的是避免無意間的多次提交,尤其在電子商務網站上,當表單作為付款流程一部份的情況下。
停用提交按鈕,主要是透過設定按鈕的 disabled 屬性為 disabled 或布林值 true 來完成:
          document.getElementById('submitButton').disabled = true;
          //or
          document.getElementById('submitButton').disabled = 'disabled';
        
在需要布林值的情況下,實際上任何值都被看作真值,所以 disabled 和布林值 true 的效果相同。

作為附加功能,停用提交功能時,最好從視覺上為使用者指出這種變異和狀態:
  1. 這可透過為按鈕應用 CSS 類別、在按鈕旁邊增加提示訊息的元素、修改按鈕上的文字或者綜合以上方法來完成
  2. 增加訊息,它採用的技術與建立表單元素的錯誤訊息相同
  3. 實際上,我們可以基於 CSS 的強制回應視窗,建立帶有訊息的一個新層級,並將頁面的其餘部分變為灰色,同時阻止使用者再次點擊按鈕,藉以通知他們發生的情況

2文字欄位和文字區域

文字輸入欄位和文字區域基礎知識


可以檢查文字輸入欄位或文字區域的 value 屬性,以便讀取使用者輸入的值:
          var data = document.getElementById('comments').value;
        
value 屬性代表元素內的任意文字,無論是使用者輸入還是預先設定,該屬性同樣適用於隱藏欄位和密碼輸入。

可為元素的 value 賦予新字串,藉以改變文字輸入欄位或文字區塊的內容:
          document.getElementById('someInput').value = 'new value';
        

可以藉助 HTML5 使用文字區域的 maxlength 屬性,不過應先向使用者說明此限制。
在驗證文字輸入欄位和文字區域時,可以透過字串方法檢查最小長度,或在合適時使用常規運算式。如果為文字輸入欄位或文字區域設定預設值,驗證常式就得加以考慮,只有當文字輸入欄位或文字區域的值不同於預設值時,才能通過驗證,就是使用 defaultValue 屬性,這個屬性代表指派給 value 屬性的值:
          
        
有了這個屬性,就能在過程中根據這個預設值進行驗證:
          if(elem.value == elem.defaultValue){
            addErrorMessage(elem.id, '請輸入一個值');
          }
        

實現自動完成


一種 JavaScript 和文字輸入欄位常見的動態用法是在使用者輸入時提供建議,這種功能稱作自動完成(AutoComplete),自動完成在許多方面改進了使用者體驗:1.它節省了使用者的輸入,意味著減少其錯誤,2.根據自動完成的實作方式,它能夠提出可用或者最有可能有用的結果,例如,尋找員工記錄的人力資源工具可能只提供實際的員工姓名,而像 Google Suggest 的自動建議功能根據已經輸入的內容,還能列出常見的搜尋詞。

自動完成能必須克服三個障礙:
  1. 搜尋演算法,如果進行的是一次瀏覽,那就不會很困難或者很慢(例如輸入 ac 時提示 ace 和 active,而非 didatic),若希望進行真正的搜尋,結果是包含輸入字元的字串,那麼就得花費更多精力(例如 ac 也適用於 didactic)
  2. 資料來源,假設搜尋較小的資料集合,可先建立一個 JavaScript 陣列,再將資料集合存放至瀏覽器
  3. 如果資料集合過大,或許希望在執行中以 Ajax 讀取資料,這個障礙比較次要:以可靠的跨瀏覽器方式顯示建議
由於上述的三個障礙,好的自動完成功能非常複雜,最好是藉助框架或程式庫來達到目的。

3選單

與文字輸入欄位、文字區域、密碼和隱藏欄位類似,選單中的選項可透過 value 屬性取得:
          var data = document.getElementById('selectMenu').value;
        
但和其他元素不同的是,無法為該屬性指派新值以便修改選單的內容,而得藉由更新選單的 selectedIndex 屬性,下列程式碼指定第二個項目為預設選項:
          document.getElementById('selectMenu').selectIndex = 1;
        
更複雜的是,選單可以變成多重選擇:
          <select name="categories" id="categories" multiple>
        
挑選多的選項後,value 屬性只代表第一個選中的值,selectedIndex 也是如此,為了讀取所有選中的內容,必須遍覽所有選項、尋找被選中的選項,引用選單的 options 屬性可以存取所有選項,options 屬性是一個陣列,它的 length 屬性可應用至 for 迴圈中,對於每個選項,都有 selected、value、text 屬性,加上這些屬性便能取得所有的選中值:
          var selected =[];
          for(var i=0, count = elem.options.length; i < count; i++){
            if(elem.options[i].selected){
              selected.push(elem.options[i].value);
            }
          }
        

驗證選單


為了檢查使用者是否更改選單,只需確認元素的 selectIndex 屬性不為 -1,這種方法適用於單選或多選的選單,如果選單允許多選,則可利用前述的程式碼取得所有值,然後讀取 selected 陣列的 length,以便了解選到的數量。如果選單有一個作為提示的初始選項(這種情況下可能不允許多選),就必須確認 selectIndex 的屬性不為 0

建立動態選擇框


一種選單的常見動態用法要求 JavaScript,這種方法就是連結選單,此類選單中,第一個選單的選項會改變第二個選單的可用選項。例如,當第一個選單列出汽車後,連結選單將顯示汽車型號。
第二個選單所用的資料可透過 Ajax 從伺服器讀取,進而將一開始必須傳遞給客戶端的資料減少到最低限度。
完成此一功能需藉助第一個選單的修改(change)事件,在事件處理器中,若想根據第一個選單的選擇更新第二個選單的選項,可以利用兩種方式完成。理論上,將選項以 HTML 形式賦值給選單的 innerHTML 屬性是一種簡單的解決方案,因為選單能夠包含 HTML 的元素(具體地說是 option 元素),這種方案應該可行,但在 IE 9 中無法正常運作,因此需要採取更複雜的方法。
舉一個具體的例子,準備讓使用者挑選一套作業系統,1.預設情況下不使用 JavaScript,他會看到一個較長的選單,並依照總類分類選項,2.對於啟用 JavaScript 的使用者來說,選單採用了漸進式改善手法,選項分佈到兩個選單,第二個選單的選項始終根據第一個選單的項目而定。

重要的初始 HTML 如下:
            //定義 updateMenu() 函數,不需任何參數,因為函數已知道所需的 HTML 元素
            function updateMenu(){
              'use strict';
              //取得兩個選單的參照
              var os = document.getElementById('os');
              var os2 = document.getElementById('os2');
              //建立一個空變數,用來儲存第二個選單的選項,根據第一個選單的項目而定
              var options = null;

              //清空第二個選單
              while(os2.firstChild){
                os2.removeChild(os2.firstChild);
              }

              //根據第一個選單的選項,將合適的選項值指派給第二個選單
              if(os.value == 'Windows'){
                options = ['7 Home Basic', '7 Home Premium', '7 Professional', '7 Ultimate', 'Vista', 'XP'];
              }else if(os.value == 'Mac OS X'){
                options = ['10.7 Lion', '10.6 Snow Leopard', '10.5 Leopard', '10.4 Tiger']
              }

              //如果第二組項目不為空(options 變數為真),則啟用第二個選單
              if(options){
                os2.disabled = false;
                //為選單增加新的選項,迴圈遍覽 options 陣列,可將所有的新選項增加到選單
                for(var i = 0, count = options.length; i < count; i++){
                  var opt = document.createElement('option');
                  opt.text = opt.value = options[i];        //將元素的 text 和 value 屬性填入目前陣列選項的內容
                  //上述等於 opt.text = options[i];  opt.value = options[i]; 的簡寫
                  os2.appendChild(opt);                     //新增到第二選單中
                }
              }else{
                //若因任何理由回到第一個選單挑選第一個選項(Choose),就得停用第二個選單,不顯示任何職
                //由於所有的項目都已被函數清除,如果使用者未選取 Windows 或 Mac OS X,option 就停用第二個選單
                os2.disabled = true;
              }
            }

            //定義初始化函數
            window.onload = function(){
              'use strict';
              //清除第一個選單選項,如果啟用 JavaScript,載入視窗就會利用更詳細的選單代替原始選單
              var os = document.getElementById('os');
              while(os.firstChild){
                os.removeChild(os.firstChild);
              }
              //增加新選項
              var options = ['Choose', 'Windows', 'Mac OS X'];
              for(var i = 0, count = options.length; i < count; i++){
                var opt = document.createElement('option');
                opt.text = opt.value = options[i];
                os.appendChild(opt);
              }
              //為選單新增一個修改事件處理器
              os.onchange = updateMenu;
              //練立第二個選單,指定 id 名稱,並放在原始選單旁邊
              var os2 = document.createElement('select');
              os2.id = 'os2';
              os2.disabled = true;
              os.parentNode.appendChild(os2);
            };
          

4核取方塊

和文字輸入欄位及文字區域不同,核取方塊無法輸入資料,只能切換狀態:選中或未選中,核取方塊的狀態可透過 checked 屬性取得:
          var which = document.getElementById('someCheckbox').checked;
        
透過指派 true(或 checked 字串)值給屬性的方式,便可利用程式選中核取方塊:
          which.checked = true;
        
value 屬性則存放核取方塊的值:
          if(which.checked){
            var value = document.getElementById('someCheckbox').value;
          }
        

核取方塊和 JavaScript 的用處之一,是在選中核取方塊時由 JavaScript 進行某種操作,例如,有些網站停用提交按鈕──阻止表單提交,直到使用者點擊核取方塊主動表示同意某些條款(毫無疑問,他從來不會閱讀條款):
          window.onload = function(){
            var termsBox = document.getElementById('termsCheckbox');
            termsBox.onclick = function(){
              document.getElementById('submit').disabled = false;
            }
          }
        
這是一個簡單而粗糙的版本,如果因為任何理由清除核取方塊,並不會重新停用提交按鈕,倘若希望得到更正式的版本,可以為核取方塊設定一個修改事件處理器,然後根據它的 checked 值啟用或停用提交按鈕。

核取方塊常常應用於電子商務站,提供使發貨地址與帳單地址相同的捷徑,可以明確監視修改事件,然後查看 checked 屬性:
          window.onload = function(){
            document.getElementById('sameAsBilling').onclick = copyBilling;
          }
          function copyBilling(){
            //應用在文字輸入欄位
            document.getElementById('shippingStreet1').value = document.getElementById('billingStreet1').value;
            //針對選單,只要兩個選單有相同順序的同樣選項
            document.getElementById('shippingState').selectIndex = document.getElementById('billingState').selectIndex;
          }
        

核取方塊最後一個常見用法是由一個核取方塊作為許多其他核取方塊的主角:
            //定義 toggleCheckboxes() 函數
            function toggleCheckboxes(){
              'use strict';
              //取得主核取方塊的 checked 值
              var status = document.getElementById('toggle').checked;
              //取得所有核取方塊,使用 querySelectorAll() 方法找出 type 屬性等於 checkbox 的 input 元素
              var boxes = document.querySelectorAll('input[type="checkbox"]');

              //用迴圈遍覽核取方塊,更新 checked 屬性
              for(var i = 1, count = boxes.length; i < count; i++){
                boxes[i].checked = status;      //在目前核取方塊的 checked 屬性填入 status 變數的值
              }
            }

            //建立視窗載入事件處理器
            window.onload = function(){
              'use strict';
              document.getElementById('toggle').onchange = toggleCheckboxes;
            }
          

5單選鈕

單選鈕在功能上與核取方塊類似,但有個限制:每個群組中只能選中一個按鈕
和核取方塊一樣,checked 屬性是一布林值,代表是否選中給定的元素,和核取方塊不同的是,我們無法取得特定單選鈕的參照,並找尋 checked 屬性,而得遍覽群組中的所有單選鈕,才能找到被選中的按鈕(就像單選選中多個選項時,必須遍覽所有選項一樣)。一組單選鈕的 name 屬性值必須相同──僅允許一個按鈕被選中,從定義上來說,id 屬性在頁面中必須是唯一的,解決方案之一是透過 getElementsByName() 方法,然後選種某個單選鈕時,可以透過 value 屬性抓取特定的選項值,而標誌變數能夠檢查使用者確實選取了至少一個單選鈕:
          var radios = document.getElementsByName('gender');
          var selected;
          for(var i = 0, count = radios.length; i < count; i++){
            if(radios[i].checked){
              selected = radios[i].value;
              break;
            }
          }
        
上述程式碼結束時,如果未選中任何單選鈕,selected 變數仍為 null,否則便是實際選中的值。

至於選單鈕的動態特效,所能做的和核取方塊相同,像是現代單選鈕最有趣的用途是許多網站提供的星狀評分系統,因為只能選擇一組單選鈕的其中一個,這對評分系統來說是種好做法(也可以使用選單),藉由增加一些 JavaScript 和 CSS,可以使乏味的按鈕變成星形,在使用者評分的時候提供良好的回饋,而且因為單選鈕屬於基本的功能,所以在所有瀏覽器都能很好地降級應用。

6檔案上傳

檔案輸入控制項提供一種途徑,好讓使用者從其電腦上傳檔案至伺服器,因為透過傳統的 HTML(與 HTML5 正好相反)無法為檔案輸入控制項帶來太多功能。(更多與檔案相關的動態功能可以用 Flash、Java 插件或 iframe 完成)
在 JavaScript 中所能做的就是存取檔案輸入控制項的標準 HTML 屬性,例如類型(className),此外還可以存取輸入控制項的 value 屬性,在使用者選擇要上傳的檔案之後,將會填寫該屬性。

不過,作為安全措施,檔案輸入控制項的 value 屬性會是 C:\fakepath\actualFileName.ext 之類的值,例如,在 Mac 上使用 Safari 時,Mac 沒有 C:\ 目錄,一旦選中 somefile.txt 後,輸入控制項的值變成 C:\fakepath\somefile.txt,此舉能夠妥善地阻止 JavaScript 了解使用者的檔案系統。同樣的,也可以藉由 JavaScript 驗證檔案輸入欄位至少有一個選中的檔案,不過,我們無法設定檔案輸入控制項的內容,此乃因為考慮到檔案系統存取權地缺乏,所以修改其值是不合邏輯的。檔案輸入控制項的樣式和行為在不同的瀏覽器和作業系統上有所差別,可利用 CSS 和 JavaScript 進行標準化,但是從 JavaScript 的角度來看,這麼做並沒有意義。

檔案輸入控制項觸發的事件(或許希望加以監視)有:change、focus、blur,有人認為檔案輸入控制項會觸發點擊事件,但實際上卻沒有,奇怪的是,可以在這個元素上呼叫 click() 方法:
          document.getElementById('fileInput').click();
        
在 firefox 以外的瀏覽器上,上述程式碼會開啟選擇檔案對話方塊,就像手動點擊輸入控制項一樣,Firefox 不會這麼做,這一點曾有激烈的爭論,有人認為,除非使用者的意圖明顯是選擇自己的電腦上的檔案,否則不應該使用上述程式──當然,它也可以自己點擊檔案輸入控制項來表達此意圖。

HTML5 可透過單個檔案輸入控制項上傳多個檔,並且擁有其他的檔案上傳功能,利用一些有趣的 JavaScript,便可在使用者選擇上傳檔案時動態建立新的檔案輸入控制項,藉以複製多檔上傳功能。

7常規運算式

常規運算式是大部份現代程式語言和許多應用程式使用的一種強大(而單調)的工具,可將其看作一個複雜的模式比對系統。首先撰寫一模式,然後以 JavaScript 的內建函數應用模式到一個值上(常規運算式適用於字串,即使字串內含數字時也是如此),字串函數將尋找某些文字中是否有姓名 John,而常規運算式也能同樣簡單地搜尋 John、Jon 和 Jonathon,常規運算式存在於大部份語言和技術中:只要學習一次,就可以在幾乎任何地方使用它!
雖然常規運算式的語法很難,但使用他們的函數卻很簡單,常規運算式有不同的類型,JavaScript 支援 Perl 相容的常規運算式(Perl-Compatible Reqular Expressions,PCRE),這是最常見的類型

什麼是常規運算式


常規運算式是一種模式到值的應用。在 JavaScript 中,模式本身由 RegExp 物件呈現,透過 RegExp 和 String 物件的不同方法,可將模式應用到任何需要的值上,不管其內容是使用者的資料還是來自於另一個伺服器。

作為一個常規運算式的實用例子,我們考慮難以驗證的值──有效的電子郵件帳號,在此研究完整的技術細節並沒有多大意義,電子郵件帳號的一般語法為「使用者名稱 @ 網域名稱」,其中有三個已知細節:1.位址中一定有 @,2.但不是第一個字元,3.也不是最後一個。可以利用 indexOf() 方法確認這三個特徵值,不過驗證電子郵件帳號的要求遠不止於此。

使用者名稱部分可能包含任何字母(不分大小寫),加上數字、破折號和句號等,但是不包含空格和某些字元,因此這部份的驗證要困難得多,網域名稱也可能包含相同的字串,但至少要有一個句點(例如 example.com)。
而且,在最後一個句號之後只能有 2~6 個字元,例如代表土耳其的 .tr 或者 .museum,理論上,我們可以透過 String 方法驗證所有條件,但是程式碼將十分費解而緩慢,常規運算式則能用一種模試驗證所有條件

舉更簡單的例子,考慮美國郵遞區號的格式:它可能是 5 個數字(12345)或者 5+4 個數字(12345-6789),兩者都是有效的格式,同一種常規運算式可以直接驗證這兩種格式。

建立常規運算式


和其他語言不同,JavaScript 的常規運算式是一種特殊類型,它與 Number、String 或 Date 一樣,和 JavaScript 大部份的值類型相同,建立常規運算式有兩種方式。第一種是首選方式──產生文字化的常規運算式,並把模式放在兩個斜線之間:
          var regexp = /pattern/;
        
首先要了解一點,模式兩端不用加上引號,例如比對美國郵遞區號的模式:
          var zip = /^\d{5}(-\d{4})?$/;
        
在結束斜線後,可以置放影響常規運算式行為的標誌,最容易理解的標誌為 i,它使模式不區分大小寫,因此下面三個模式都能驗證一個不分大小寫的字母:
          var letter = /[A-Za-z]/;
          var letter = /[A-Z]/i;
          var letter = /[a-z]/i;
        

第二種建立常規運算式的方法是產生一個 RegExp 類型的新物件,就像很少透過正式的 String 物件建立字串一樣,通常也很少產生 RegExp 類型的物件──這麼做會造成其他語法問題。

常規運算式函數


有了常規運算式物件之後,就能透過它進行模式和字串的比較,有四種方法可用於此目的: 假設已經建立一個常規運算式變數 r,並打算測試字串 s,則可使用如下的函數:
          r.test(s);
          r.exec(s);
          s.search(r);
          s.match(r);
        
上述程式碼呼叫 RegExp 方法時,將字串作為參數,呼叫 String() 方法時則將常規運算式作為參數。

整理如下: String 物件的 split() 方法也可以常規運算式作為參數,代表分隔符號,此舉將根據文字中找到的 HTML 元素分隔文字,String 物件的 replace() 方法也能以常規運算式找尋需要替換的資料。

定義簡單的模式


使用 JavaScript 的常規運算式函數很容易,但定義所用的模式則十分困難,建立模式有許多規則,可以單獨或合併使用這些規則,以便產生簡單或複雜的模式。
定義模式的第一類字元是文字(Literal),文字值完全是字面上的意思,例如,模式 a 比對的就是字母 a,模式 ab 匹配字串 ab,以此類推,因此,假設進行的不分大小寫的搜尋,rom 將比對如下的字串,因為它們都包含 rom: 除了文字之外,模式還能使用元字元(Meta-characters),元字元是特殊的符號,具有字面以外的意義,a 僅僅代表字母 a,而句點(.)將比對除了換行符號以外的任何單個字元(. 比對 a、b、c、底線與空格等,但不包括 \n),為了滿足元字元,必須先行轉義,就像列印前的先轉義引號一樣,因此,\. 代表句點,1.99 滿足 1.99 或者 1B99、1299(1 後面跟上任何字元,再加上 99),而 1\.99 只匹配 1.99。

元字元 意義
\ 轉義字元
^ 字串的開頭
$ 字串的結束
. 除了換行之外的單個字元
| 備選(或)
[ 類別的開始
] 類別的結束
( 子模式的開始
) 子模式的結束
{ 限定符號號的開始
} 限定符號號的結束

特定字元的位置由兩個元字元指定,^ 代表字串的開始,$ 則表示字串的結束。依此類推,^a 比對任何以 a 開頭的字串,而 a$ 對應以 a 結束的字串,因此,^a$ 僅僅對應至一個 a(一個以 a 開始和結束的字串),等同於:
          if(str == 'a'){
            ...
          }
        

^ 和 $ 對於驗證來說很關鍵,因為驗證通常要求檢查整個字串值,而不只是部分的內容,例如,不包含這兩個字元的電子郵件匹配模式,將比對任何包含電子郵件帳號的字串,而已 ^ 開始、$ 結束的電子郵件匹配模式,將搜尋僅包含有效電子郵件帳號的字串。
| 等同於「或」:a|b 比對包含 a 或 b 的字元(在模式中使用 | 稱作替代(Alternation)或分支(Branching)),所以 yes|no 接受 yes 或 no 這兩個完整的單字(替代比對的不是 s 和 n 之間的那個字元)。
領略基本的符號之後,就可以開始使用括號將字元組織成更複雜的模式,分組的工作方式與期待的一樣:「abc」比對 abc,「trout」比對 trout,括號用來建立更大規模的文字,根據 Perl 相容常規運算式的優先權規則,yes|no 等同於 (yes)|(no),但是(even|heavy\-)handed 則找出「evenhanded」或者「heavy-handed」。

使用簡單模式的步驟如下:
  1. 載入 Web 瀏覽器的控制台面板
  2. 檢查字串是否包含 cat 字母
    為此,請用 cat 字母做為模式,任何數量的字串為目標,這將比對下列任何一個字串:catalog、catastrophe、my cat left 等,此處暫時使用全小寫字母,所以 cat 不匹配 Cat
    記住,模式兩旁要加上分隔符號
  3. 檢查字串是否以 cat 開始
    使用 ^ 作為第一個字元「^cat」,以便建立適用於字串起始位置的模式,因此句子 my cat left 現在便不符合條件
  4. 檢查字串是否包含 color 或 colour
    查找 color 一詞的美國或英國的拼寫模式 col(o|ou)r,前三個字元──col──必須出現,後面緊跟著 o 或 ou,最後需要字母 r
如果打算精確比對一個字串(不需要任何的彈性),應該始終使用 indexOf() 方法或是相等比較,這兩種方法都要快得多。

使用限定符號


接下來的三個字元考慮了多次出現的情況:a* 將比對 0 到多個 a(不包含 a's、a、aa、aaa 等);a+ 匹配 1 或多的 a(a、aa、aaa 等,至少有一個 a);a? 則比對最多 1 個 a(a 或者沒有 a),這些元字元在模式中都可以作為限定符號,和大括號的作用相同。

限定符號 意義
? 0 或 1
* 0 或多個
+ 1 或多個
{x} 出現 x 次
{x,y} x 與 y 之間(含)
{x,} 至少出現 x 次

為了比對出現特定次數的內容,可於大括號({})內放入數量,指出具體次數、最小次數或最小與最大次數,因此,a{3} 比對 aaa;a{3,} 比對 aaa、aaaa 等;a{3,5} 只比對 aaa、aaaa 和 aaaaa(重複 3~5 次)。以大括號指定字元數量時,必須始終包含最小數量,最大數量則可有可無:可以接受 a{3} 和 a{3,},但是不能使用 a{,3}。
注意,限定符號適用於它之前的字元,所以 a? 比對 0 或者多個 a,ab? 比對 1 個 a 後面跟上 0 或 1 個 b,而 (ab)? 比對 0 或 1 個 ab,因此也可以利用模式 colou?r 匹配 color 或 colour。

使用簡單模式的步驟如下:
  1. 在瀏覽器中載入控制台面板
  2. 檢查一字串是否包含字母 c 和 t,兩個字母之間有一或多個字母
    答案是使用 c.+t 模式,以任意字串作為目標,記住,句點比對任何字串(除了換行符號之外),下列字串都符合條件:cat、count、coefficient 等,但不包含 doctor 一詞,因為 c 和 t 之間沒有字母
  3. 檢查一字串是否匹配 cat 或者 cats
    首先如果希望進行精確比對,就使用 ^ 或者 $,做法是採用文字 cat,後面跟上 s 和 ?(表示 0 或 1 個 s),最終形成的模式──^cats?──找出 cat 或者 cats,但是不包括 my cat left 或 I like cats。
  4. 檢查一字串是否以 .33、.333 或者 .3333 結束
    尋找句點必須以反斜線轉義:\.,3 則使用文字 3,為了找到一系列 3,可以採用大括號({}),這樣組合起來的模式為 \.3{2,4},由於字串到此結束(後面沒有其他內容),因此以 $ 結束模式:\.3{2,4}$
    很明顯,這類例子十分愚蠢(不知道什麼時候需要這麼做),不過確實說明了一些問題,上述模式將比對 12.333、vaarmit.3333、.33、look.33──而 12.3 或者 12.334 則不符合。
  5. 匹配 5 位數的數字
    數字表示 0~9 的一個數字,所以模式的核心是(0|1|2|3|4|5|6|7|8|9),簡單地說,這意味著:數字是一個 0 或一個 1、一個 2、一個 3...,為了組成 5 為數數字,可以在這個模式後面加上限定符號:(0|1|2|3|4|5|6|7|8|9){5},最後為了精確地比對(而不是匹配字串內的 5 位數數字),所以加上 ^ 和 $:^(0|1|2|3|4|5|6|7|8|9){5}$。
    這當然也是比對美國 5 位數郵遞區號的一種方法。
          
學習更多關於常規運算式的知識
由於建立常規運算是式的規則很複雜,目前只強調最重要的知識,其他像是逆向參照、順序環視、逆序環視以及其他瘋狂的技術等,尚可擴展模式本身。JavaScript 程式還有特殊的比對模式,例如,常規運算式可以匹配郵遞區號,同時將編碼分解成 5 位數和「附加的 4 位數數字」。此外,還可以將常規運算式作為替換功能:用其文字取代比對的模式,例如,上述觀念能夠動態地把 URL 轉換為作用中的 HTML 連結。

使用字元類別


如果列出所有數字來比對數值是很愚蠢的做法,如果希望匹配任何四個字母的單字,就得使用下列模式:^(a|b|c|d...){4}$(還沒有考慮大寫字母),因此可以透過字元類別簡化這些常見的參照。
將字元放在中括號([])內就可以建立類別,例如,以 [aeiou] 比對任何母音字母,等同於 (a|e|i|o|u),也可以用連字符號指出一系列字元:[a-z] 是任何一個小寫字母,[A-Z] 是任何大寫字母,[A-Z a-z] 是任何字母,[0-9] 代表任意數字,例如,[a-z]{3} 找出 abc、def、oiw 等。
類別中,除了四個字元外,其餘元字元都被當作文字看待,反斜線仍是轉義字元,當 ^ 放在類別的第一個字母時,代表否定運算子,因此 [^aeiou] 比對非母音字母,類別中僅有的另一個字元是 -,它代表範圍(如果 - 是最後一個字元,就代表字面意義上的 -),當然 ] 仍然是類別的結束符號。
          
性能問題
常規運算式奇妙而強大,但在程式執行時付出沉重的代價。為了將常規運算式對腳本性能的影響降到最低,最好只在絕對必要的時候使用常規運算式,多數情況下,字串方法就能完成常規運算式的工作,以客戶端的驗證來說,往往檢查字串的長度就夠了。使用常規運算式時,如果需要多次測試同一個腳本,可將模式指派給一變數(也就是建立一個常規運算式物件),如此便可優化瀏覽器的性能
智慧型的常規運算式還應該「快速失敗」,也就是說,無效值(例如不以 http 開頭的 URL)將很快被排除。
最後,替代(透過 |)對性能也有不利的影響,如果可能,請改用字元類別代替 |。在必須使用替代功能的情況下,儘可能減少字元數,這樣 JavaScript 在分析字串有效性時就不用考慮太多的可能性了。
自然,類別可以包含範圍與文字,人名可能有字母、空格、單引號和句點等,可用 [A-z '\.] 來表示,這個類別不需要轉義句號,因為在此並不帶有字元的含意,但是為了安全起見,一般寧願對可能出現問題的字元進行轉義。

除了自訂的類別之外,還能用捷徑代表六種預定的類別,數字和空格類別很容易理解,word 字元類別並非意味著語意上的「單字」,而是為被空格或標點符號打斷的字串,請注意,單字類別只在使用非重音的拉丁字母(A 到 Z)時有意義,其他不使用或不受限這一字母範圍的語言,或許採用其他字元組合來形成「單字」。

字元類別 捷徑 意義
[0-9] \d 任何數字
[\f\r\t\n\v] \s 任何空白
[A-Za-z0-9] \w 任何的單字字元
[^0-9] \D 非數字
[^\f\r\t\n\v] \S 非空白
[^A-Za-z0-9] \W 非單字字元

5 位數數字(也就是郵遞區號)模式可以更簡單地寫作 ^[0-9]{5}$ 或 ^\d{5}$,另舉一例,can\s?not 將比對 can not 和 cannot(單字 can,後面加上 0 或 1 個空格,然後是 not)。

使用字元類別的步驟如下:
  1. 在瀏覽器中載入控制台面板
  2. 檢查字串是否為有效的美國郵遞區號
    美國郵遞區號始終以 5 位數字(^\d{5})開始,另一種有效的郵遞區號是一個 - 加上額外 4 位數數字(-\d{4}$),如果加上 ?(表示 0 或 1 的限定符號),便使後半段成為可有可無,完整的模式變成 ^(\d{5})(-\d{4})?$,為了讓整個模式看上去更加清楚,第一部份(比對 5 位數數字)被集中在括號內,但是並非必要。
  3. 檢查一字串是否不包含空格
    字元類別捷徑方式 \S 比對任何非空格字元,為了確保整個字串都沒有空格,可採用 ^ 和 $:^\S$,如果不使用這些符號,整個模式便確認目標字元包含至少一個非空格字元。
  4. 驗證電子郵件帳號
    模式 ^[\w.-]+@[\w.-]+\.[A-Za-z]{2,6}$ 提供合理的電子郵件帳號驗證,由於透過 ^ 和 $ 封裝,因此符合的字串必然是一個有效的電子郵件位址,而不含其他內容,電子郵件以字母、數字和底線(由 \w 代表)開始,加上 . 和 -,模式的第一部份找出 larryullman、larry.ullman、larry-ullman 等字串,接下來,所有的電子郵件帳號都包含一個(而且僅有一個)@,@ 之後可以是任何字母、數字、句點、破折號,這一部份是網域名稱:larryullman、smith-jones、amazon.co(amazon.co.uk)等。
    最後,所以電子郵件帳號都已一個句點和 2~6 個字母作為結束,此為 .com、.edu、.info、.travel 等尾碼,這個電子郵件驗證模式相當不錯,但是還不完美,某些無效的位址(如以一個句點開始的位址,或者有好幾個連續句點的位址)會通過驗證。不過,萬無一失的驗證模式很長,使用常規運算式通常只是為了排除大部份的無效輸入,並保證不會由於疏忽而排除有效的輸入。
          
使用邊界
邊界(Boundary)是一種捷徑,用來協助尋找單字的範圍,在某種程度上,我們已經見過這種方式:以 ^ 字元和 $ 比對字串值的開始和結束部分。但是,如果希望在字串值內搜尋邊界:最清楚的邊界在單字與非單字之間,這裡所指的「單字」不是 cat、month 或 zeitgeist 之類的詞語,而 \W 的快捷方式:字母 A-Z(大寫和小寫)加上數字 0-9 與底線。\b 捷徑能夠將單字作為邊界,而 \B 則把非單字字元作為邊界,模式 \bfor\b 可以找到 they've come for you,但是無法匹配 force 或 forebode;而 \bfor\B 能找出 force,但是不匹配 they've come for you 或 informal。

8整合

將所有表單的相關資訊、建立錯誤訊息的功能和新學到的常規運算式知識整合在一起,設計一個完整範例:註冊頁面
            //定義 validateForm() 函數
            function validateForm(e) {
              'use strict';
              //該函數以事件作為參數,以便發生錯誤時阻止瀏覽器行為(提交表單給伺服器)
              if (typeof e == 'undefined') e = window.event;

              //取得表單元素參照
              var firstName = U.$('firstName');
              var lastName = U.$('lastName');
              var email = U.$('email');
              var phone = U.$('phone');
              var city = U.$('city');
              var state = U.$('state');
              var zip = U.$('zip');
              var terms = U.$('terms');

              //建立一個標誌變數,用來確認表單是否完全正常,一開始初始值為 false,表示沒有發生任何錯誤
            	var error = false;

              //驗證名字
            	if (/^[A-Z \.\-']{2,20}$/i.test(firstName.value)) {
            		removeErrorMessage('firstName');
            	} else {
            		addErrorMessage('firstName', 'Please enter your first name.');
            		error = true;
            	}

            	//驗證電子郵件
            	if (/^[\w.-]+@[\w.-]+\.[A-Za-z]{2,6}$/.test(email.value)) {
            		removeErrorMessage('email');
            	} else {
            		addErrorMessage('email', 'Please enter your email address.');
            		error = true;
            	}

            	//驗證電話號碼
            	if (/\d{3}[ \-\.]?\d{3}[ \-\.]?\d{4}/.test(phone.value)) {
            		removeErrorMessage('phone');
            	} else {
            		addErrorMessage('phone', 'Please enter your phone number.');
            		error = true;
            	}

            	//驗證所在的州別
            	if (state.selectedIndex != 0) {
            		removeErrorMessage('state');
            	} else {
            		addErrorMessage('state', 'Please select your state.');
            		error = true;
            	}

            	//驗證郵遞區號
            	if (/^\d{5}(-\d{4})?$/.test(zip.value)) {
            		removeErrorMessage('zip');
            	} else {
            			addErrorMessage('zip', 'Please enter your zip code.');
            		error = true;
            	}

                //如果發生錯誤,阻止預設行為
            	if (error) {
            	    if (e.preventDefault) {
            	        e.preventDefault();
            	    } else {
            	        e.returnValue = false;
            	    }
            	    return false;
            	}
            }

            //定義 toggleSubmit() 函數,偵測是否開啟提交按鈕
            function toggleSubmit() {
            	'use strict';
            	var submit = U.$('submit');

            	//勾選同意選項,啟用提交按鈕,否則便停用
            	if (U.$('terms').checked) {
            		submit.disabled = false;
            	} else {
            		submit.disabled = true;
            	}
            }

            //建立初始化功能
            window.onload = function() {
              'use strict';
              U.addEvent(U.$('theForm'), 'submit', validateForm);     //註冊一個表單提交事件處理器
              U.$('submit').disabled = true;                          //一開始應該停用提交按鈕
              U.addEvent(U.$('terms'), 'change', toggleSubmit);       //註冊一個 terms 核取方塊的修改事件處理器
              U.enableTooltips('phone');                              //啟動電話號碼工具提示
            };
          
DEMO MEMO