讲解

转义解决「我想匹配一个元字符本身」的问题:在元字符前加反斜杠,它就还原成普通字符——. 是字面的点,* 是字面的星号,\ 匹配一个真正的反斜杠。方向不要搞反:元字符加 \ 变字面,普通字母加 \ 反而可能变成元字符(d 是字面字母,\d 是数字类)。

JavaScript 里有个独有的麻烦:正则可以用 /\d+/ 字面量写,也可以用 new RegExp('\d+') 从字符串构造。字符串本身也有一层转义——字符串里的 \ 会变成一个真正的反斜杠——所以用字符串构造时,正则需要的每个 \ 都要写成两个:new RegExp('\d+') 才等价于 /\d+/。什么时候需要字符串构造?模式包含运行时才能确定的变量时。

把用户输入直接拼进正则是隐蔽的 bug 源:用户输入 a.b 里的点会被当成通配符,输入 ( 甚至会让整个正则语法错误。正确做法是先对输入做「元字符消毒」:把所有元字符前面加反斜杠,再拼进模式。这个 escapeRegExp 小函数几乎是每个项目的必备工具函数。

示例

// 匹配元字符本身:加反斜杠
console.assert(/1\+1/.test('1+1=2'));
console.assert(/\(\d{3}\)/.test('(010) 是区号格式'));
console.assert(/\/path\/to/.test('/path/to/file')); // 字面量里的 / 也要转义
console.assert(/\$9\.99/.test('标价 $9.99'));

// 匹配反斜杠本身:正则里的 \\ 表示一个 \
const win = 'C:\\Users\\name'; // 字符串里的两个 \ 是一个真正的反斜杠
console.assert(/C:\\Users\\name/.test(win));

字符串构造 new RegExp:双层转义与变量拼接。

// 字符串构造时,正则要的每个 \ 在字符串里都要写成两个
const re1 = /\d+/;
const re2 = new RegExp('\\d+');
console.assert(re2.test('abc123'));
console.assert(re1.source === re2.source); // 两种方式造出的模式完全相同

// 模式来自变量时,必须用字符串构造
const keyword = 'cat';
const dynamic = new RegExp('\\b' + keyword + '\\b');
console.assert(dynamic.test('my cat sleeps'));
console.assert(dynamic.test('category') === false);

用户输入拼进正则前的消毒:

// escapeRegExp:把所有元字符转义,让输入只按字面匹配
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const input = 'a.b(c)';
const safe = new RegExp(escapeRegExp(input));
console.assert(safe.test('x a.b(c) y')); // 转义后按字面匹配
console.assert(safe.test('x axb(c) y') === false);

// 不消毒的后果:点号成了通配符,(c) 成了分组
const unsafe = new RegExp(input);
console.assert(unsafe.test('axbc') === true); // 误匹配!

常见坑

  • 字符串构造忘双层转义:new RegExp('\d') 里的 \d 在字符串层就退化成了字母 d,正则看到的是普通字符 d,匹配数字永远失败。
  • 方向搞反:\ 加在元字符前是「变字面」,加在普通字母前可能「变元字符」(\d \w \s \b)。不要给所有字符随手加反斜杠。
  • 原样拼用户输入:轻则点号通配带来误匹配,重则未闭合括号让整个正则抛 SyntaxError。凡是含外部输入的模式,先过 escapeRegExp。
  • 在字符类里过度转义:[.] 能跑但没必要——类内大多数字符本来就是字面量,[.] 即可。需要转义的只有 ] \ ^(首位)和 -(中间位置)。

小结

\ 让元字符还原为字面字符;new RegExp 字符串构造要双层转义;拼接外部输入前必须用 escapeRegExp 消毒。下一节看控制整体行为的标志位。