RegExp.escape() 怎麼用?動態建立 JavaScript 正則表達式先處理輸入
只要把使用者輸入、搜尋關鍵字或外部設定放進 new RegExp(),就不能把它當成普通字串直接串接。輸入裡的 ., *, (、反斜線或 / 可能改變正則表達式的語意;某些看似只是標點的字元,放在錯誤位置還可能造成 syntax error。
現在 JavaScript 有專門的 RegExp.escape():它把字串轉成可以安全放進正則表達式、代表「字面值」的片段。 最常見的用法是:
const keyword = "price.*(sale)";const pattern = new RegExp(RegExp.escape(keyword), "iu");
pattern.test("PRICE.*(SALE)"); // true這裡的重點是「把輸入當 literal 搜尋」,不是讓使用者輸入正則語法。若產品真的要讓使用者撰寫 regex,應另做語法限制、錯誤處理與資源上限,不能用 RegExp.escape() 取代那套設計。
為什麼手寫 replace 不夠
很多舊範例會用一段 replace 把 . * + ? ^ $ { } ( ) | [ ] \ 前面加反斜線。這類程式碼常漏掉 /、連字號、換行或其他需要在不同 regex context 特別處理的字元,也容易在字串前面再接上 \1、\u 等 escape 時產生歧義。
RegExp.escape() 的規則不只是「每個特殊字元前面加反斜線」:
| 輸入情況 | 轉換目的 |
|---|---|
| 正則語法字元與 / | 避免被解讀成 pattern syntax 或 literal delimiter |
| 字串開頭的 ASCII 英文字母或數字 | 用 \x 表示,避免和前面既有的 escape 串在一起 |
| - 等 punctuator | 使用 \x 形式,避免某些 context 中無法只靠反斜線安全表示 |
| 空白、控制字元與 lone surrogate | 依規則轉成可放入 pattern 的表示 |
因此,不要把輸出長相當成「醜陋但多餘」。例如:
RegExp.escape("foo-bar.*");// "\\x66oo\\x2dbar\\.\\*"前面的 f 變成 \x66,連字號變成 \x2d,這正是 API 要避免相鄰 escape 改變語意的地方。
動態搜尋的完整寫法
假設頁面有一個搜尋框,需求是找出包含使用者輸入的文字;輸入應該只代表 literal,不應該變成 regex:
function containsKeyword(text, keyword) { const pattern = new RegExp(RegExp.escape(keyword), "iu"); return pattern.test(text);}
containsKeyword("Release v26.9.0", "v26.9.0"); // truecontainsKeyword("a+b", "a+b"); // true如果要組合一小段固定 regex 語法,也只逃逸變動的部分,不要把整個 pattern 都包起來:
function matchesHost(url, host) { const literalHost = RegExp.escape(host); const pattern = new RegExp( "^https?://" + literalHost + "(?=/|$)", "i", ); return pattern.test(url);}
matchesHost("https://example.com/docs", "example.com"); // truematchesHost("https://exampleXcom/docs", "example.com"); // false這種寫法讓 ^https?:// 和 lookahead 保留程式設計者定義的語意,只有 host 被視為外部 literal。若 host 也來自不可信輸入,仍需在 URL parser、允許的 domain 與 regex 三個層次分別驗證。
encodeURIComponent() 不能代替 RegExp.escape()
encodeURIComponent() 的目標是 URI component,不是 regex pattern。它會產生 % 編碼,無法保證輸入在 new RegExp() 裡代表原字串;反過來,regex escape 也不能拿來做 URL encoding。
const keyword = "a+b/c";
new RegExp(RegExp.escape(keyword), "u"); // 搜尋 literal "a+b/c"encodeURIComponent(keyword); // URL 使用的編碼,不是 regex escape兩者要依資料會進入的語法層次分開處理。若一段輸入先進 URL、再進 regex,應先設計清楚每一層的資料邊界,不要重複或錯用另一種 encoder。
舊環境的 feature detection 與 fallback
RegExp.escape() 是較新的標準 API。若程式需要跑在尚未提供它的 runtime,先做 feature detection:
function escapeRegexLiteral(value) { if (typeof RegExp.escape === "function") { return RegExp.escape(value); }
throw new Error( "RegExp.escape() is unavailable; load an approved polyfill before building a pattern.", );}正式專案有兩個選擇:
- 將支援矩陣提高到提供 RegExp.escape() 的 runtime。
- 依專案的 browser/Node 版本,載入經維護、經測試和資安審查的 polyfill,並把它加入相容性測試。
不要在 fallback 裡臨時補一個只處理 .、* 和 ? 的 replace。它可能在今天的範例通過,卻在連字號、slash、開頭字元或 Unicode 輸入上改變語意。
可重複的最小驗證
升級 Node.js、瀏覽器或 polyfill 後,可以用一個包含不同邊界的字串做 smoke test:
node -e 'const value = "foo-bar.*"; console.log(typeof RegExp.escape, RegExp.escape(value));'預期在支援的 runtime 中會得到 function,以及類似 \x66oo\x2dbar.* 的輸出。接著再測試:
- 輸入開頭是 ASCII 英文字母或數字。
- 輸入包含 -、/、\、.、*、?、( 和 )。
- 輸入包含換行、emoji、非 ASCII 字元與 lone surrogate(若應用程式會接收這些資料)。
- pattern 放在固定語法前後時,仍只匹配 literal,而不會擴大範圍。
這個檢查只能確認 API 和基本輸出,不能代替應用程式的資源限制。若 pattern 仍可能很大,或輸入與固定語法組合後可能造成高成本回溯,還是要設長度限制、測試 worst case,並考慮避免把 regex 當成搜尋引擎。
結論
當需求是「把外部字串當成正則表達式裡的 literal」時,優先使用 RegExp.escape(),再把結果放進 new RegExp()。它不只是替特殊字元加反斜線,也處理開頭 ASCII 字元、punctuator、slash 與 Unicode 邊界。舊環境先做 feature detection,必要時使用經審查的 polyfill;不要用 encodeURIComponent() 或不完整的手寫 replace 代替。
常見問題
Q: RegExp.escape() 會讓輸入變成可以執行的 regex 嗎?
A: 它做相反的事:把輸入轉成 literal 片段,讓輸入中的 regex 語法字元不再改變 pattern。若產品要支援使用者撰寫 regex,不能先 escape 再期待語法保留。
Q: 為什麼第一個字母會變成 \x66?
A: 標準為字串開頭的 ASCII 英文字母或數字採用 \x 表示,避免它和 pattern 前面已有的數字或 escape 序列相鄰時被解讀成同一個 escape。這不是多餘的格式化,而是降低組合 pattern 時的歧義。
Q: 只要呼叫 RegExp.escape() 就不用限制輸入長度了嗎?
A: 不用。Escape 解決的是語法與字面值邊界,不會替你處理超長輸入、regex 執行時間、資源耗用或固定語法組合後的回溯風險。仍要設長度上限並測試最壞情況。
參考資料:
回報錯字、失效連結,或告訴我你想看的延伸主題。