身為前端為什麼要懂正規表達式?
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]
[要匹配的內容]
[^不要匹配的內容]
{要匹配多少字元}
{最少匹配字元數, 最多匹配字元數}
# 常用符號
?
- 指定一個字元是可有可無+
- 代表至少該出現一次,可以出現多次*
- 代表可以多次出現,但也可能沒出現.
- 代表匹配所有字元|
- 多重規則選項\
- 跳脫字元,使用時機為當需要直接匹配具有特別意義的符號時,如:\.$*?
^
- 從起點開始匹配$
- 從結尾開始匹配
如: /^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 上面去看更完整的說明。
這邊也分享一個我在撰寫、測試正規表達式 RegExp 時常用的小工具: regex101: build, test, and debug regex (opens new window)
這個網站有提供很多不同語言的正規表達式環境,記得要切成 ECMAScript 喔!
# 進階小挑戰
最後,就來出個小題目給你,請嘗試理解看看下面這端 RegExp,為什麼可以達成這樣的效果吧!
也許有些難度、也許要花上一些時間,但如果能看懂並理解,我相信你在正規表達式的熟練度就已經有不錯的水準了,平常在開發上一定可以好好活用這個強大的武器。
'1234567890'.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
// '1,234,567,890'