正则表达式
模式 Patterns 和修饰符 flags
正则表达式
由模式和修饰符(可选)两部分组成
创建语法(动态):
let regexp = new RegExp("pattern", "flags");
// 示例,可以动态创建字符串模板
let regexp = new RegExp(`<${tag}>`);
创建语法(静态):
斜线
/.../告诉 JavaScript 正在创建一个正则表达式。它的作用与字符串的引号作用相同但是此种方式不允许表达式插入,因此适用于固定的字符串模板
let regexp = /pattern/; // 没有修饰符
let regexp = /pattern/gmi; // 带有修饰符 g、m 和 i
修饰符
正则表达式中的修饰符(flags)用于改变匹配行为。JavaScript 支持以下 6 个修饰符:
-
i(ignore case):忽略大小写。例如,/a/i能匹配"A"或"a"。
示例:console.log(/hello/i.test("Hello World")); // true -
g(global):全局搜索。不加g时,仅返回第一个匹配项;加上后会查找所有匹配项(常用于match()、replace()等方法)。
示例:const str = "cat, bat, sat"; console.log(str.match(/at/)); // ["at"](仅第一个) console.log(str.match(/at/g)); // ["at", "at", "at"](全部) -
m(multiline):多行模式。使^和$分别匹配每一行的开头和结尾,而不仅限于整个字符串的首尾。
示例:const text = "First line\nSecond line"; console.log(text.match(/^Second/m)); // ["Second"](匹配第二行开头) -
s(dotAll):启用“点号匹配所有”模式。默认情况下,.不匹配换行符(如\n),使用s修饰符后,.可以匹配任意字符,包括换行符。
示例:const str = "a\nb"; console.log(/a.b/.test(str)); // false(. 不匹配 \n) console.log(/a.b/s.test(str)); // true(. 匹配 \n) -
u(unicode):启用完整 Unicode 支持。能正确处理代理对(如 emoji)和 Unicode 属性类(如\p{Letter}),确保在处理非 BMP 字符时行为正确。
示例:// 正确匹配 emoji(由两个 UTF-16 单元组成) console.log(/😀/.test("😀")); // true(即使不用 u 也可能工作) console.log(/./u.test("😀")); // true(u 修饰符下 . 匹配整个 emoji) // 使用 Unicode 属性类(必须带 u) console.log(/\p{Emoji}/u.test("😀")); // true -
y(sticky):粘滞模式。从正则表达式的lastIndex属性指定的位置开始精确匹配,且不向前搜索。若该位置无匹配,则立即失败(与g不同,y不会跳过字符寻找匹配)。
示例:const str = "abcabc"; const regex = /abc/y; regex.lastIndex = 3; // 从索引 3 开始 console.log(regex.exec(str)); // ["abc"](匹配位置 3 的 "abc") regex.lastIndex = 1; console.log(regex.exec(str)); // null(位置 1 不是 "abc" 开头)
字符串匹配方法
JavaScript 提供了多个将正则表达式与字符串结合使用的方法,其中最常用的是 str.match、str.replace 和 regexp.test。
str.match(regexp)
在字符串 str 中查找与正则表达式 regexp 匹配的内容。根据是否使用 g 修饰符,行为有所不同:
-
带
g修饰符:返回所有匹配项组成的数组(不包含捕获组信息)。let str = "We will, we will rock you"; console.log(str.match(/we/gi)); // ["We", "we"] -
不带
g修饰符:返回一个包含第一个匹配项的数组,该数组还带有额外属性:result[0]:完整匹配项result.index:匹配位置result.input:原始字符串
let str = "We will, we will rock you"; let result = str.match(/we/i); console.log(result[0]); // "We" console.log(result.index); // 0 console.log(result.input); // "We will, we will rock you" -
无匹配时:无论是否带
g,均返回null(不是空数组!)。let matches = "JavaScript".match(/HTML/); // null // 安全做法:提供默认空数组 matches = "JavaScript".match(/HTML/) || []; if (!matches.length) { console.log("No matches"); }
注意:如果正则表达式包含捕获组(括号),不带
g的match返回的数组还会包含捕获内容(如result[1],result[2]等)。
str.replace(regexp, replacement)
用 replacement 替换 str 中匹配 regexp 的部分:
- 若
regexp带g修饰符,则替换所有匹配项; - 否则仅替换第一个匹配项。
示例:
// 仅替换第一个
console.log("We will, we will".replace(/we/i, "I")); // "I will, we will"
// 替换全部
console.log("We will, we will".replace(/we/ig, "I")); // "I will, I will"
在 replacement 中使用特殊符号:
| 符号 | 含义 |
|---|---|
$& | 整个匹配项 |
$` | 匹配项之前的字符串部分 |
$' | 匹配项之后的字符串部分 |
$n | 第 n 个捕获组(n 为 1–2 位数字) |
$<name> | 名为 name 的命名捕获组 |
$$ | 插入字面量 $ |
示例:
console.log("I love HTML".replace(/HTML/, "$& and JavaScript"));
// 输出: "I love HTML and JavaScript"
console.log("Price: $10".replace(/\$(\d+)/, "¥$1"));
// 输出: "Price: ¥10"
regexp.test(str)
检查字符串 str 中是否存在至少一个匹配项,返回 true 或 false。
示例:
let str = "I love JavaScript";
let regexp = /LOVE/i;
console.log(regexp.test(str)); // true
该方法常用于条件判断,简洁高效。
这些方法构成了 JavaScript 中正则表达式与字符串交互的核心。后续学习捕获组、前瞻断言等高级特性时,会进一步扩展它们的能力。
字符类
字符类(Character classes)是一种特殊符号,用于匹配特定集合中的任意一个字符。它们在处理如电话号码、格式化文本等实际任务中非常有用。
常用字符类
-
\d(digit):匹配任意一位数字(0–9)。"+7(903)-123-45-67".match(/\d/g); // ["7", "9", "0", "3", ..., "7"] -
\s(space):匹配任意空白字符,包括空格、制表符\t、换行符\n、回车\r、换页\f、垂直制表符\v等。"a\t\n b".match(/\s/g); // ["\t", "\n", " "] -
\w(word):匹配“单词字符”——即拉丁字母(a–z, A–Z)、数字(0–9)或下划线_。不包括非拉丁字母(如中文、西里尔字母等)。"hello_123".match(/\w/g); // ["h","e","l","l","o","_","1","2","3"]
这些字符类可与普通字符混合使用:
"CSS4".match(/CSS\d/); // ["CSS4"]
"I love HTML5!".match(/\s\w\w\w\w\d/); // [" HTML5"]
反向字符类
每个字符类都有对应的反向形式(用大写字母表示),匹配不属于该类的任意字符:
\D:非数字(等价于[^\d])\S:非空白字符(等价于[^\s])\W:非单词字符(等价于[^\w])
应用示例:从电话号码中提取纯数字:
let str = "+7(903)-123-45-67";
// 方法一:提取所有数字
str.match(/\d/g).join(''); // "79031234567"
// 方法二:删除所有非数字
str.replace(/\D/g, ""); // "79031234567"
点号 .:匹配“任何字符”
-
默认情况下,
.匹配除换行符\n外的任意单个字符。"Z".match(/./); // ["Z"] "CS-4".match(/CS.4/); // ["CS-4"] "CS4".match(/CS.4/); // null(缺少中间字符) -
与换行符匹配:默认
.不匹配\n:"A\nB".match(/A.B/); // null -
使用
s修饰符(dotAll 模式):使.能匹配包括换行符在内的任何字符。"A\nB".match(/A.B/s); // ["A\nB"]注意:IE 浏览器不支持
s修饰符。 -
兼容性替代方案:使用
[\s\S]、[\d\D]或[^]表示“任意字符”:"A\nB".match(/A[\s\S]B/); // ["A\nB"]
空格的重要性
正则表达式对空格敏感——空格本身就是一个字符,必须显式处理:
"1 - 5".match(/\d-\d/); // null(未考虑空格)
"1 - 5".match(/\d - \d/); // ["1 - 5"](显式包含空格)
"1 - 5".match(/\d\s-\s\d/); // ["1 - 5"](使用 \s 更灵活)
关键提示:在正则表达式中,所有字符都重要,包括空格。忽略空格是常见错误来源。
Unicode:修饰符“u”和类\p{...}
JavaScript 使用 Unicode 编码字符串。大多数常见字符(如英文字母、数字)使用 2 字节表示,但一些特殊字符(如数学符号 𝒳、表情 😄 或汉字)需要 4 字节(称为“代理对”,surrogate pairs)。在没有正确处理的情况下,这些字符会被误认为是两个独立的 2 字节字符。
例如:
console.log('😄'.length); // 2(实际应为 1)
console.log('𝒳'.length); // 2
默认情况下,正则表达式也会将这类 4 字节字符错误地视为两个字符,导致匹配行为异常。修饰符 u(Unicode 模式) 可以解决这个问题,并启用对完整 Unicode 特性的支持,包括 Unicode 属性类 \p{...}。
启用 Unicode 模式:修饰符 u
只有在正则表达式中使用 u 修饰符时,才能:
- 正确识别 4 字节 Unicode 字符;
- 使用
\p{...}和\P{...}(反向)进行基于 Unicode 属性的匹配。
let str = "A ბ ㄱ";
// 没有 u 修饰符:无法识别 \p{L}
console.log(str.match(/\p{L}/g)); // null
// 有 u 修饰符:正确匹配所有语言的字母
console.log(str.match(/\p{L}/gu)); // ["A", "ბ", "ㄱ"]
Unicode 属性类:\p{Property}
每个 Unicode 字符都有一组属性,描述其类别、用途、书写系统等。通过 \p{...} 可匹配具有特定属性的字符。
常见通用类别(General Categories)
| 类别 | 含义 | 子类示例 |
|---|---|---|
L(Letter) | 字母 | Ll(小写)、Lu(大写)、Lo(其他字母,如中文) |
N(Number) | 数字 | Nd(十进制数字 0–9)、Nl(字母数字,如罗马数字) |
P(Punctuation) | 标点 | Po(其他标点)、Pd(连字符) |
S(Symbol) | 符号 | Sc(货币符号)、Sm(数学符号) |
Z(Separator) | 分隔符 | Zs(空格) |
M(Mark) | 附加符号(如重音) | Mn(非间距标记) |
C(Other) | 其他 | Cc(控制字符)、Cs(代理对) |
可使用全名(如
\p{Letter})或缩写(如\p{L})。
示例:匹配小写字母
"Hello 世界".match(/\p{Ll}/gu); // ["e", "l", "l", "o"]
高级属性:Script(书写系统)
通过 Script=<value> 可匹配特定书写系统的字符:
\p{sc=Han}:中文(汉字)\p{sc=Cyrillic}:西里尔字母\p{sc=Greek}:希腊字母\p{sc=Latin}:拉丁字母
示例:提取中文字符
let str = "Hello Привет 你好 123";
console.log(str.match(/\p{sc=Han}/gu)); // ["你", "好"]
实用示例
1. 匹配十六进制数字(x 后跟两位 hex)
let regexp = /x\p{Hex_Digit}\p{Hex_Digit}/u;
console.log("number: xAF".match(regexp)); // ["xAF"]
2. 匹配货币符号 + 数字
let regexp = /\p{Sc}\d+/gu; // \p{Sc} = Currency Symbol
let str = "Prices: $2, €10, ¥99";
console.log(str.match(regexp)); // ["$2", "€10", "¥99"]
注意:
\p{Sc}是\p{Currency_Symbol}的缩写。
重要提示
- 必须使用
u修饰符,否则\p{...}会抛出语法错误。 - 反向匹配使用
\P{...}(大写 P),表示“不具有该属性”。 - 完整属性列表可参考:
通过 u 修饰符和 \p{...},JavaScript 正则表达式能够真正实现国际化文本处理,准确识别任意语言、符号或书写系统的字符。
锚点:字符串开始^与末尾$
在正则表达式中,插入符号 ^ 和美元符号 $ 被称为锚点(anchors)。它们不匹配任何实际字符,而是用来断言字符串的特定位置。
^:匹配字符串的开始位置。$:匹配字符串的结束位置。
基本用法
检查字符串是否以某内容开头
let str = "Mary had a little lamb";
console.log(/^Mary/.test(str)); // true
等价于 str.startsWith("Mary"),但正则适用于更复杂的模式。
检查字符串是否以某内容结尾
let str = "it's fleece was white as snow";
console.log(/snow$/.test(str)); // true
等价于 str.endsWith("snow")。
完全匹配验证
将 ^ 和 $ 组合使用,可确保整个字符串完全符合指定模式,常用于表单验证:
let timeRegexp = /^\d\d:\d\d$/;
console.log(timeRegexp.test("12:34")); // true
console.log(timeRegexp.test("12:345")); // false(多出一个数字)
console.log(timeRegexp.test("2:34")); // false(小时位不足两位)
注意:若省略
^或$,正则可能匹配字符串中间的部分,导致误判。
零宽特性
锚点是零宽度断言(zero-width assertions):它们只检查位置条件,不消耗字符,也不包含在匹配结果中。
常见零宽字符/断言表格
| 符号 | 名称 | 说明 |
|---|---|---|
^ | 行首锚点 | 匹配字符串或行的开始(受 m 修饰符影响) |
$ | 行尾锚点 | 匹配字符串或行的结束(受 m 修饰符影响) |
\b | 单词边界 | 匹配单词与非单词字符之间的位置(如字母与空格之间) |
\B | 非单词边界 | 匹配不在单词边界的位置 |
(?=...) | 正向先行断言 | 匹配后面紧跟 ... 的位置,但不包含 ... |
(?!...) | 负向先行断言 | 匹配后面不紧跟 ... 的位置 |
(?<=...) | 正向后行断言 | 匹配前面紧邻 ... 的位置(ES2018+) |
(?<!...) | 负向后行断言 | 匹配前面不紧邻 ... 的位置(ES2018+) |
所有上述结构均不捕获字符,仅用于位置判断,因此称为“零宽”。
锚点 ^ $ 的多行模式,修饰符 "m"
默认情况下,正则表达式中的锚点 ^ 和 $ 仅匹配整个字符串的开始和结束位置。但通过启用 修饰符 m(multiline),它们的行为会发生变化:
^不仅匹配字符串开头,还匹配每一行的开头(即每个换行符\n之后的位置);$不仅匹配字符串末尾,还匹配每一行的末尾(即每个换行符\n之前的位置)。
行首匹配:^
let str = `1st place: Winnie
2nd place: Piglet
3rd place: Eeyore`;
// 启用多行模式:匹配每行开头的数字
console.log(str.match(/^\d/gm)); // ["1", "2", "3"]
// 无多行模式:仅匹配整个字符串开头
console.log(str.match(/^\d/g)); // ["1"]
在多行模式下,
^匹配:
- 字符串的最开始;
- 每个
\n换行符之后的位置。
行尾匹配:$
let str = `Winnie: 1
Piglet: 2
Eeyore: 3`;
// 多行模式:匹配每行末尾的数字
console.log(str.match(/\d$/gm)); // ["1", "2", "3"]
// 无多行模式:仅匹配整个字符串末尾的数字
console.log(str.match(/\d$/g)); // ["3"]
在多行模式下,
$匹配:
- 每个
\n换行符之前的位置;- 字符串的真正末尾(即使末尾没有
\n)。
与 \n 的区别
使用 \n 和使用 $/^ 有本质不同:
\n是一个实际字符,会被包含在匹配结果中;^和$是零宽锚点,仅表示位置,不消耗字符。
示例对比:
let str = `Winnie: 1
Piglet: 2
Eeyore: 3`;
console.log(str.match(/\d\n/g)); // ["1\n", "2\n"] —— 只有前两行,且包含 \n
console.log(str.match(/\d$/gm)); // ["1", "2", "3"] —— 三行都匹配,不含 \n
注意:最后一行末尾没有
\n,因此\d\n无法匹配3,但\d$可以,因为$能匹配字符串真正的结尾。
使用建议
- 若需在每行开头或结尾查找内容(如解析日志、配置文件),使用
^/$+ 修饰符m; - 若需提取包含换行符的片段,使用
\n; - 锚点适用于位置判断,而
\n适用于字符匹配。
词边界:\b
词边界 \b 是一种零宽断言,用于匹配“单词边界”的位置。它不消耗任何字符,仅检查当前位置是否满足“一边是单词字符(\w),另一边不是”的条件。
什么是词边界?
一个位置被视为词边界(\b)当且仅当满足以下任一情况:
- 字符串开头,且第一个字符是单词字符(
\w); - 字符串末尾,且最后一个字符是单词字符(
\w); - 两个相邻字符之间,其中一个是
\w,另一个不是(例如字母与空格、标点、数字与非数字等)。
单词字符
\w仅包括:
- 拉丁字母
a–z、A–Z- 数字
0–9- 下划线
_
不包括中文、西里尔字母、表情符号等非拉丁字符。
基本用法示例
匹配完整单词
"Hello, Java!".match(/\bJava\b/); // ["Java"]
"Hello, JavaScript!".match(/\bJava\b/); // null("Java" 是 "JavaScript" 的一部分)
多个边界验证
"Hello, Java!".match(/\bHello\b/); // ["Hello"] —— H 前是开头(边界),o 后是逗号(非 \w)
"Hello, Java!".match(/\bHell\b/); // null —— "ll" 后仍是字母 "o",无边界
"Hello, Java!".match(/\bJava!\b/); // null —— "!" 不是 \w,其后无法形成边界
匹配独立的两位数
"1 23 456 78".match(/\b\d\d\b/g); // ["23", "78"]
"12,34,56".match(/\b\d\d\b/g); // ["12", "34", "56"] —— 逗号是非 \w,构成边界
重要限制:仅适用于 \w 字符集
由于 \b 的判断基于 \w(即 [a-zA-Z0-9_]),它无法正确处理非拉丁文字:
// 西里尔字母(俄语)
"привет мир".match(/\bмир\b/); // null(因为 "м" 不属于 \w)
// 中文
"你好世界".match(/\b世界\b/); // null(汉字不属于 \w)
在这些语言中,所有字符都被视为“非单词字符”,因此
\b无法在其间识别边界。
使用建议
- 适用于英文、数字、下划线组成的标识符、关键词或代码片段的精确匹配;
- 不适用于国际化文本(如中文、阿拉伯语、俄语等);
- 若需处理 Unicode 单词边界,需借助其他技术(如 Intl.Segmenter API),正则表达式原生
\b无法胜任。
词边界 \b 是确保匹配“完整单词”而非“子串”的关键工具,但务必注意其对字符集的限制。
转义,特殊字符
在正则表达式中,某些字符具有特殊含义,用于构建模式结构。这些特殊字符包括:
[ ] { } ( ) \ ^ $ . | ? * +
此外,在 JavaScript 中使用字面量语法 /.../ 定义正则时,斜杠 / 也需特别处理,因为它用于界定正则表达式的开始和结束。
转义:让特殊字符“变普通”
若想匹配这些字符的字面值(即当作普通字符),必须在其前加反斜杠 \ 进行转义。
示例:匹配点号 .
"Chapter 5.1".match(/\d\.\d/); // ["5.1"] —— \. 表示字面点号
"Chapter 511".match(/\d\.\d/); // null
示例:匹配括号 ()
"function g()".match(/g$$/); // ["g()"]
示例:匹配反斜杠 \
"1\\2".match(/\\/); // ["\"] —— 需用 \\ 表示一个字面反斜杠
示例:匹配斜杠 /
在字面量语法中,/ 是分隔符,因此需转义:
"/".match(/\//); // ["/"]
若使用
new RegExp构造函数,则无需转义/:"/".match(new RegExp("/")); // ["/"]
使用 new RegExp() 时的双重转义问题
当通过字符串创建正则(new RegExp("pattern"))时,字符串本身会先解析转义序列,这可能导致正则失效。
错误示例:
let reg = new RegExp("\d\.\d");
console.log("Chapter 5.1".match(reg)); // null
原因:字符串 "\d\.\d" 在 JavaScript 中被解释为 "d.d",因为 \d 和 \. 在字符串中无特殊含义,反斜杠被丢弃。
正确做法:对反斜杠进行双重转义
let regStr = "\\d\\.\\d"; // 字符串内容为 \d\.\d
let regexp = new RegExp(regStr);
console.log("Chapter 5.1".match(regexp)); // ["5.1"]
规则:在
new RegExp的字符串参数中,每个\必须写成\\。
总结:何时转义?
| 场景 | 是否需要转义特殊字符 | 是否需要转义 / |
|---|---|---|
字面量 /.../ | ✅ 是 | ✅ 是(因 / 是分隔符) |
new RegExp("...") | ✅ 是(且需双重转义 \) | ❌ 否 |
始终记住:
- 正则中的特殊字符 → 用
\转义; - 字符串中的反斜杠 → 用
\\表示一个\; - 字面量中的
/→ 用\/转义。
集合和范围 [...]
方括号 [...] 定义一个字符集合,用于匹配其中任意一个字符。它可以包含具体字符、字符范围或预定义字符类。
基本集合
[eao] 表示匹配 'e'、'a' 或 'o' 中的任意一个:
"Mop top".match(/[tm]op/gi); // ["Mop", "top"]
集合只匹配一个字符,因此 V[oi]la 能匹配 "Vola" 或 "Vila",但不能匹配 "Voila"。
字符范围
使用连字符 - 表示连续范围:
[a-z]:小写英文字母[0-9]:数字[0-9A-F]:十六进制字符
"Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g); // ["xAF"]
可组合多个范围,如 [0-9A-Fa-f]。
与字符类结合
可在集合中使用预定义类:
[\w-]:单词字符或连字符[\s\d]:空白字符或数字
常见简写:
\d≡[0-9]\w≡[a-zA-Z0-9_]\s≡ 空白字符(含空格、换行等)
注意:\w 仅支持拉丁字母,不包括中文、西里尔字母等。
多语言支持(Unicode)
使用 Unicode 属性构建通用单词字符集:
let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu;
"Hi 你好 12".match(regexp); // ["H","i","你","好","1","2"]
需启用修饰符 u。IE 不支持 \p{...},可改用特定语言范围(如 [а-я])或使用 XRegExp 库。
排除集合:[^...]
开头加 ^ 表示排除:匹配不在集合中的字符。
[^aeiou]:非元音字母[^0-9]≡\D[^\s]≡\S
"alice15@gmail.com".match(/[^\d\sA-Z]/gi); // ["@", "."]
转义规则(在 [...] 内)
大多数特殊字符在方括号内无需转义:
.、+、(、)等直接表示字面值- 连字符
-在开头或结尾时无需转义(如[-abc]或[abc-]) ^仅在开头有特殊含义(表示排除),其他位置为普通字符- 右方括号
]必须转义:$$
let reg = /[-().^+]/g;
"1 + 2 - 3".match(reg); // ["+", "-"]
转义非必需字符(如 [\-$$\.\^\+])也不会出错。
代理对与修饰符 u
对于 emoji、数学符号等代理对字符(如 𝒳),必须使用修饰符 u:
'𝒳'.match(/[𝒳𝒴]/); // 错误:返回半个字符
'𝒳'.match(/[𝒳𝒴]/u); // 正确:返回 "𝒳"
范围同样受影响:
'𝒴'.match(/[𝒳-𝒵]/u); // 成功
'𝒴'.match(/[𝒳-𝒵]/); // 报错:Invalid regular expression
处理非 BMP Unicode 字符时,务必添加 u 修饰符。
量词 +, *, ? 和 {n}
量词用于指定一个字符、字符类或子表达式应重复多少次。它们是构建灵活且强大的正则表达式的关键。
精确数量:{n}
在元素后加上 {n} 表示恰好匹配 n 次。
"I'm 12345 years old".match(/\d{5}/); // ["12345"]
若要确保匹配的是完整数字(而非更长数字的一部分),可结合词边界:
"123456".match(/\b\d{5}\b/); // null(因为 123456 是六位)
范围数量:{n,m}
{3,5}:匹配 3 到 5 次{3,}:匹配至少 3 次(无上限)
"I'm not 12, but 1234 years old".match(/\d{3,5}/); // ["1234"]
"I'm 345678 years old".match(/\d{3,}/); // ["345678"]
应用于电话号码提取:
let str = "+7(903)-123-45-67";
str.match(/\d{1,}/g); // ["7", "903", "123", "45", "67"]
常用量词缩写
+(一个或多个)≡ {1,}
str.match(/\d+/g); // 同上,结果相同
?(零个或一个)≡ {0,1} —— 表示“可选”
"color colour".match(/colou?r/g); // ["color", "colour"]
*(零个或多个)≡ {0,}
"100 10 1".match(/\d0*/g); // ["100", "10", "1"]
"100 10 1".match(/\d0+/g); // ["100", "10"] —— "1" 不匹配,因需至少一个 0
实际应用示例
匹配小数(浮点数)
"0 1 12.345 7890".match(/\d+\.\d+/g); // ["12.345"]
注意:此模式要求整数和小数部分都至少有一位数字。
匹配 HTML 标签(无属性)
-
基础版(仅字母):
"<body> ... </body>".match(/<[a-z]+>/gi); // ["<body>"] -
更符合 HTML 规范(首字符为字母,后续可含数字):
"<h1>Hi!</h1>".match(/<[a-z][a-z0-9]*>/gi); // ["<h1>"] -
同时匹配开闭标签:
"<h1>Hi!</h1>".match(/<\/?[a-z][a-z0-9]*>/gi); // ["<h1>", "</h1>"]其中
\/?表示可选的斜杠/,需转义为\/以避免与正则字面量的/冲突。
精确性 vs 简洁性
正则表达式越精确,通常越复杂:
<\w+>简洁,但可能匹配非法标签(如<1div>,因\w包含数字和下划线)<[a-z][a-z0-9]*>更严格,符合 HTML 标签名规范(首字符必须是字母)
在实际开发中,可根据上下文选择:
- 若输入可控或后续可过滤,可用简单模式;
- 若需高可靠性(如解析不可信 HTML),应使用更严格的规则。
量词使正则能灵活适应不同长度的文本结构,是实现高效模式匹配的核心工具。
贪婪量词和惰性量词
正则表达式中的量词默认采用**贪婪(greedy)策略,即尽可能多地匹配字符。但在某些场景下,我们需要惰性(lazy)**匹配——尽可能少地匹配。理解这两种模式对编写正确正则至关重要。
贪婪搜索(默认行为)
量词 +、*、?、{n,m} 默认都是贪婪的。它们会先尝试匹配尽可能多的字符,只有在后续模式无法匹配时才逐步回溯(减少匹配长度)。
问题示例:替换引号
目标:将 "witch" 和 "broom" 分别替换为 «witch» 和 «broom»。
错误正则:/".+"/g
let str = 'a "witch" and her "broom" is one';
str.match(/".+"/g); // ['"witch" and her "broom"'] ❌
原因分析:
- 引擎找到第一个
"(位置 2) .+贪婪匹配所有后续字符直到字符串末尾- 发现末尾无
",开始回溯:逐个缩短.+的匹配 - 直到倒数第二个
"("broom"的结束引号)才成功匹配 - 最终匹配整个
"witch" and her "broom"
贪婪模式:先吃光,再吐出。
惰性模式(最小匹配)
在量词后添加 ? 可启用惰性模式(如 +?、*?、??、{n,m}?)。此时引擎会:
- 先匹配最少次数(如
+?至少 1 次) - 立即检查后续模式是否匹配
- 若不匹配,才增加重复次数
正确解决方案
let str = 'a "witch" and her "broom" is one';
str.match(/".+?"/g); // ['"witch"', '"broom"'] ✅
匹配过程:
- 找到第一个
"(位置 2) .+?先匹配 1 个字符w- 立即检查下一个字符是否为
"→ 是i,失败 .+?增加到 2 个字符wi→ 仍不是"- 重复直到匹配
witch后的"→ 成功得到"witch" - 继续从下一个位置搜索,同理匹配
"broom"
惰性模式:先尝一口,不够再加。
? 的双重含义
- 独立使用:
?作为量词表示“0 或 1 次”(可选)/colou?r/ // 匹配 color 或 colour - 附加在量词后:
?将量词转为惰性模式/".+?"/ // +? 表示惰性匹配 1 次或多次
替代方案:排除法(有时优于惰性)
惰性并非万能。考虑匹配 HTML 链接:
<a href="link1" class="doc">...<a href="link2" class="doc">
- 错误贪婪:
/<a href=".*" class="doc">/g→ 匹配两个链接整体 - 错误惰性:
/<a href=".*?" class="doc">/g→ 可能跨标签匹配(如包含<p>)
正确方案:用排除字符类精准限定范围
/<a href="[^"]*" class="doc">/g
[^"]*表示“非引号字符任意次”,自然在最近的"处停止- 无需惰性,且避免跨标签风险
关键区别:
- 惰性量词:通过控制重复次数实现最小匹配
- 排除法:通过限定字符集天然限制范围
实际应用对比
| 场景 | 贪婪模式 | 惰性模式 | 排除法 |
|---|---|---|---|
| 匹配引号内容 | ".+" → 错误 | ".+?" → 正确 | "[^"]+" → 更优 |
| 匹配 HTML 标签 | <.*> → 跨标签 | <.*?> → 可能跨标签 | <[^>]+> → 安全 |
- 默认贪婪:量词尽可能多匹配,失败时回溯
- 惰性匹配:量词后加
?,尽可能少匹配,逐步扩展 - 优先排除法:当目标有明确边界(如引号、尖括号),用
[^...]比惰性更可靠 - 注意
?上下文:独立时是量词,附加时是惰性修饰符
选择策略:
- 边界明确 → 用排除法(
[^x]*) - 无明确边界但需最小匹配 → 用惰性量词(
*?) - 需最大匹配 → 保留贪婪(默认)
捕获组
正则表达式中的分组机制允许我们将模式的一部分视为整体,用于提取、重复或逻辑组合。分组通过圆括号实现,并支持多种类型以满足不同需求。
基本捕获组:(...)
括号 (...) 创建捕获组,具有双重作用:
- 将子表达式作为整体应用量词
- 在匹配结果中单独保存该部分的内容
示例:重复模式
不带括号时,go+ 匹配 g 后跟一个或多个 o;而 (go)+ 将 go 视为整体,匹配 go、gogo、gogogo 等:
'Gogogo now!'.match(/(go)+/ig); // ["Gogogo"]
示例:域名匹配
域名由重复的单词加点组成(最后无点):
let regexp = /(\w+\.)+\w+/g;
"site.com my.site.com".match(regexp); // ["site.com", "my.site.com"]
为支持连字符,可改用 ([\w-]+\.)+[\w-]+。
提取组内容
当 match 不带 g 标志时,返回数组包含:
[0]:完整匹配[1]、[2]...:各捕获组内容
例如提取 HTML 标签名:
let str = '<h1>Hello, world!</h1>';
let tag = str.match(/<(.*?)>/);
console.info(tag[0]); // "<h1>"
console.info(tag[1]); // "h1"
嵌套组与编号规则
组按左括号出现顺序从左到右编号:
let str = ' <span class="my">';
let regexp = /<(([a-z]+)\s*([^>]*))>/;
let result = str.match(regexp);
// result[0]: ' <span class="my">'
// result[1]: 'span class="my"' ← 外层组
// result[2]: 'span' ← 标签名
// result[3]: 'class="my"' ← 属性
可选组处理
即使组因量词(如 (...)?)未匹配,结果数组仍保留对应位置,值为 undefined:
'a'.match(/a(z)?(c)?/);
// ["a", undefined, undefined]
'ac'.match(/a(z)?(c)?/);
// ["ac", undefined, "c"]
数组长度始终等于组数加一(包含完整匹配)。
matchAll:全局匹配并提取所有组
当使用 g 标志时,match 无法返回捕获组,仅给出完整匹配列表。matchAll 解决了此问题:
关键特性
- 返回可迭代对象(非数组),需用
Array.from()转换或直接遍历 - 每个匹配项都是含组信息的数组(格式同无
g的match) - 无匹配时返回空可迭代对象(非
null)
示例:提取所有标签
let str = '<h1> <h2>';
let results = str.matchAll(/<(.*?)>/gi);
// 遍历方式
for (let result of results) {
console.info(result[0]); // "<h1>", "<h2>"
console.info(result[1]); // "h1", "h2"
}
// 转数组
results = Array.from(str.matchAll(/<(.*?)>/gi));
console.info(results[0][1]); // "h1"
每个匹配项还包含:
index:匹配起始位置input:源字符串
设计优势
可迭代对象实现惰性求值:仅在需要时计算下一个匹配,避免不必要的全量搜索。
命名捕获组:(?...)
通过 (?<name>...) 为组指定名称,提升可读性和维护性:
基本用法
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
let str = "2019-04-30";
let groups = str.match(dateRegexp).groups;
console.info(groups.year); // "2019"
console.info(groups.month); // "04"
全局匹配结合 matchAll
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30 2020-01-01";
for (let result of str.matchAll(dateRegexp)) {
let {year, month, day} = result.groups;
console.info(`${day}.${month}.${year}`); // "30.10.2019", "01.01.2020"
}
替换中的组引用
str.replace 支持在替换字符串中引用捕获组:
数字引用
"John Bull".replace(/(\w+) (\w+)/, '$2, $1'); // "Bull, John"
命名引用
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
"2019-10-30".replace(regexp, '$<day>.$<month>.$<year>'); // "30.10.2019"
非捕获组:(?:...)
当只需分组应用量词但无需保存内容时,使用 (?:...):
let str = "Gogogo John!";
let regexp = /(?:go)+ (\w+)/i;
let result = str.match(regexp);
// result[0]: "Gogogo John"
// result[1]: "John" (只有显式捕获组被记录)
优势:
- 减少内存占用
- 避免干扰后续组的编号
- 无法在替换中引用(因未被捕获)
总结
- 捕获组
(...):提取子匹配,按左括号顺序编号 - 命名组
(?<name>...):通过名称访问,提高可读性 - 非捕获组
(?:...):仅用于分组,不保存匹配内容 - matchAll:全局搜索时获取所有匹配及其捕获组,返回可迭代对象
- 替换引用:用
$n或$<name>在替换字符串中插入组内容
合理运用分组机制,能显著提升正则表达式的结构化处理能力,尤其在复杂文本解析场景中不可或缺。
模式中的反向引用:\N 和 \k
在正则表达式中,我们不仅可以将捕获组用于提取或替换,还能在模式内部引用已匹配的组内容。这种机制称为反向引用(backreference),用于确保模式前后部分的一致性。
按编号反向引用:\N
使用 \N(N 为组编号)可在模式中引用第 N 个捕获组实际匹配到的文本。
典型场景:匹配成对引号
目标:正确匹配单引号 '...' 或双引号 "..." 字符串,且起止引号必须一致。
错误写法:
let str = `He said: "She's the one!".`;
let regexp = /['"](.*?)['"]/g;
str.match(regexp); // ["She'"] ❌ 混合引号
问题:结束引号与开始引号类型无关,导致提前终止。
正确写法(使用 \1):
let regexp = /(['"])(.*?)\1/g;
str.match(regexp); // ["\"She's the one!\""] ✅
工作原理:
(['"])捕获起始引号("或'),存为第 1 组(.*?)非贪婪匹配任意字符\1要求此处必须出现与第 1 组完全相同的字符(即相同类型的引号)
注意:
\1、\2等仅在模式内部使用;在替换字符串中应使用$1、$2。
重要限制
- 非捕获组不可引用:
(?:...)不会被编号,也无法用\N引用 - 编号基于左括号顺序:从左到右依次为
\1、\2...
按名称反向引用:\k
当正则表达式包含多个组时,使用命名捕获组可提升可读性,并通过 \k<name> 引用。
示例:命名引号组
let str = `He said: "She's the one!".`;
let regexp = /(?<quote>['"])(.*?)\k<quote>/g;
str.match(regexp); // ["\"She's the one!\""] ✅
(?<quote>['"]):将引号捕获为名为quote的组\k<quote>:要求此处匹配与quote组相同的内容
优势
- 避免编号混乱:尤其在复杂正则中,名称比数字更直观
- 增强可维护性:修改组顺序不影响引用逻辑
反向引用 vs 替换引用
| 上下文 | 语法 | 示例 |
|---|---|---|
| 模式内部 | \1, \k<name> | /(['"])\1/, /(?<q>['"])\k<q>/ |
| 替换字符串 | $1, $<name> | str.replace(/(...)/, "$1") |
混淆两者是常见错误,需特别注意使用场景。
实际应用场景
-
HTML/XML 标签匹配
确保开闭标签名称一致:/<(\w+)>.*?<\/\1>/g // 匹配 <div>...</div>,但不匹配 <div>...</span> -
重复单词检测
查找连续重复的单词(忽略大小写):/\b(\w+)\s+\1\b/gi // 匹配 "the the", "is is" -
对称结构验证
如简单回文检测(有限场景):/^(\w)(\w)\2\1$/ // 匹配 "abba", "deed"
注意事项
- 性能影响:反向引用会增加引擎回溯成本,避免在高频或大数据场景滥用
- 动态内容限制:反向引用匹配的是实际捕获的字符串,而非原始子模式。例如
(\d)\1匹配 "11"、"22",但不匹配 "12" - 浏览器支持:命名反向引用
\k<name>需 ES2018 支持(现代浏览器均兼容)
反向引用是正则表达式实现上下文一致性约束的关键工具,合理使用可解决许多结构化文本匹配难题。
选择 (OR) |
正则表达式中的选择(alternation),即逻辑“或”,通过竖线 | 实现。它允许在多个完整子表达式之间进行匹配,只要其中任意一个匹配成功即可。
基本用法
A|B|C 表示匹配 A、B 或 C 中的任意一个:
let regexp = /html|php|css|java(script)?/gi;
let str = "First HTML appeared, then CSS, then JavaScript";
console.info(str.match(regexp)); // ['HTML', 'CSS', 'JavaScript']
java(script)?表示匹配 "java" 或 "javascript"- 选择作用于整个子表达式,而非单个字符
与字符类 [...] 的区别
- 字符类
[ae]:仅在单个字符中选择(如gr[ae]y匹配 gray/grey) - 选择操作符
a|e:可在任意复杂表达式间选择
等价示例:
gr[ae]y // 等同于
gr(a|e)y // 但后者更冗长,不推荐
// 而以下完全不同:
gra|ey // 匹配 "gra" 或 "ey"
gr(a|e)y // 匹配 "gray" 或 "grey"
分组控制选择范围
选择的优先级较低,通常需用括号明确作用范围:
// 错误:匹配 "I love HTML" 或 "CSS"
/I love HTML|CSS/
// 正确:匹配 "I love HTML" 或 "I love CSS"
/I love (HTML|CSS)/
括号确保 | 仅作用于 HTML 和 CSS,而非整个左侧字符串。
实战:精确匹配有效时间
目标:匹配 hh:mm 格式且数值合法的时间(如 23:59),排除无效值(如 25:99)。
步骤分析
-
小时部分:
- 若首位为
0或1,第二位可为任意数字 →[01]\d - 若首位为
2,第二位只能是0-3→2[0-3] - 其他首位(如
3-9)无效 - 合并为:
[01]\d|2[0-3]
- 若首位为
-
分钟部分:
- 首位
0-5,第二位任意数字 →[0-5]\d
- 首位
错误写法
/[01]\d|2[0-3]:[0-5]\d/
此模式实际匹配:
[01]\d(任意以 0/1 开头的两位数),或2[0-3]:[0-5]\d(20-23 点的时间)
导致 15(无冒号)也被匹配!
正确写法
用括号限定小时部分的选择范围:
let regexp = /([01]\d|2[0-3]):[0-5]\d/g;
console.info("00:00 10:10 23:59 25:99 1:2".match(regexp));
// ["00:00", "10:10", "23:59"]
([01]\d|2[0-3])确保小时整体为 00-23- 冒号和分钟部分
[0-5]\d对所有情况生效
选择的常见陷阱
-
优先级误解
|优先级低于连接(concatenation),常需括号明确范围。 -
过度匹配
未分组时,|可能分割出意外的子模式,如/start|end/匹配 "start" 或 "end",而非 "start..." 或 "...end"。 -
性能影响
多个选择项会增加回溯成本,应将最可能匹配的选项放在前面(某些引擎支持)。
高级技巧
- 空选择:
A|表示匹配 A 或空字符串(谨慎使用) - 嵌套选择:
(A|B)|(C|D)可简化为A|B|C|D - 结合捕获组:
(red|green|blue)可提取颜色值
选择操作符是构建灵活匹配模式的基础工具,配合分组可精确控制匹配逻辑,避免模糊或错误匹配。
前瞻断言与后瞻断言
正则表达式中的断言(assertions) 用于匹配位置而非实际字符,允许我们在不消耗字符的前提下检查上下文。JavaScript 支持四种断言:前瞻(lookahead)和后瞻(lookbehind),每种又分为肯定与否定形式。
肯定前瞻:x(?=y)
匹配 x,但仅当其后紧跟 y 时成立。y 部分不会被包含在匹配结果中。
示例:匹配带单位的价格
let str = "1 turkey costs 30€";
console.info(str.match(/\d+(?=€)/)); // ["30"]
\d+匹配数字(?=€)确保数字后是欧元符号,但 € 不属于匹配内容
示例:提取带特定后缀的单词
let str = "Hello, John! How are you, John?";
console.info(str.match(/John(?=!)/g)); // ["John"](仅匹配后跟 ! 的 John)
否定前瞻:x(?!y)
匹配 x,但仅当其后不紧跟 y 时成立。
示例:排除特定单位的价格
let str = "2 turkeys cost 60€";
console.info(str.match(/\d+(?!€)/)); // ["2"](匹配不后跟 € 的数字)
示例:查找非重复单词
console.info("abc ab abc".match(/\b\w+\b(?!\s*\1)/g)); // 需配合其他技巧,单纯此模式不足
肯定后瞻:(?<=y)x(ES2018)
匹配 x,但仅当其前紧邻 y 时成立。y 部分不包含在结果中。
示例:提取美元金额
let str = "1 turkey costs $30";
console.info(str.match(/(?<=\$)\d+/)); // ["30"]
(?<=\$)确保数字前有$- 匹配结果仅为数字部分
示例:获取带前缀的标签
let str = "@admin: delete the page";
console.info(str.match(/(?<=@)\w+/)); // ["admin"]
否定后瞻:(?<!y)x(ES2018)
匹配 x,但仅当其前不紧邻 y 时成立。
示例:排除特定前缀的数字
let str = "1 turkey costs $30";
console.info(str.match(/(?<!\$)\d+/)); // ["1"](匹配不前跟 $ 的数字)
捕获组在断言中的行为
断言内部的捕获组仍会捕获内容,即使断言本身不消耗字符:
let str = "123:456";
let match = str.match(/(\d+)(?::(\d+))/);
// match[1] = "123", match[2] = "456"
但更常见的是使用非捕获组避免干扰:
/(?<=\$(?:\d{1,3},)*)(\d+)/ // 提取千分位格式后的数字
实际应用场景
-
密码强度验证
确保包含数字、小写字母、大写字母和特殊字符:let password = "Password123!"; let valid = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%]).{8,}$/.test(password); -
提取特定上下文的数据
- 从日志中提取错误码:
/(?<=ERROR:\s)\d+/ - 获取邮箱域名:
/(?<=@)[^>]+/
- 从日志中提取错误码:
-
避免越界匹配
确保单词边界外无特定字符:/\b\w+\b(?!\.)/ // 匹配不以点结尾的单词
浏览器兼容性
- 前瞻断言:所有现代浏览器支持
- 后瞻断言:需 ES2018 支持(Chrome 62+、Firefox 78+、Safari 16.4+)
在旧环境中需用替代方案(如两次匹配或字符串处理)。
性能提示
- 断言会增加回溯复杂度,尤其在长文本中
- 避免在断言内使用模糊量词(如
.*) - 优先使用肯定断言而非否定,因后者可能尝试更多路径
断言是高级正则技巧的核心,能解决许多“匹配但不包含上下文”的难题,显著提升模式精确度。
抱歉!以下是使用简体中文重新整理后的内容:
灾难性回溯
某些正则表达式看似简单,却可能在特定输入下导致 JavaScript 引擎“挂起”——CPU 占用率飙升至 100%,浏览器无响应。这种现象称为灾难性回溯(Catastrophic Backtracking),根源在于正则引擎在匹配失败时尝试了指数级增长的路径组合。
问题本质:模糊结构引发组合爆炸
正则引擎默认采用深度优先搜索 + 回溯机制:
- 贪婪量词(如
\w+)先匹配最长可能子串 - 若后续匹配失败,则逐步回溯(缩短当前匹配)
- 嵌套或模糊量词会导致回溯路径呈指数增长
懒惰量词(
\w+?)无法解决此问题——仅改变尝试顺序,不减少总组合数。
典型案例
案例一:可选空格陷阱
let regexp = /^(\w+\s?)*$/;
let str = "An input string...!";
- 意图:匹配由单词和可选空格组成的字符串
- 问题:单词
"input"可被拆解为多种\w+组合:(input)(inpu)(t)(inp)(u)(t)
...(共2^{n-1}种拆分,n为单词长度)
- 触发条件:字符串以非法字符(如
!)结尾时,引擎穷举所有拆分方案后才宣告失败。
案例二:极简复现
let regexp = /^(\d+)*$/;
let str = "0123456789z"; // 末尾非数字
- 长度
n=30的数字串需尝试超 10 亿次组合 - 最小复现:
"A".repeat(20) + "!"即可卡死旧版引擎
其他高危模式
- 交替分支含公共前缀:
/(a|aa|aaa)+/→ 改用/a+/ - 开放式嵌套量词:
/(a+)+b/→ 限制范围/(a{1,10})+b/
解决方案
1. 重写正则:消除歧义结构
通过明确分隔符禁止无效拆分:
// 危险 ❌
/^(\w+\s?)*$/
// 安全 ✅
/^(\w+\s)*\w*$/ // 强制空格作为分隔符
/^\w+(\s+\w+)*$/ // 至少一个单词,后续带空格+单词
2. 模拟占有型量词(防回溯)
JavaScript 不支持原生占有量词(如 \w++),但可用前瞻断言锁定匹配长度:
// 使用命名捕获组
let regexp = /^((?=(?<word>\w+))\k<word>\s?)*$/;
// 或数字引用
let regexp = /^((?=(\w+))\2\s?)*$/;
原理:
(?=(\w+)):前瞻查找完整单词(不消耗字符)\1:直接引用整个单词 → 永不回溯
效果验证:
// 普通模式:回溯成功
"JavaScript".match(/\w+Script/); // ["JavaScript"]
// 占有型模拟:禁止回溯
"JavaScript".match(/(?=(\w+))\1Script/); // null
3. 替代方案:分步验证
对复杂逻辑,放弃单一正则:
function isValid(str) {
return str.split(' ').every(chunk => /^\w+$/.test(chunk));
}
调试与防御
测试技巧
- 边界用例:用
"A".repeat(30)+"!"验证性能 - 可视化工具:regex101.com(启用 debugger 观察回溯步骤)
运行时防护
// 超时包装(关键场景)
function safeTest(regexp, str, timeout = 100) {
return new Promise(resolve => {
const timer = setTimeout(() => resolve(false), timeout);
resolve(regexp.test(str));
clearTimeout(timer);
});
}
浏览器兼容性
- 现代 V8(Chrome 88+):对部分回溯场景优化
- Firefox/Safari:仍可能卡死 → 代码必须自身免疫
核心原则
- 警惕嵌套量词:避免
(\w+)*、(\s*\w+)*等模糊结构 - 明确语义边界:用分隔符强制结构(如
(\w+\s)+\w*) - 高危场景防回溯:用
(?=(pattern))\1锁定匹配 - 始终测试失败用例:合法输入快 ≠ 非法输入安全
灾难性回溯源於正则的非确定性与引擎的穷举策略。编写时需自问:是否存在多种方式匹配同一子串?能否通过精确结构消除歧义?
粘性修饰符 "y",在位置处搜索
粘性修饰符 y 允许我们在源字符串的指定确切位置进行正则匹配,而不是从该位置开始向后查找。这是词法分析(如解析代码、HTML 等)等场景中的关键特性。
问题背景:为什么需要 y?
假设我们有如下 JavaScript 代码字符串:
let str = 'let varName = "value"';
我们想从位置 4(即 'v' 处)精确读取一个变量名(用 \w+ 匹配)。
- 使用
str.match(/\w+/)→ 只返回"let"(第一个匹配) - 使用全局匹配
str.match(/\w+/g)→ 返回所有单词,无法定位到位置 4 - 使用
regexp.exec()+ 手动设置lastIndex(配合g修饰符):
let regexp = /\w+/g;
regexp.lastIndex = 4;
let word = regexp.exec(str); // ✅ 得到 "varName"
看似可行,但存在严重问题:
regexp.lastIndex = 3; // 位置 3 是空格
let word = regexp.exec(str); // ❌ 仍然返回 "varName"(在位置 4)
即使
lastIndex = 3处不匹配,g修饰符仍会继续向后搜索,直到找到匹配项。
但在词法分析中,我们必须知道位置 3 到底是什么——如果是空格,就应识别为空格,而不是跳过它去匹配后面的标识符。
粘性修饰符 y 的作用
y 修饰符强制正则表达式仅在 lastIndex 指定的位置进行匹配,不允许向前或向后偏移。
示例对比
let str = 'let varName = "value"';
// 使用全局修饰符 g
let regG = /\w+/g;
regG.lastIndex = 3;
console.log(regG.exec(str)); // ["varName"] —— 在位置 4 匹配(错误!)
// 使用粘性修饰符 y
let regY = /\w+/y;
regY.lastIndex = 3;
console.log(regY.exec(str)); // null —— 位置 3 是空格,不匹配(正确!)
regY.lastIndex = 4;
console.log(regY.exec(str)); // ["varName"] —— 位置 4 确实是单词(正确!)
核心特性总结
| 特性 | g 修饰符 | y 修饰符 |
|---|---|---|
| 搜索起点 | 从 lastIndex 开始 | 仅在 lastIndex 处 |
| 是否允许跳过字符 | ✅ 是 | ❌ 否 |
| 适用场景 | 查找所有匹配项 | 精确位置匹配(如词法分析) |
| 性能 | 长文本无匹配时较慢 | 仅检查单点,更快 |
实际应用场景:词法分析器
在解析编程语言时,我们需要逐字符判断当前 token 类型:
let code = 'let x = 10;';
let pos = 0;
let tokens = [];
while (pos < code.length) {
let matched = false;
// 尝试匹配关键字
let keywordReg = /let|const|var/y;
keywordReg.lastIndex = pos;
let kw = keywordReg.exec(code);
if (kw) {
tokens.push({ type: 'keyword', value: kw[0] });
pos = kw.index + kw[0].length;
matched = true;
}
// 尝试匹配标识符
let identReg = /[a-zA-Z_]\w*/y;
identReg.lastIndex = pos;
let id = identReg.exec(code);
if (id) {
tokens.push({ type: 'identifier', value: id[0] });
pos = id.index + id[0].length;
matched = true;
}
// ...其他 token 类型
if (!matched) {
pos++; // 跳过未知字符(如空格、=、;)
}
}
使用
y修饰符确保每个正则只在当前解析位置尝试匹配,不会“偷看”后面的内容,保证解析准确性。
注意事项
-
lastIndex必须手动设置
y修饰符依赖regexp.lastIndex指定匹配位置。 -
匹配成功后自动更新
lastIndex
与g类似,成功匹配后lastIndex会被设为匹配结束位置。 -
匹配失败时
lastIndex不变
若未匹配,lastIndex保持原值(需手动推进位置)。 -
性能优势
在长文本中,y只检查一个位置,避免不必要的全文扫描。
总结
y修饰符 = “粘性”匹配:正则表达式像“粘”在lastIndex位置上,只在那里尝试匹配。- 核心用途:实现精确位置匹配,适用于词法分析、语法高亮、自定义解析器等场景。
- 与
g的区别:g是“从某处开始找”,y是“就在某处匹配”。
当需要“这个位置是不是某种模式?”而不是“从这个位置往后有没有这种模式?”时,请使用
y修饰符。
正则表达式和字符串的方法
JavaScript 提供了多种使用正则表达式处理字符串的方法,包括正则对象的方法(如 exec、test)和字符串原型上的方法(如 match、replace、search、split)。它们在功能和返回值上各有特点。
regexp.exec(str):返回匹配的完整信息
- 作用:在字符串
str中执行搜索,返回第一个匹配结果的详细信息(包括捕获组、索引等),若无匹配则返回null。 - 特点:
- 返回一个数组,包含匹配文本、捕获组、
index(匹配位置)、input(原字符串)等属性。 - 若正则带有
g(全局)修饰符,会更新regexp.lastIndex,支持连续调用查找下一个匹配。 - 若不带
g,每次调用都从字符串开头开始搜索。
- 返回一个数组,包含匹配文本、捕获组、
使用示例
let str = 'More about JavaScript at https://javascript.info';
let regexp = /(\w+)\.info/g;
let match = regexp.exec(str);
console.log(match);
// ["javascript.info", "javascript", index: 28, input: "..."]
console.log(match[0]); // "javascript.info"
console.log(match[1]); // "javascript"
console.log(match.index); // 28
注意:连续调用
exec可遍历所有匹配(需g修饰符):
let regexp = /a/g;
let str = 'abac';
console.log(regexp.exec(str)); // ["a"], lastIndex=1
console.log(regexp.exec(str)); // ["a"], lastIndex=3
console.log(regexp.exec(str)); // null, lastIndex=0(重置)
regexp.test(str):仅检查是否存在匹配
- 作用:测试字符串
str是否与正则regexp匹配,返回布尔值(true/false)。 - 特点:
- 忽略捕获组,只关心是否匹配。
- 若带
g修饰符,也会更新lastIndex(但通常不用于此目的)。因此在同一文本上调用
regexp.test两次,会出现不同的结果,或者手动将lastIndex置为0
使用示例
let str = 'Hello';
console.log(/hello/i.test(str)); // true(忽略大小写)
let regexp = /\d+/g;
console.log(regexp.test("There are 12 apples")); // true
console.log(regexp.lastIndex); // 10(匹配结束位置)
str.match(regexp):根据正则是否有 g 返回不同结果
- 作用:在字符串中查找匹配项。
- 行为差异:
- 无
g修饰符:返回与regexp.exec(str)相同的结果(数组或null)。 - 有
g修饰符:返回所有匹配的字符串数组(不含捕获组、索引等信息),若无匹配则返回null。
- 无
使用示例
let str = 'I love HTML5 and CSS3';
// 无 g:返回详细信息
console.log(str.match(/\d+/));
// ["5", index: 11, input: "..."]
// 有 g:返回所有匹配的字符串
console.log(str.match(/\d+/g)); // ["5", "3"]
// 无匹配
console.log(str.match(/\d{4}/g)); // null
注意:若想获取所有匹配的详细信息(含捕获组),必须使用
regexp.exec()循环。
str.matchAll(regexp):返回所有匹配的迭代器(ES2020+)
- 作用:返回一个可迭代对象,包含所有匹配的详细信息(类似多次调用
exec的结果)。 - 要求:
regexp必须带有g修饰符,否则抛出错误。 - 优势:避免手动管理
lastIndex,代码更清晰。
使用示例
let str = 'My phone numbers: 123-456-7890, 098-765-4321';
let regexp = /(\d{3})-(\d{3})-(\d{4})/g;
let matches = str.matchAll(regexp);
for (let match of matches) {
console.log(`${match[0]} -> (${match[1]})`);
}
// 输出:
// 123-456-7890 -> (123)
// 098-765-4321 -> (098)
// 转为数组
console.log([...str.matchAll(regexp)]);
str.search(regexp):返回第一个匹配的位置
- 作用:查找第一个匹配项的起始索引,若无匹配返回
-1。 - 特点:
- 忽略
g修饰符(总是从头开始找第一个)。 - 不支持捕获组,仅返回位置。
- 忽略
使用示例
let str = 'A drop of ink may make a million think';
console.log(str.search(/ink/)); // 12
console.log(str.search(/xyz/)); // -1
str.replace(regexp, replacement):替换匹配内容
- 作用:用
replacement替换字符串中与正则表达式匹配的部分,返回一个新字符串(原字符串不变)。 replacement类型:- 字符串:可在其中使用特殊占位符(如
$1、$&等)动态插入匹配信息。 - 函数:每次匹配时调用,接收匹配详情并返回要插入的替换字符串(详见下方“函数替换”部分)。
- 字符串:可在其中使用特殊占位符(如
g修饰符影响:- 无
g:仅替换第一个匹配项。 - 有
g:替换所有匹配项。
- 无
regexp参数类型的影响:- 若传入的是字符串(如
"abc"),则只替换第一个匹配项,且不支持正则特性(如\d、捕获组等)。 - 若传入的是正则表达式对象,则按其规则(包括修饰符、捕获组等)进行匹配和替换。
- 若传入的是字符串(如
字符串替换中的特殊占位符
当 replacement 是字符串时,可使用以下符号引用匹配上下文:
| 符号 | 替换字符串中的行为 |
|---|---|
$& | 插入整个匹配项 |
| 插入匹配项之前的字符串部分(注意:是反引号,非单引号) | |
$' | 插入匹配项之后的字符串部分 |
$n | 如果 n 是 1-2 位数字,插入第 n 个捕获组的内容(例如 $1, $2) |
$<name> | 插入名为 name 的命名捕获组的内容(需配合 (?<name>...) 使用) |
$$ | 插入字面量字符 $(用于转义) |
示例
let str = 'Price: 100';
// $&:整个匹配
console.log(str.replace(/\d+/, '$$$&')); // "Price: $100"
// $1, $2:普通捕获组
console.log('John Smith'.replace(/(\w+) (\w+)/, '$2, $1')); // "Smith, John"
// $<name>:命名捕获组
console.log('John Smith'.replace(/(?<first>\w+) (?<last>\w+)/, '$<last>, $<first>')); // "Smith, John"
// $` 和 $'
console.log('abc123def'.replace(/\d+/, '[$`] [$&] [$\']')); // "[abc] [123] [def]"
函数作为替换器(高级用法)
当 replacement 是函数时,每次匹配都会调用该函数,并用其返回值作为替换内容。
函数参数
function replacer(
match, // 完整匹配项
p1, p2, ..., pn, // 捕获组内容(若有)
offset, // 匹配起始位置
input, // 原始字符串
groups // 命名捕获组对象(ES2018+,若存在)
)
- 若正则无捕获组,函数仅接收
(match, offset, input)三个参数。 - 若使用命名捕获组,
groups对象始终是最后一个参数。
示例
// 转大写
'html and css'.replace(/html|css/gi, m => m.toUpperCase()); // "HTML and CSS"
// 替换为匹配位置
"Ho-Ho-ho".replace(/ho/gi, (m, offset) => offset); // "0-3-6"
// 交换姓名(位置参数)
"John Smith".replace(/(\w+) (\w+)/, (m, first, last) => `${last}, ${first}`); // "Smith, John"
// 使用 rest + groups
"John Smith".replace(/(?<name>\w+) (?<surname>\w+)/, (...args) => {
const groups = args.pop();
return `${groups.surname}, ${groups.name}`;
}); // "Smith, John"
✅ 函数替换的优势:可访问完整匹配上下文、结合外部状态、实现复杂逻辑(如格式化、查表、条件替换等),是实现“智能替换”的核心手段。
使用示例
let str = 'John Doe, John Smith';
// 字符串替换(仅第一个)
console.log(str.replace(/John/, 'Mr.$&')); // "Mr.John Doe, John Smith"
// 全局替换 + 捕获组
console.log(str.replace(/(\w+) (\w+)/g, '$2, $1'));
// "Doe, John, Smith, John"
// 函数替换(转大写)
console.log(str.replace(/\b\w+\b/g, word => word.toUpperCase()));
// "JOHN DOE, JOHN SMITH"
str.split(regexp):按正则分割字符串
- 作用:以匹配项为分隔符,将字符串拆分为数组。
- 特点:
- 若正则包含捕获组,捕获的内容也会被包含在结果数组中。
- 可指定最大分割数(第二个参数)。
使用示例
let str = 'apple,banana;cherry';
// 按逗号或分号分割
console.log(str.split(/[,;]/)); // ["apple", "banana", "cherry"]
// 使用捕获组(保留分隔符)
console.log(str.split(/([,;])/));
// ["apple", ",", "banana", ";", "cherry"]
// 限制分割次数
console.log(str.split(/[,;]/, 2)); // ["apple", "banana"]
方法对比速查表
| 方法 | 返回值 | 全局匹配 (g) | 捕获组 | 适用场景 |
|---|---|---|---|---|
regexp.exec(str) | 详细匹配数组 / null | ✅(更新 lastIndex) | ✅ | 需要索引、捕获组、逐个遍历 |
regexp.test(str) | true / false | ⚠️(更新 lastIndex) | ❌ | 仅需判断是否匹配 |
str.match(regexp) | 数组 / null | 无 g:同 exec;有 g:仅匹配文本 | 无 g:✅;有 g:❌ | 快速获取匹配文本或简单信息 |
str.matchAll(regexp) | 可迭代对象 | ✅(必须带 g) | ✅ | 获取所有匹配的完整信息 |
str.search(regexp) | 索引 / -1 | ❌(始终找第一个) | ❌ | 仅需匹配位置 |
str.replace(...) | 新字符串 | 无 g:替换第一个;有 g:全部替换 | ✅(通过 $1 或函数) | 替换文本 |
str.split(regexp) | 字符串数组 | ✅ | ✅(捕获组会插入结果) | 分割字符串 |