身為前端為什麼要懂正規表達式?

Thu Sep 02 2021

前端開發

我發現很多人其實都沒有真正的去了解過正規表達式,每當遇到一些像是 Email、Phone、姓名...等等格式驗證的問題時,就會直接去 Google 複製那一大串下來,自己也不知道到底有沒有問題、符不符合規範就直接使用了。

但我個人的經驗來說,其實前端開發的過程處理字串的機會其實非常多,而正規表達式 RegExp 就是處理字串的重要工具,就算沒有要到信手捻來的程度,也必須要有基本的閱讀能力才行。

就先聊聊為什麼要用正規表達式吧。

# 不使用正規表達式 RegExp VS 使用正規表達式 RegExp

可以思考看看,如果你需要驗證一段電話號碼,格式必須是 09xx-xxxxxx 時,如果沒有正規表達式,你可能會必須寫成這樣:

const phone = '0912-345678';

if(
    phone.length === 11 &&
    phone.slice(0,2) === '09' &&
    !isNaN(Number(phone.slice(2,4))) &&
    phone.slice(4,5) === '-' &&
    !isNaN(Number(phone.slice(5,11)))
) {
    alert('格式正確');
} else {
    alert('格式錯誤');
}

是不是覺得相當冗長麻煩呢?而且一但驗證需求改變,可能又要大手筆地進行調整。

再來我們就看看使用正規表達式的版本吧!

const phone = '0912-345678';
const phoneReg = /^09\d{2}-\d{6}$/;

if(phoneReg.test(phone)) {
    alert('格式正確');
} else {
    alert('格式錯誤');
}

可以看出使用正規表達式的版本在可讀性、維護性上都有明顯的提升對吧?

如果是完全沒有碰過正規表達式的朋友,可能會覺得上面這段 /^09\d{2}-\d{6}$/ 到底是什麼亂七八糟的亂碼。

不要急,我們現在就來簡單的說明一下正規表達式的基礎語法。

# 正規表達式的兩種宣告法

首先來談談怎麼宣告、建立正規表達式的 RegExp 物件吧!

# 建構器函式

透過 new 一個 RegExp 物件的方式,把 匹配條件旗標 分作兩個參數帶入,就可以成功的建立 RegExp 物件囉。

const reg = new RegExp('test', 'gi')

# 表達式實值

另一種寫法就是像我們平常宣告物件用 {},宣告陣列用 [],一樣的實值宣告,而 RegExp 的實值語法就是 //,把 匹配條件 輸入在兩個斜線之間, 旗標 則接續在第二條斜線之後。

如果還沒有輸入匹配條件之前,可是會看起來像註解呢。

下面的這個寫法,跟上面舉例的 new RegExp('test', 'gi') 是一樣的喔。

const reg = /test/gi

# 如何選擇用何種方式宣告

竟然有兩種宣告方式的話,就會讓人疑惑究竟哪種宣告方式比較好呢?

其實我個人在習慣上會統一使用 /test/gi 這樣的實值宣告,同時也因為實值宣告會在載入時被預先編譯處理,也會有更好的效能。

就像我們宣告物件其實也很少會看到 const obj = new Object() 這樣的寫法對吧?

但其實建構器函式的建立方式有他的不可取代性存在,那就是 匹配條件 需要納入變數的時候。

const needMatch = 'test'
const reg = new RegExp(needMatch, 'gi')

像這樣的宣告也是可以的喔,可以為正規表達式的條件增加許多的靈活性。

# RegExp Flags 旗標

旗標比起匹配條件,更像是一種「設定」的概念 ,幫你把你的 RegExp 變成你需要的形狀

旗標的種類有下面五種:

  • i:不區分大小寫
  • g:匹配所有符合的地方
  • m:允許多行文字
  • y:黏著性匹配
  • u:對 Unicode 進行跳脫

像上面舉例的 const reg = /test/gi 因為有 i 的旗標,所以不論是 Test 或是 teSt 都可以準確的匹配到。

而使用了 g 旗標,假設匹配字串多次出現也會進行多次匹配囉:

'test 啦啦啦 test'.replace(/test/gi, '哈哈哈') // '哈哈哈 啦啦啦 哈哈哈'
'test 啦啦啦 test'.replace(/test/i, '哈哈哈') // '哈哈哈 啦啦啦 test'

# 匹配條件的語法

# 多重條件的設定

當有想要設定多種匹配條件時,可以使用 [] 把各項要可以被匹配的字包進去組成一個條件,還可以透過用 - 來表達範圍,最常見的應該就是 [a-z]

  1. [要匹配的內容]
  2. [^不要匹配的內容]
  3. {要匹配多少字元}
  4. {最少匹配字元數, 最多匹配字元數}

# 常用符號

  • ? - 指定一個字元是可有可無
  • + - 代表至少該出現一次,可以出現多次
  • * - 代表可以多次出現,但也可能沒出現
  • . - 代表匹配所有字元
  • | - 多重規則選項
  • \ - 跳脫字元,使用時機為當需要直接匹配具有特別意義的符號時,如: \.$*?
  • ^ - 從起點開始匹配
  • $ - 從結尾開始匹配

如: /^test$/ 表示只能匹配 test 字串,前後不能有任何其它字元

/^[a-z]{1}$/i.test('a')
// true

/^[a-z]{1}$/i.test('aa')
// false

# 貪婪模式與不貪婪的差別

+ 這個符號代表了前方的條件最少要出現一次,但也可以很多次,如果用 /a+/ 來匹配 aaaaaaaaaa 的話,會因為預設的貪婪模式而達到最大匹配 aaaaaaaaaa,如果我們只想要匹配一個 a 就好的話,這時候就可以在 + 的後面補上 ? 變成 /a+?/,此時則只會匹配 a

'aaaaaaaaaa'.match(/a+?/)
// ['a', index: 0, input: 'aaaaaaaaaa', groups: undefined]

'aaaaaaaaaa'.match(/a+/)
// ['aaaaaaaaaa', index: 0, input: 'aaaaaaaaaa', groups: undefined]

# 常見預先定義詞彙

說明
\d 十進位數字:[0-9]
\D 十進位數字以外的字元:[^0-9]
\w 任何英文字母、數字包含下底線:[a-zA-Z0-9_]
\W 任何英文字母、數字、下底線以外的字元:[^a-zA-Z0-9_]
\s 任何空白字元(空格、Tab)
\S 空白字元以外的字元
\b 字元邊界
\B 非字元邊界

# 捕捉與分組

()內的判斷都會同時被視為捕捉和分組 捕捉是會將匹配結果暫存下來,以供後續透過 /\1/ 匹配或是以 $1$2 的方式用在字串取代的方法上

# 參考資料與工具

本文主要是提供一個比較入門基礎的部分,以常見的語法為主,如果想要更深入的了解 RegExp,可以到 MDN 上面去看更完整的說明。

正規表達式 - JavaScript | MDN (opens new window)

這邊也分享一個我在撰寫、測試正規表達式 RegExp 時常用的小工具: regex101: build, test, and debug regex (opens new window)

這個網站有提供很多不同語言的正規表達式環境,記得要切成 ECMAScript 喔!

# 進階小挑戰

最後,就來出個小題目給你,請嘗試理解看看下面這端 RegExp,為什麼可以達成這樣的效果吧!

也許有些難度、也許要花上一些時間,但如果能看懂並理解,我相信你在正規表達式的熟練度就已經有不錯的水準了,平常在開發上一定可以好好活用這個強大的武器。

'1234567890'.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
// '1,234,567,890'