1850 字
9 分鐘

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"); // true
containsKeyword("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"); // true
matchesHost("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.",
);
}

正式專案有兩個選擇:

  1. 將支援矩陣提高到提供 RegExp.escape() 的 runtime。
  2. 依專案的 browser/Node 版本,載入經維護、經測試和資安審查的 polyfill,並把它加入相容性測試。

不要在 fallback 裡臨時補一個只處理 .、* 和 ? 的 replace。它可能在今天的範例通過,卻在連字號、slash、開頭字元或 Unicode 輸入上改變語意。

可重複的最小驗證#

升級 Node.js、瀏覽器或 polyfill 後,可以用一個包含不同邊界的字串做 smoke test:

Terminal window
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 當成搜尋引擎。

用斷言檢查完整字面值與 Unicode 邊界#

只看 escape 輸出,還不能確認組合後的 pattern 符合需求。以下可存成 escape-check.mjs,在提供 RegExp.escape() 的 Node 環境執行 node escape-check.mjs:

import assert from 'node:assert/strict';
const samples = [
'foo-bar.*', 'a+b/c', '中文😀',
'line\nbreak', '\u2028', '\uD800', '',
];
for (const value of samples) {
const pattern = new RegExp('^(?:' + RegExp.escape(value) + ')$', 'u');
const match = pattern.exec(value);
assert.equal(match?.[0], value);
assert.equal(pattern.test('x' + value), false);
assert.equal(pattern.test(value + 'x'), false);
}
assert.equal(new RegExp(RegExp.escape('a+b'), 'u').test('aaab'), false);
assert.equal(new RegExp(RegExp.escape('Sale'), 'u').test('sale'), false);
assert.equal(new RegExp(RegExp.escape('Sale'), 'iu').test('sale'), true);
console.log('escape checks passed');

本次維護在 Node.js v24.13.0 執行這組斷言,全部通過;沒有測試瀏覽器或外部 polyfill。測試涵蓋空字串、換行、emoji、行分隔符與 lone surrogate,也確認正則語法 + 沒有變成量詞。MDN 與 ECMAScript 規格 說明了這些字元的 escaping 規則。

這裡用 u 旗標檢查原始大小寫;前面的搜尋框範例使用 iu,因此會接受大小寫變體。escape 決定輸入是字面值,flags 決定匹配方式。 若需求是整段輸入完全相同,除了固定前後邊界,還應確認匹配結果覆蓋完整字串;若加上 m 旗標,^ 與 $ 會改成逐行邊界,可能只匹配多行輸入中的其中一行,不能拿它宣稱整段字串完全相等。只需字串相等時直接用 === 更清楚。

若同一個 lazy-loaded 搜尋功能還要處理載入失敗,可另參考 動態 import 的有限重試;不要把模組載入錯誤和搜尋輸入錯誤共用一套修復邏輯。

結論#

當需求是「把外部字串當成正則表達式裡的 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 執行時間、資源耗用或固定語法組合後的回溯風險。仍要設長度上限並測試最壞情況。

參考資料:

MDN:RegExp.escape()

ECMAScript Specification:RegExp.escape

MDN:multiline 旗標

RegExp.escape() 怎麼用?動態建立 JavaScript 正則表達式先處理輸入
https://laplusda.com/posts/javascript-regexp-escape-guide/
作者
Zero
發佈於
2026-09-19
許可協議
CC BY-NC-SA 4.0