正则表达式

模式 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.matchstr.replaceregexp.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");
    }
    

注意:如果正则表达式包含捕获组(括号),不带 gmatch 返回的数组还会包含捕获内容(如 result[1], result[2] 等)。

str.replace(regexp, replacement)

replacement 替换 str 中匹配 regexp 的部分:

  • regexpg 修饰符,则替换所有匹配项;
  • 否则仅替换第一个匹配项。

示例

// 仅替换第一个
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 中使用特殊符号

符号含义
$&整个匹配项
$`匹配项之前的字符串部分
$'匹配项之后的字符串部分
$nn 个捕获组(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 中是否存在至少一个匹配项,返回 truefalse

示例

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)当且仅当满足以下任一情况:

  1. 字符串开头,且第一个字符是单词字符(\w);
  2. 字符串末尾,且最后一个字符是单词字符(\w);
  3. 两个相邻字符之间,其中一个是 \w,另一个不是(例如字母与空格、标点、数字与非数字等)。

单词字符 \w 仅包括:

  • 拉丁字母 a–zA–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"'] ❌

原因分析

  1. 引擎找到第一个 "(位置 2)
  2. .+ 贪婪匹配所有后续字符直到字符串末尾
  3. 发现末尾无 ",开始回溯:逐个缩短 .+ 的匹配
  4. 直到倒数第二个 ""broom" 的结束引号)才成功匹配
  5. 最终匹配整个 "witch" and her "broom"

贪婪模式:先吃光,再吐出

惰性模式(最小匹配)

在量词后添加 ? 可启用惰性模式(如 +?*???{n,m}?)。此时引擎会:

  1. 先匹配最少次数(如 +? 至少 1 次)
  2. 立即检查后续模式是否匹配
  3. 若不匹配,才增加重复次数

正确解决方案

let str = 'a "witch" and her "broom" is one';
str.match(/".+?"/g); // ['"witch"', '"broom"'] ✅

匹配过程

  1. 找到第一个 "(位置 2)
  2. .+? 先匹配 1 个字符 w
  3. 立即检查下一个字符是否为 " → 是 i,失败
  4. .+? 增加到 2 个字符 wi → 仍不是 "
  5. 重复直到匹配 witch 后的 " → 成功得到 "witch"
  6. 继续从下一个位置搜索,同理匹配 "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 标签<.*> → 跨标签<.*?> → 可能跨标签<[^>]+> → 安全
  1. 默认贪婪:量词尽可能多匹配,失败时回溯
  2. 惰性匹配:量词后加 ?,尽可能少匹配,逐步扩展
  3. 优先排除法:当目标有明确边界(如引号、尖括号),用 [^...] 比惰性更可靠
  4. 注意 ? 上下文:独立时是量词,附加时是惰性修饰符

选择策略:

  • 边界明确 → 用排除法([^x]*
  • 无明确边界但需最小匹配 → 用惰性量词(*?
  • 需最大匹配 → 保留贪婪(默认)

捕获组

正则表达式中的分组机制允许我们将模式的一部分视为整体,用于提取、重复或逻辑组合。分组通过圆括号实现,并支持多种类型以满足不同需求。

基本捕获组:(...)

括号 (...) 创建捕获组,具有双重作用:

  1. 将子表达式作为整体应用量词
  2. 在匹配结果中单独保存该部分的内容

示例:重复模式

不带括号时,go+ 匹配 g 后跟一个或多个 o;而 (go)+go 视为整体,匹配 gogogogogogo 等:

'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() 转换或直接遍历
  • 每个匹配项都是含组信息的数组(格式同无 gmatch
  • 无匹配时返回空可迭代对象(非 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 组
  2. (.*?) 非贪婪匹配任意字符
  3. \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")

混淆两者是常见错误,需特别注意使用场景。

实际应用场景

  1. HTML/XML 标签匹配
    确保开闭标签名称一致:

    /<(\w+)>.*?<\/\1>/g
    // 匹配 <div>...</div>,但不匹配 <div>...</span>
    
  2. 重复单词检测
    查找连续重复的单词(忽略大小写):

    /\b(\w+)\s+\1\b/gi
    // 匹配 "the the", "is is"
    
  3. 对称结构验证
    如简单回文检测(有限场景):

    /^(\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)/

括号确保 | 仅作用于 HTMLCSS,而非整个左侧字符串。

实战:精确匹配有效时间

目标:匹配 hh:mm 格式且数值合法的时间(如 23:59),排除无效值(如 25:99)。

步骤分析

  1. 小时部分

    • 若首位为 01,第二位可为任意数字 → [01]\d
    • 若首位为 2,第二位只能是 0-32[0-3]
    • 其他首位(如 3-9)无效
    • 合并为:[01]\d|2[0-3]
  2. 分钟部分

    • 首位 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 对所有情况生效

选择的常见陷阱

  1. 优先级误解
    | 优先级低于连接(concatenation),常需括号明确范围。

  2. 过度匹配
    未分组时,| 可能分割出意外的子模式,如 /start|end/ 匹配 "start" 或 "end",而非 "start..." 或 "...end"。

  3. 性能影响
    多个选择项会增加回溯成本,应将最可能匹配的选项放在前面(某些引擎支持)。

高级技巧

  • 空选择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+)/ // 提取千分位格式后的数字

实际应用场景

  1. 密码强度验证
    确保包含数字、小写字母、大写字母和特殊字符:

    let password = "Password123!";
    let valid = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%]).{8,}$/.test(password);
    
  2. 提取特定上下文的数据

    • 从日志中提取错误码:/(?<=ERROR:\s)\d+/
    • 获取邮箱域名:/(?<=@)[^>]+/
  3. 避免越界匹配
    确保单词边界外无特定字符:

    /\b\w+\b(?!\.)/ // 匹配不以点结尾的单词
    

浏览器兼容性

  • 前瞻断言:所有现代浏览器支持
  • 后瞻断言:需 ES2018 支持(Chrome 62+、Firefox 78+、Safari 16.4+)

在旧环境中需用替代方案(如两次匹配或字符串处理)。

性能提示

  • 断言会增加回溯复杂度,尤其在长文本中
  • 避免在断言内使用模糊量词(如 .*
  • 优先使用肯定断言而非否定,因后者可能尝试更多路径

断言是高级正则技巧的核心,能解决许多“匹配但不包含上下文”的难题,显著提升模式精确度。

抱歉!以下是使用简体中文重新整理后的内容:

灾难性回溯

某些正则表达式看似简单,却可能在特定输入下导致 JavaScript 引擎“挂起”——CPU 占用率飙升至 100%,浏览器无响应。这种现象称为灾难性回溯(Catastrophic Backtracking),根源在于正则引擎在匹配失败时尝试了指数级增长的路径组合。

问题本质:模糊结构引发组合爆炸

正则引擎默认采用深度优先搜索 + 回溯机制

  1. 贪婪量词(如 \w+)先匹配最长可能子串
  2. 若后续匹配失败,则逐步回溯(缩短当前匹配)
  3. 嵌套或模糊量词会导致回溯路径呈指数增长

懒惰量词(\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:仍可能卡死 → 代码必须自身免疫

核心原则

  1. 警惕嵌套量词:避免 (\w+)*(\s*\w+)* 等模糊结构
  2. 明确语义边界:用分隔符强制结构(如 (\w+\s)+\w*
  3. 高危场景防回溯:用 (?=(pattern))\1 锁定匹配
  4. 始终测试失败用例:合法输入快 ≠ 非法输入安全

灾难性回溯源於正则的非确定性引擎的穷举策略。编写时需自问:是否存在多种方式匹配同一子串?能否通过精确结构消除歧义?

粘性修饰符 "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 修饰符确保每个正则只在当前解析位置尝试匹配,不会“偷看”后面的内容,保证解析准确性。

注意事项

  1. lastIndex 必须手动设置
    y 修饰符依赖 regexp.lastIndex 指定匹配位置。

  2. 匹配成功后自动更新 lastIndex
    g 类似,成功匹配后 lastIndex 会被设为匹配结束位置。

  3. 匹配失败时 lastIndex 不变
    若未匹配,lastIndex 保持原值(需手动推进位置)。

  4. 性能优势
    在长文本中,y 只检查一个位置,避免不必要的全文扫描。

总结

  • y 修饰符 = “粘性”匹配:正则表达式像“粘”在 lastIndex 位置上,只在那里尝试匹配。
  • 核心用途:实现精确位置匹配,适用于词法分析、语法高亮、自定义解析器等场景。
  • g 的区别g 是“从某处开始找”,y 是“就在某处匹配”。

当需要“这个位置是不是某种模式?”而不是“从这个位置往后有没有这种模式?”时,请使用 y 修饰符。

正则表达式和字符串的方法

JavaScript 提供了多种使用正则表达式处理字符串的方法,包括正则对象的方法(如 exectest)和字符串原型上的方法(如 matchreplacesearchsplit)。它们在功能和返回值上各有特点。

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)数组 / nullg:同 exec;有 g:仅匹配文本g:✅;有 g:❌快速获取匹配文本或简单信息
str.matchAll(regexp)可迭代对象✅(必须带 g获取所有匹配的完整信息
str.search(regexp)索引 / -1❌(始终找第一个)仅需匹配位置
str.replace(...)新字符串g:替换第一个;有 g:全部替换✅(通过 $1 或函数)替换文本
str.split(regexp)字符串数组✅(捕获组会插入结果)分割字符串