Skip to content

JavaScript 正则表达式 — W3C 标准视角

本文从 ECMAScript 规范与浏览器实现角度梳理 JavaScript 正则表达式,区别于基础语法讲解,重点关注规范定义的内部机制、Symbol 方法协议以及与浏览器环境的交互。


1. RegExp 在 ECMAScript 规范中的定义

1.1 规范结构

ECMAScript 规范(tc39.es/ecma262)将 RegExp 定义为内置对象,其行为由 RegExp 构造函数、RegExp.prototype 以及多个内部槽(Internal Slots) 决定。核心章节为 "21.2 RegExp (Regular Expression) Objects"

关键内部槽:

内部槽含义
[[OriginalSource]]正则模式原始字符串(含斜杠分隔符外的全部内容)
[[OriginalFlags]]原始 flags 字符串
[[RegExpMatcher]]执行匹配的内部算法实现
[[LastIndex]]上一次匹配结束的位置(用于 g/y flag)
[[Groups]]具名捕获组元信息(ES2018+)

1.2 字面量语法解析

js
/pattern/flags

字面量语法 /.../词法扫描器(Scanner) 在解析 JavaScript 源码时处理:

  1. 遇到 / 后进入 PatternToken 状态
  2. 持续读取字符直到遇到匹配的结束 /
  3. 期间对 \ 进行特殊处理(转义),但不解释正则元字符
  4. 结束后读取 flags(允许的字符:d, g, i, m, s, u, v, y
  5. 构造 RegExp 对象,[[OriginalSource]] 保存原始模式字符串

规范细节:字面量解析发生在语法分析阶段,比构造函数 new RegExp() 的解析(运行时)更早,且不执行字符串转义。

1.3 构造器 vs 字面量

字面量 /pattern/flagsnew RegExp(pattern, flags)
解析时机编译时(源码扫描)运行时
pattern 类型字面量源码字符字符串
\ 转义不解释(保留原样)需双写 \\ 转义
相同模式对象共享(规范要求共享字面量对象,浏览器通常复用)每次 new 创建新对象

2. String.prototype 与正则的交互(规范算法)

ECMAScript 规范为每个 String 方法定义了与 RegExp 交互的标准算法,通过 Well-Known Symbol 触发内部方法。

2.1 核心 Symbol 方法对应关系

Symbol 方法触发来源返回值
RegExp.prototype[@@match]String.prototype.match()Array 或 null
RegExp.prototype[@@replace]String.prototype.replace()替换后字符串
RegExp.prototype[@@search]String.prototype.search()匹配起始索引
RegExp.prototype[@@split]String.prototype.split()分割后的字符串数组
RegExp.prototype[@@matchAll]String.prototype.matchAll()可迭代的 Match 对象

2.2 String.prototype.match() 算法(规范 §21.1.3.10)

text
String.prototype.match(regexp)
1. If regexp is undefined or null, return String(this) match against /\/*s*/.
2. Let rx be ? ToObject(regexp).
3. Let matcher be ? GetMethod(rx, @@match).
4. Return ? matcher.call(rx, this).

关键点:

  • 如果传入的不是对象(或没有 @@match 方法),规范不会自动回退到正则字面量匹配
  • @@match 方法的存在本身作为正则身份标识:若 @@match === undefined(非正则对象),某些操作会抛出 TypeError

2.3 String.prototype.replace()@@replace

@@replace 算法(规范 §21.1.3.17)核心流程:

  1. 从左到右执行所有匹配(g flag)
  2. 对每个匹配调用 Replacer Algorithm:解析 $1, $<name> 等替换占位符
  3. 捕获组结果通过 MatchRecord 传递
js
// 具名捕获组在 replace 中的使用
"2026-04-01".replace(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u, 
  "$<day>/$<month>/$<year>"
)
// "01/04/2026"

3. Unicode 模式(u flag)与 Code Point

3.1 UTF-16 与 JavaScript 字符串

JavaScript 字符串采用 UTF-16 编码

  • Basic Multilingual Plane(BMP,U+0000–U+FFFF):1 个代码单元 = 1 个代码点
  • 增补平面(Supplementary,U+10000+):2 个代码单元(surrogate pair)
js
"😄".length        // 2 — 是两个 UTF-16 代码单元
[..."😄"].length   // 1 — 展开后是 1 个代码点

3.2 u flag 的规范效果

启用 u flag 后,正则引擎行为发生根本变化:

行为u flagu flag
. 匹配粒度UTF-16 代码单元Unicode 代码点
\u{xxxx} 语法语法错误匹配代码点 U+xxxx
\p{...} Unicode 属性语法错误匹配 Unicode 属性
字符类范围按代码单元排序按代码点排序
量词作用范围1 个代码单元1 个代码点
js
// 无 u flag:超出 BMP 的字符被截断处理
/./.test("😄")    // false — . 不匹配 0xD83D(surrogate half)

// 有 u flag:正确匹配整个表情符号
/./u.test("😄")   // true — . 匹配代码点 U+1F604

3.3 \p{...} Unicode 属性逃逸(ES2018+)

需要 u flag 才能使用:

js
// 匹配所有 Unicode 字母(含中文)
/\p{L}/u.test("中")  // true
/\p{L}/u.test("A")   // true

// 匹配 Unicode 数字
/\p{N}/u.test("5")   // true(全角数字)

// 匹配 Emoji
/\p{Emoji}/u.test("😄") // true

// 匹配 Unicode 脚本(Script)
/\p{Script=Han}/u.test("乐")  // true — 汉字
/\p{Script=Latin}/u.test("a") // true

3.4 v flag(ES2024)

v flag 是 u 的扩展,支持:

  • 集合操作[[a-z]&&[m-p]] 交集、[[a-z]--[m-p]] 差集
  • 字符串属性:如 \p{Script_Extensions=Hiragana}
js
// v flag 下可以使用字符串属性和集合操作
/\p{RGI_Emoji_Set}/v.test("😄")  // true

注意:uv 不能同时使用,会报 SyntaxError


4. lastIndex、Sticky(y)与 Global(g)flag

4.1 lastIndex 内部槽

lastIndexRegExp.prototype 的一个可写属性,映射到内部槽 [[LastIndex]]。它只在 gy flag 存在时对匹配行为产生影响。

4.2 g flag — 全局匹配

g flag 下,@@match(对应 String.prototype.match())会循环调用 RegExp.prototype.exec(),直到返回 null

js
const re = /ab/g;
re.lastIndex = 0;  // 初始化
const str = "_ab_ab_";
const matches = [];
let result;
while ((result = re.exec(str)) !== null) {
  matches.push(result[0]);
  // lastIndex 自动推进到上一次匹配结束位置
}
console.log(matches); // ["ab", "ab"]

关键规范行为

  • exec() 成功 → lastIndex 设为 matchEnd
  • exec() 失败 → lastIndex 重置为 0
  • match() 调用 @@match,内部按同样逻辑处理

4.3 y flag — Sticky(粘滞)匹配

y flag 保证匹配严格从 lastIndex 位置开始,不向前扫描:

js
const re = /ab/y;
re.lastIndex = 1;
re.test("_ab_")  // true — 从 index 1 开始匹配
re.test("_ab_")  // false — lastIndex=3,但字符串[3]="_",无法匹配"ab"

4.4 gy 的组合

规范规定:当 gy 同时存在时,exec() 的行为优先遵循 sticky 语义(即只从 lastIndex 位置匹配,不循环):

js
const re = /ab/gy;
re.lastIndex = 1;
re.exec("_ab_ab_")  // null — index 1 是 "_",不是 "ab" 的起点
re.lastIndex = 3;
re.exec("_ab_ab_")  // ["ab", index: 3] — 从 index 3 匹配

4.5 lastIndex 推进规则

规范 §21.2.2.2(RegExpBuiltinExec)定义的推进规则:

匹配结果lastIndex 行为
成功,非空匹配推进到 endIndex(匹配内容末尾)
成功,空匹配u flag → 推进 1 个代码点;无 u → 推进 1 个代码单元
失败重置为 0

5. 具名捕获组(Named Capture Groups / NFE)

5.1 规范背景

ES2018 引入 Named Capture Groups,语法 (?<name>pattern)。结果对象通过 groups 属性访问:

js
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
const result = re.exec("2026-04-01");
result.groups        // { year: "2026", month: "04", day: "01" }
result.groups.year   // "2026"

5.2 @@replace 中的具名反向引用

js
// 交换年月日顺序
"2026-04-01".replace(
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u,
  "$<day>.$<month>.$<year>"
)
// "01.04.2026"

5.3 具名反向引用 \k<name>

在模式内部引用具名组(Named Backreference):

js
// 匹配带引号的字符串(单引号或双引号)
/(?<quote>["'])(?<content>.*?)\k<quote>/.exec('"hello"')
// ["\"hello\"", '"', 'hello', index: 0, ...]

5.4 浏览器兼容性

特性ChromeFirefoxSafariEdge
(?<name>...) 具名捕获组64+78+11.1+79+
groups 属性64+78+11.1+79+
\k<name> 具名反向引用64+78+11.1+79+
$<name> 替换占位符64+78+11.1+79+

来源:MDN & Can I Use。IE 全系不支持。移动端 iOS Safari 11.1+、Android Chrome 64+ 均已支持。


6. RegExp.prototype[@@match] 内部槽协议

6.1 Symbol 作为内部方法

ECMAScript 规范使用 Well-Known Symbols 表示内部方法的钩子:

js
RegExp.prototype[Symbol.match]    // 规范定义的内部 [[Match]] 方法
RegExp.prototype[Symbol.replace]  // 规范定义的内部 [[Replace]] 方法
RegExp.prototype[Symbol.search]   // 规范定义的内部 [[Search]] 方法
RegExp.prototype[Symbol.split]    // 规范定义的内部 [[Split]] 方法
RegExp.prototype[Symbol.matchAll]  // 规范定义的内部 [[MatchAll]] 方法

6.2 Symbol 方法的意义

  1. 子类化:可以继承 RegExp 并覆盖这些 Symbol 方法改变匹配行为
  2. 身份标识Symbol.match 属性用于判断对象是否"像正则"
  3. 正则化对象:让非 RegExp 对象也能参与正则协议
js
// 自定义一个"正则化"对象
const obj = {
  [Symbol.match](str) {
    return str.includes("foo") ? ["foo"] : null;
  }
};
"hello-foo-world".match(obj)  // ["foo"]

6.3 @@matchAll 与 Match Iterator

ES2020 引入 Symbol.matchAll,返回 Match Iterator

js
const re = /(\w)-(\d)/g;
const str = "a-1 b-2 c-3";
for (const match of str.matchAll(re)) {
  console.log(match[0], match[1], match[2], match.groups);
}
// "a-1" "a" "1" { undefined, undefined }
// (无命名组则 groups 为 undefined)

注意:matchAll 要求正则必须带 g flag,否则抛出 TypeError


7. 浏览器的 RegExp 兼容性(Can I Use)

以下为主要 RegExp 特性的浏览器支持情况:

特性ChromeFirefoxSafariEdgeIE
基本 RegExp✅ 1+✅ 1+✅ 1+✅ 12+✅ 5.5+
lastIndex(g/y)✅ 39+✅ 25+✅ 8+✅ 12+
u flag✅ 50+✅ 29+✅ 10+✅ 79+
s flag (dotAll)✅ 62+✅ 78+✅ 11.1+✅ 79+
具名捕获组✅ 64+✅ 78+✅ 11.1+✅ 79+
断言 (?<=) (?<!)✅ 62+✅ 78+✅ 11.1+✅ 79+
v flag (ES2024)✅ 122+✅ 127+✅ 17.4+✅ 122+
\p{...} Unicode 属性✅ 64+✅ 78+✅ 11.1+✅ 79+
d flag (indices)✅ 126+✅ 126+✅ 16.4+✅ 126+

d flag:ES2022 引入,给匹配结果添加 indices 数组,表示每个捕获组在原字符串中的起始/结束位置。


8. 实际应用:从 W3C 角度理解

8.1 URL 正则的陷阱

解析 URL 时,按 W3C URL 规范(WHATWG URL Standard),正则处理需要考虑:

js
// ❌ 常见错误:假设 URL 只有一个"."或不含 Unicode
// /https?:\/\/[^\/]+/.test("https://example.com/") 

// ✅ 正确处理 Unicode 域名和路径
const URL_REGEX = /^(?<protocol>https?):\/\/(?<host>[\w.-]+)(?<path>\/.*)?$/u;

// ✅ 匹配百分号编码的 Unicode
/%[0-9A-Fa-f]{2}/.test("https://example.com/%E4%B8%AD%E6%96%87")

W3C 角度:URL 的合法字符集在 WHATWG URL Standard 中定义,正则只是验证工具。真正的 URL 解析应使用 new URL() API,而非纯正则。

8.2 HTML 标签匹配

js
// 匹配自闭合标签(SVG、MathML 等)
const SELF_CLOSING_TAG = /<([a-z][a-z0-9-]*)\s*([^>]*)\/?>/gi;

// 匹配带命名空间的标签
const NS_TAG = /<([a-zA-Z][a-zA-Z0-9]*):([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*)>/gi;

// 匹配 HTML 注释(条件注释不适用)
const COMMENT = /<!--[\s\S]*?-->/g;

规范注意:HTML 规范中的标签名必须遵循 HTML5 标签名规则,正则只能做近似验证。

8.3 CSS 选择器转正则(简化)

很多场景需要将 CSS 选择器转为正则进行 DOM 查询:

js
/**
 * 将简单 CSS 选择器转为正则(支持 class、id、属性选择器)
 * 仅用于理解原理,实际应使用 DOM API(querySelector)
 */
function cssToRegex(selector) {
  let pattern = selector
    // 转义正则特殊字符
    .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
    // class 选择器 .foo → 匹配 class 属性中的 foo
    .replace(/\.([a-zA-Z_-]+)/g, '(?:.*\\s)?$1(?:\\s|$)')
    // id 选择器 #foo
    .replace(/#([a-zA-Z_-]+)/g, '(?:id=["\']?)$1(?:["\']?\\s|$)');

  return new RegExp(`^${pattern}`, 'i');
}

cssToRegex('.card')    // /^ (?:.*\s)?card(?:\s|$) /i
cssToRegex('#header')  // /^ (?:id=["\']?)header(?:["\']?\s|$) /i

8.4 表单验证(实际业务场景)

js
// 符合 W3C HTML5 验证规范的输入模式
const patterns = {
  // 邮箱(简化版,真实场景用 type="email")
  email: /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zZ0-9-]{0,61}[a-zA-Z0-9])?)*$/,

  // URL(简化版)
  url: /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/,

  // 中国手机号(宽松匹配)
  phoneCN: /^(?:\+86)?1[3-9]\d{9}$/,

  // IPv4 地址
  ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
};

9. RegExp.prototype.exec() 规范算法要点

规范 §21.2.2.2(RegExpBuiltinExec)定义了 exec() 的标准行为:

  1. 初始化lastIndexthis.lastIndex 读取
  2. 位置校验:如果 lastIndex > str.length,执行失败,重置 lastIndex0,返回 null
  3. 匹配尝试:从 lastIndex 位置开始尝试模式匹配
  4. Sticky 检查:若 sticky flag 存在且匹配位置 ≠ lastIndex,返回 null
  5. 结果记录:匹配成功后记录 startIndexendIndex,并填充捕获组
  6. 推进lastIndex 设为 endIndex
  7. 空匹配处理:若匹配长度为 0,推进 1 个字符单位(见 §4.5 规则)
js
// 演示空匹配推进行为
const re = /a?/g;      // 匹配 "a" 或 空串
const r = /a?/gu;      // 同上,但空匹配按代码点推进
"abc".match(re)        // ["a", "", "b", "", "c", ""] — 6 个结果

10. 参考资料