JavaScript 字串操作 — 正規表達式(RegExp)
簡介
在日常的前端開發中,字串驗證、搜尋與取代是最常見的需求之一。雖然 String 內建的方法已能處理簡單的情境,但面對複雜的模式比對時,正規表達式(RegExp)則是不可或缺的利器。透過 RegExp,我們可以在單行程式碼內完成 email、電話號碼、網址等格式的驗證,或是一次抓取所有符合條件的子字串,極大提升程式的可讀性與效能。
本篇文章將從 RegExp 的語法基礎、實作範例、常見陷阱,一路帶到 實務應用,幫助初學者快速上手,同時提供中階開發者優化技巧,讓你在 JavaScript 中玩轉字串,寫出更健全的程式碼。
核心概念
1. RegExp 物件的建立
在 JavaScript 中,正規表達式有兩種建立方式:
// 方式一:字面量
const pattern1 = /abc/;
// 方式二:建構子
const pattern2 = new RegExp('abc');
字面量寫法較為直觀,且可直接加入 旗標(flags),如 i(不區分大小寫)、g(全域搜尋)與 m(多行模式):
// 不區分大小寫且全域搜尋
const emailRegex = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
2. 常用的元字符(Metacharacters)
| 元字符 | 說明 |
|---|---|
. |
匹配除換行符之外的任意單一字元 |
\d |
匹配任意數字,等價於 [0-9] |
\w |
匹配字母、數字或底線,等價於 [A-Za-z0-9_] |
\s |
匹配任意空白字元(空格、Tab、換行) |
^ |
錨點,表示字串開頭 |
$ |
錨點,表示字串結尾 |
* |
前面的子表達式出現 0 次或多次 |
+ |
前面的子表達式出現 1 次或多次 |
? |
前面的子表達式出現 0 次或 1 次 |
| ` | ` |
() |
捕獲群組,用於提取子字串或設定優先順序 |
[] |
字元集合,例如 [aeiou] 匹配任一母音 |
3. 旗標(flags)的意義
| 旗標 | 功能 |
|---|---|
g |
全域搜尋,返回所有匹配結果(matchAll、replace 會受影響) |
i |
不區分大小寫 |
m |
多行模式,^ 與 $ 會匹配每一行的起始與結束 |
s |
讓 . 可以匹配換行符(ES2018+) |
u |
以 Unicode 方式處理(支援 Emoji、補充平面) |
y |
粘性搜尋(只在 lastIndex 位置開始匹配) |
程式碼範例
以下示範 5 個在日常開發中常會用到的 RegExp 範例,並加上詳細註解說明。
1. Email 格式驗證
// Email 正規表達式(簡化版,實務上可依需求調整)
const emailPattern = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
/**
* 判斷字串是否為合法的 Email
* @param {string} str 輸入字串
* @returns {boolean}
*/
function isValidEmail(str) {
return emailPattern.test(str);
}
// 範例測試
console.log(isValidEmail('john.doe@example.com')); // true
console.log(isValidEmail('invalid_email@com')); // false
重點:使用
^與$錨點確保整個字串必須完整符合模式,i旗標讓大小寫不敏感。
2. 台灣手機號碼驗證
// 台灣手機號碼:09xx-xxxxxx 或 09xxxxxxxx
const twMobilePattern = /^09\d{2}-?\d{6}$/;
/**
* 檢查是否為有效的台灣手機號碼
*/
function isTaiwanMobile(num) {
return twMobilePattern.test(num);
}
console.log(isTaiwanMobile('0912-345678')); // true
console.log(isTaiwanMobile('0912345678')); // true
console.log(isTaiwanMobile('1234-567890')); // false
技巧:
-?表示「連字符可以出現也可以不出現」,讓兩種常見寫法都能匹配。
3. 從文字中抓取所有網址
// 簡易網址匹配(支援 http/https、www、子域名與路徑)
const urlPattern = /https?:\/\/(?:www\.)?[\w.-]+(?:\.[a-z]{2,})+(?:\/[\w./?%&=-]*)?/gi;
/**
* 從給定文字中找出所有網址
* @param {string} text
* @returns {Array<string>}
*/
function extractUrls(text) {
// matchAll 會返回 Iterator,使用展開運算子轉成陣列
return [...text.matchAll(urlPattern)].map(m => m[0]);
}
const sample = `
這裡有兩個連結:
1. https://developer.mozilla.org/zh-TW/
2. http://example.com/path?query=1
`;
console.log(extractUrls(sample));
// [ 'https://developer.mozilla.org/zh-TW/', 'http://example.com/path?query=1' ]
說明:使用
g旗標取得所有匹配,i讓協議部份不區分大小寫。
4. 替換字串中的多個空白為單一空格
// 匹配一個或多個連續的空白字元
const multiSpacePattern = /\s+/g;
/**
* 正規化字串空白
*/
function normalizeSpaces(str) {
return str.replace(multiSpacePattern, ' ').trim();
}
const messy = ' JavaScript 正規表達式 教學 ';
console.log(normalizeSpaces(messy)); // "JavaScript 正規表達式 教學"
提示:
trim()先移除首尾空白,再用replace把中間的多個空白壓縮成單一空格。
5. 使用捕獲群組(Capture Group)拆分日期
// 日期格式:YYYY/MM/DD 或 YYYY-MM-DD
const datePattern = /^(\d{4})[-\/](\d{2})[-\/](\d{2})$/;
/**
* 把日期字串分割成 [year, month, day]
* @param {string} dateStr
* @returns {Array<string>|null}
*/
function splitDate(dateStr) {
const match = datePattern.exec(dateStr);
if (!match) return null;
// match[1] = year, match[2] = month, match[3] = day
return [match[1], match[2], match[3]];
}
console.log(splitDate('2023-11-19')); // ["2023","11","19"]
console.log(splitDate('2023/11/19')); // ["2023","11","19"]
重點:
()形成捕獲群組,exec會回傳完整匹配與每個群組的內容,方便後續處理。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方法 |
|---|---|---|
忘記加錨點 (^、$) |
正則會匹配子字串,導致驗證不嚴格。 | 在需要完整匹配時務必加上 ^ 與 $。 |
全域旗標 g 與 test() 結合 |
test() 會改變 RegExp 的 lastIndex,連續呼叫可能得到錯誤結果。 |
每次使用前手動 regex.lastIndex = 0,或直接移除 g。 |
| 字元集合的「-」誤用 | [-a] 會被解析為範圍,可能產生預期外的匹配。 |
若要匹配 - 本身,置於集合最前或最後,或使用 \ 逃脫。 |
| Unicode 字元(Emoji)匹配失敗 | 默認正則只支援 BMP,對於補充平面會被切成兩個 surrogate。 | 加上 u 旗標,或使用 \p{Emoji}(需要支援的環境)。 |
| 過度複雜的單行正則 | 可讀性差,維護困難。 | 盡量拆成多個小正則或使用命名捕獲群組(ES2018+ (?<name>…))。 |
最佳實踐:
- 先寫測試:使用 Jest 或 Vitest 撰寫單元測試,確保正則行為符合預期。
- 保持簡潔:若正則超過 80 個字元,考慮拆解或加上註解說明。
- 使用
RegExp靜態方法:String.prototype.matchAll、replaceAll(ES2021)可提升可讀性。 - 避免硬編碼:將常用模式抽成常量或函式,提升重用性。
實際應用場景
| 場景 | 需求 | 正則解法 |
|---|---|---|
| 表單驗證 | Email、電話、身分證號碼 | 直接在 onSubmit 前使用 test() 或 match()。 |
| 文字搜尋 | 高亮關鍵字、搜尋結果分頁 | new RegExp(keyword, 'gi') 搭配 replace 包裝 <mark> 標籤。 |
| 日誌分析 | 從伺服器 log 抽取 IP、時間戳 | (\d{1,3}\.){3}\d{1,3} 抓取 IPv4,/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/ 抓時間。 |
| 內容過濾 | 移除 HTML 標籤、清除敏感字 | /\<[^>]*\>/g 移除所有 HTML,或 `/\b(word1 |
| 資料轉換 | 把 CSV 文字拆成陣列 | /,(?=(?:[^"]*"[^"]*")*[^"]*$)/ 處理含引號的欄位。 |
範例:在聊天室中即時高亮使用者輸入的關鍵字:
function highlight(text, keyword) {
const escKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // 逃脫特殊字元
const regex = new RegExp(`(${escKeyword})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
console.log(highlight('JavaScript 正規表達式很強大', '正規表達式'));
// => "JavaScript <mark>正規表達式</mark>很強大"
總結
正規表達式是 字串處理的萬能工具,在 JavaScript 中只要掌握以下三點,就能在日常開發中如虎添翼:
- 語法與旗標:熟悉元字符、錨點與常用旗標的意義,避免因忘記
^、$或g而產生錯誤匹配。 - 實作範例:透過實際的 Email、電話、URL、空白正規化與日期拆解等範例,了解
test、exec、matchAll、replace的使用差異。 - 防坑與最佳實踐:注意
lastIndex、Unicode、字元集合的寫法,並將正則抽成常量、寫測試、保持可讀性。
只要在開發過程中適時使用 RegExp,從 表單驗證、文字搜尋、日誌分析 到 資料清理,都能大幅減少程式碼量、提升效能,讓你的 JavaScript 應用更可靠、更易維護。祝你玩轉正規表達式,寫出更乾淨、更強大的程式!