模式(Patterns)和修饰符(flags)
- 正则表达式
- 正则表达式(可叫作 “regexp”,或 “reg”)由 模式 和可选的 修饰符 组成。
- 有两种创建正则表达式对象的语法。
- 较长一点的语法:regexp = new RegExp("pattern", "flags");
- 较短一点的语法,使用斜线
"/":
JavaScript斜线regexp = /pattern/; // 没有修饰符 regexp = /pattern/gmi; // 带有修饰符 g、m 和 i(后面会讲到)/.../告诉 JavaScript 我们正在创建一个正则表达式。它的作用与字符串的引号作用相同。 - 修饰符
- 正则表达式可能有影响搜索结果的修饰符。
- 在 JavaScript 中,只有 6 个修饰符:
i:使用此修饰符后,搜索时不区分大小写:A和a之间没有区别(请参见下面的示例)。g:使用此修饰符后,搜索时会寻找所有的匹配项 —— 没有它,则仅返回第一个匹配项。m:多行模式(详见 锚点 ^ $ 的多行模式,修饰符 "m")。s:启用 “dotall” 模式,允许点.匹配换行符\n(在 字符类 中有详细介绍)。u:开启完整的 Unicode 支持。该修饰符能够正确处理代理对。详见 Unicode:修饰符 "u" 和类 \p{...}。y:粘滞(Sticky)模式,在文本中的确切位置搜索(详见 粘性修饰符 "y",在位置处搜索)
- 颜色
- 正则表达式 ——
red - 字符串(我们搜索的地方)——
blue - 结果 ——
green
- 正则表达式 ——
- 搜索:str.match
str.match(regexp)方法在字符串str中寻找regexp的所有匹配项。- 它有 3 种工作模式:
- 如果正则表达式具有修饰符
g,它返回一个由所有匹配项所构成的数组 - 如果没有这样的修饰符,它则会以数组形式返回第一个匹配项,索引
0处保存着完整的匹配项,返回的结果的属性中还有一些其他详细信息 - 最后,如果没有匹配项,则返回
null(无论是否有修饰符g)。
- 如果正则表达式具有修饰符
//1
let str = "We will, we will rock you";
alert( str.match(/we/gi) ); // We,we(由两个匹配的子字符串构成的数组)
//请注意,`We` 和 `we` 都被找到了,因为修饰符 `i` 使得正则表达式在进行搜索时不区分大小写。
//2
let str = "We will, we will rock you";
let result = str.match(/we/i); // 没有修饰符 g
alert( result[0] ); // We(第一个匹配项)
alert( result.length ); // 1
// 详细信息:
alert( result.index ); // 0(匹配项的位置)
alert( result.input ); // We will, we will rock you(源字符串)
//如果正则表达式中有一部分内容被包在括号里,那么返回的数组可能会有 `0` 以外的索引。我们将在 [捕获组](https://zh.javascript.info/regexp-groups) 中学习这部分相关内容。
//3
let matches = "JavaScript".match(/HTML/) || [];
if (!matches.length) {
alert("No matches"); // 现在可以了
}- 替换:str.replace
str.replace(regexp, replacement)方法使用replacement替换在字符串str中找到的regexp的匹配项(如果带有修饰符g则替换所有匹配项,否则只替换第一个)。
| 符号 | 在替换字符串中的行为 |
|---|---|
| $& | 插入整个匹配项 |
| $` | 插入字符串中匹配项之前的字符串部分 |
| $' | 插入字符串中匹配项之后的字符串部分 |
| $n | 如果 n 是一个 1-2 位的数字,则插入第 n 个分组的内容,详见 捕获组 |
$<name> | 插入带有给定 name 的括号内的内容,详见 捕获组 |
| $$ | 插入字符 $ |
- 测试:regexp.test
regexp.test(str)方法寻找至少一个匹配项,如果找到了,则返回true,否则返回false
let str = "I love JavaScript";
let regexp = /LOVE/i;
alert( regexp.test(str) ); // true字符类
字符类(Character classes) 是一种特殊的符号,匹配特定集合中的任何符号。 让我们探索“数字”类。它写为 \d,对应于“任何一位数字”。 让我们找到电话号码的第一个数字:
let str = "+7(903)-123-45-67";
let regexp = /\d/;
alert( str.match(regexp) ); // 7如果没有修饰符 g,则正则表达式仅查找第一个匹配项,即第一个数字 \d。
let str = "+7(903)-123-45-67";
let regexp = /\d/g;
alert( str.match(regexp) ); // 匹配项构成的数组:7,9,0,3,1,2,3,4,5,6,7
// 让我们将其输出为纯数字构成的电话号码:
alert( str.match(regexp).join('') ); // 79031234567这是数字的字符类。还有其他字符类。最常用的是:
\d:(“d” 来自 “digit”):数字:从0到9的字符。\s:(“s” 来自 “space”):空格符号:包括空格,制表符\t,换行符\n和其他少数稀有字符,例如\v、\f和\r。\w:(“w” 来自 “word”):“单字”字符:拉丁字母或数字或下划线_。非拉丁字母(如西里尔字母或印地文)不属于\w。- 例如,
\d\s\w表示“数字”,后跟“空格字符”,后跟“单字字符”,例如1 a。
正则表达式可能同时包含常规符号和字符类。CSS\d 匹配 CSS 后面带有一个数字的字符串:
let str = "Is there CSS4?";
let regexp = /CSS\d/
alert( str.match(regexp) ); // CSS4- 反向类
- 对于每个字符类,都有一个“反向类”,用相同的字母表示,但是大写的。
- “反向”表示它与所有其他字符匹配,例如:
\D:非数字:除\d以外的任何字符,例如字母。\S:非空格符号:除\s以外的任何字符,例如字母。\W:非单字字符:除\w以外的任何字符,例如非拉丁字母或空格。
- 点(.)匹配“任何字符”
- 点
.是一种特殊字符类,它与“除换行符之外的任何字符”匹配。
JavaScript点表示“任何字符”,而不是“缺少字符”。必须有一个与之匹配的字符:let regexp = /CS.4/; alert( "CSS4".match(regexp) ); // CSS4 alert( "CS-4".match(regexp) ); // CS-4 alert( "CS 4".match(regexp) ); // CS 4(空格也是一个字符)JavaScriptalert( "CS4".match(/CS.4/) ); // null,没有匹配项,因为这里没有与点匹配的字符 - 点
- 带有修饰符 “s” 时点字符类匹配任何字符
- 默认情况下,点与换行符
\n不匹配。
JavaScript//正则表达式 `A.B` 匹配 `A`,然后匹配 `B` 和它们之间的任何字符,除了换行符`\n`: alert( "A\nB".match(/A.B/) ); // null(无匹配项) //这就是修饰符 `s` 所做的事。如果有一个正则表达式具有该修饰符,那么点 `.` 能够匹配任何字符: alert( "A\nB".match(/A.B/s) ); // A\nB(匹配了!)- IE 浏览器不支持
- IE 浏览器不支持
s修饰符。 - 幸运的是,有一个替代方案,我们可以使用像
[\s\S]这样的正则表达式来匹配“任何字符”(此模式将在 集合和范围 [...] 中介绍)。
- IE 浏览器不支持
- 注意空格
- 通常我们很少注意空格。对我们来说,字符串
1-5和1 - 5几乎相同。
- 通常我们很少注意空格。对我们来说,字符串
- 一个空格是一个字符。与其他字符同等重要。
- 我们无法在正则表达式中添加或删除空格后,还期望它的作用能像之前那样不被改变。
- 默认情况下,点与换行符
- 字符类:
\d—— 数字。\D—— 非数字。\s—— 空格符号,制表符,换行符。\S—— 除了\s。\w—— 拉丁字母,数字,下划线'_'。\W—— 除了\w。.—— 带有修饰符's'时匹配任何字符,否则匹配除换行符\n之外的任何字符。
Unicode:修饰符 "u" 和类 \p{...}
JavaScript 对字符串使用 Unicode 编码。大多数字符使用 2 个字节编码,但这种方式只能编码最多 65536 个字符。 这个范围不足以对所有可能的字符进行编码,这就是为什么使用 4 个字节对一些罕见的字符进行编码,比如 𝒳(数学符号 X)或 😄(笑脸),一些象形文字等等。
| 字符 | Unicode | Unicode 中的字节数 |
|---|---|---|
| a | 0x0061 | 2 |
| ≈ | 0x2248 | 2 |
| 𝒳 | 0x1d4b3 | 4 |
| 𝒴 | 0x1d4b4 | 4 |
| 😄 | 0x1f604 | 4 |
- Unicode 属性 \p{…}
- Unicode 中的每个字符都有很多属性。它们描述了字符所属的“类别”,包含了关于字符的各种信息。
- 以下是主要的字符类别和它们对应的子类别:
- 字母(Letter)
L:- 小写(lowercase)
Ll, - 修饰(modifier)
Lm, - 首字母大写(titlecase)
Lt, - 大写(uppercase)
Lu, - 其它(other)
Lo。
- 小写(lowercase)
- 数字(Number)
N:- 十进制数字(decimal digit)
Nd, - 字母数字(letter number)
Nl, - 其它(other)
No。
- 十进制数字(decimal digit)
- 标点符号(Punctuation)
P:- 连接符(connector)
Pc, - 横杠(dash)
Pd, - 起始引号(initial quote)
Pi, - 结束引号(final quote)
Pf, - 开(open)
Ps, - 闭(close)
Pe, - 其它(other)
Po。
- 连接符(connector)
- 标记(Mark)
M(accents etc):- 间隔合并(spacing combining)
Mc, - 封闭(enclosing)
Me, - 非间隔(non-spacing)
Mn。
- 间隔合并(spacing combining)
- 符号(Symbol)
S:- 货币(currency)
Sc, - 修饰(modifier)
Sk, - 数学(math)
Sm, - 其它(other)
So。
- 货币(currency)
- 分隔符(Separator)
Z:- 行(line)
Zl, - 段落(paragraph)
Zp, - 空格(space)
Zs。
- 行(line)
- 其它(Other)
C:- 控制符(control)
Cc, - 格式(format)
Cf, - 未分配(not assigned)
Cn, - 私有(private use)
Co, - 代理伪字符(surrogate)
Cs。
- 控制符(control)
- 也有其它派生的类别,例如:
Alphabetic(Alpha),包含了字母L,加上字母数字Nl(例如 Ⅻ —— 罗马数字 12),加上一些其它符号Other_Alphabetic(OAlpha)。
Hex_Digit包括 16 进制数字0-9,a-f。- ……等等。
- Unicode 支持很多不同的属性,列出整个清单需要占用大量的篇幅,因此在这里列出相关的链接:
- 按照属性列出所有的字符:https://unicode.org/cldr/utility/list-unicodeset.jsp.
- 属性的对应缩写形式:https://www.unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt.
- 以文本格式整理的所有 Unicode 字符,包含了所有的属性:https://www.unicode.org/Public/UCD/latest/ucd/.
- 字母(Letter)
- 举例:16 进制数字
- 让我们来查找 16 进制数字,写作
xFF其中F是一个 16 进制的数字(0…9 或者 A…F)。
- 让我们来查找 16 进制数字,写作
let regexp = /x\p{Hex_Digit}\p{Hex_Digit}/u;
alert("number: xAF".match(regexp)); // xAF- 举例:中文字符
- 有一个 Unicode 属性
Script(一个书写系统),这个属性可能有一个值:Cyrillic、Greek、Arabic、Han(中文)等等,这里是一个完整的列表。 - 要在给定的书写系统中查找字符,我们需要使用
Script=<value>,例如对于西里尔字母:\p{sc=Cyrillic},中文象形文字:\p{sc=Han},等等。
JavaScriptlet regexp = /\p{sc=Han}/gu; // 返回中文象形文字 let str = `Hello Привет 你好 123_456`; alert( str.match(regexp) ); // 你,好 - 有一个 Unicode 属性
- 举例:货币
- 表示货币的字符,例如
$、€和¥,具有 Unicode 属性\p{Currency_Symbol},缩写为\p{Sc}。
- 表示货币的字符,例如
let regexp = /\p{Sc}\d/gu;
let str = `Prices: $2, €1, ¥9`;
alert( str.match(regexp) ); // $2,€1,¥9锚点:字符串开始 ^ 和末尾 $
插入符号 ^ 和美元符号 $ 在正则表达式中具有特殊的含义。它们被称为“锚点”。 插入符号 ^ 匹配文本开头,而美元符号 $ 则匹配文本末尾。
let str1 = "it's fleece was white as snow";
alert( /snow$/.test(str1) ); // true- 测试完全匹配
- 这两个锚点
^...$放在一起通常被用于测试一个字符串是否完全匹配一个模式。例如,检查用户输入的格式是否正确。
JavaScriptlet goodInput = "12:34"; let badInput = "12:345"; let regexp = /^\d\d:\d\d$/; alert( regexp.test(goodInput) ); // true alert( regexp.test(badInput) ); // false //在这个例子中 \d\d:\d\d 所对应的匹配项必须正好在文本 ^ 的开头之后开始,并且结尾 $ 必须紧跟其后。- 锚点“宽度”为零
- 锚点
^和$属于测试。它们的宽度为零。 - 换句话说,它们并不匹配一个具体的字符,而是让正则引擎测试所表示的条件(文本开头/文本末尾)。
- 锚点
- 这两个锚点
锚点 ^ $ 的多行模式,修饰符 "m"
多行模式由修饰符 m 启用。 它只影响 ^ 和 $ 的行为。 在多行模式下,它们不仅仅匹配文本的开始与末尾,还匹配每一行的开始与末尾。
- 搜索行的开头
^- 在这个有多行文本的例子中,模式
/^\d/gm将从每行的开头取一个数字:
- 在这个有多行文本的例子中,模式
let str = `1st place: Winnie
2nd place: Piglet
3rd place: Eeyore`;
console.log( str.match(/^\d/gm) ); // 1, 2, 3没有修饰符 m 时,仅会匹配第一个数字:
let str = `1st place: Winnie
2nd place: Piglet
3rd place: Eeyore`;
console.log( str.match(/^\d/g) ); // 1- 搜索行的末尾
$- 美元符
$的行为也类似。 - 正则表达式
\d$寻找每行的最后一个数字
- 美元符
let str = `Winnie: 1
Piglet: 2
Eeyore: 3`;
console.log( str.match(/\d$/gm) ); // 1,2,3“行的末尾”表示“就在换行符之前”:多行模式下的测试 $ 匹配所有以换行符 \n 结尾的位置。 3. 搜索 \n 而不是 ^ $ 1. 要寻找新的一行,我们不仅可以使用锚点 ^ 和 $,也可以使用换行符 \n。 2. 在这里我们使用 \d\n 进行搜索,而不是使用 \d$:
let str = `Winnie: 1
Piglet: 2
Eeyore: 3`;
console.log( str.match(/\d\n/g) ); // 1\n,2\n词边界:\b
词边界 \b 是一种检查,就像 ^ 和 $ 一样。 有三种不同的位置可作为词边界:
- 在字符串开头,如果第一个字符是单词字符
\w。 - 在字符串中的两个字符之间,其中一个是单词字符
\w,另一个不是。 - 在字符串末尾,如果最后一个字符是单词字符
\w。 例如,可以在Hello, Java!中找到\bJava\b的匹配项,其中Java是一个独立的单词,而在Hello, JavaScript!中则不行。
alert( "Hello, Java!".match(/\bJava\b/) ); // Java
alert( "Hello, JavaScript!".match(/\bJava\b/) ); // null因此,它与模式 \bHello\b 相匹配,因为:
- 字符串的开头符合第一个检查
\b。 - 然后匹配了单词
Hello。 - 然后与
\b再次匹配,因为我们在o和逗号之间。
所以模式 \bHello\b 会匹配,但 \bHell\b 不会匹配(因为在 l 之后没有单词边界),Java!\b 也不会匹配(因为感叹号不是单词字符 \w,所以其后没有词边界)。
alert( "Hello, Java!".match(/\bHello\b/) ); // Hello
alert( "Hello, Java!".match(/\bJava\b/) ); // Java
alert( "Hello, Java!".match(/\bHell\b/) ); // null(无匹配项)
alert( "Hello, Java!".match(/\bJava!\b/) ); // null(无匹配项)\b 既可以用于单词,也可以用于数字。 例如,模式 \b\d\d\b 查找独立的两位数。换句话说,它查找的是两位数,其周围是与 \w 不同的字符,例如空格或标点符号(或文本开头/结尾)。
alert( "1 23 456 78".match(/\b\d\d\b/g) ); // 23,78
alert( "12,34,56".match(/\b\d\d\b/g) ); // 12,34,56词边界 \b 不适用于非拉丁字母 词边界测试 \b 检查该位置的一侧是否匹配 \w,而另一侧则不匹配 “\w”。 但是,\w 表示拉丁字母 a-z(或数字或下划线),所以此检查不适用于其他字符,如西里尔字母(cyrillic letters)或象形文字(hieroglyphs)。
转义,特殊字符
正如我们所看到的,反斜杠 \ 用于表示字符类,例如 \d。所以它是正则表达式中的一个特殊字符(就像在常规字符串中一样)。 还存在其它特殊字符,这些字符在正则表达式中有特殊的含义,例如 [ ] { } ( ) \ ^ $ . | ? * +。它们用于执行更强大的搜索。
- 转义
- 假如我们想要找到一个点号
.。要将特殊字符用作常规字符,请在其前面加上反斜杠:\.。 - 例如:
- 假如我们想要找到一个点号
alert( "Chapter 5.1".match(/\d\.\d/) ); // 5.1(匹配了!)
alert( "Chapter 511".match(/\d\.\d/) ); // null(寻找一个真正的点 \.)括号也是特殊字符,所以如果我们想要查找它们,我们应该使用 \(。下面的例子会查找一个字符串 "g()":
alert( "function g()".match(/g\(\)/) ); // "g()"如果我们想查找反斜杠 \,我们就应该使用两个反斜杠:
alert( "1\\2".match(/\\/) ); // '\'- 一个斜杠
- 斜杠符号
'/'并不是一个特殊字符,但是它被用于在 Javascript 中开启和关闭正则匹配:/...pattern.../,所以我们也应该转义它。 - 搜索斜杠
'/'的表达式:
- 斜杠符号
alert( "/".match(/\//) ); // '/'从另一个方面看,如果我们没使用 /.../,而是使用另一种 new RegExp 的方式创建正则表达式,则不需要转义斜杠:
alert( "/".match(new RegExp("/")) ); // 找到了 /- new RegExp
- 如果我们使用
new RegExp创建正则表达式,那么我们不必转义/,但需要进行一些其他转义。 - 示例:
- 如果我们使用
let reg = new RegExp("\d\.\d");
alert( "Chapter 5.1".match(reg) ); // null在之前的示例中我们使用 /\d\.\d/ 进行类似的搜索没问题,但 new RegExp("\d\.\d") 不起作用,为什么? 因为反斜杠被字符串“消耗”了。我们可能还记得,常规字符串有自己的特殊字符,例如 \n,反斜杠用于转义。 下面是 “\d.\d” 的感知形式:
alert("\d\.\d"); // d.d在字符串中的反斜杠表示转义或者类似 \n 这种只能在字符串中使用的特殊字符。这个引用会“消耗”并且解释这些字符,比如说:
\n—— 变成一个换行字符,\u1234—— 变成该编码所对应的 Unicode 字符,- ……而当没有特殊含义时:如
\d或者\z,碰到这种情况时则会自动移除反斜杠。 所以调用 new RegExp 会获得一个没有反斜杠的字符串。这就是搜索不起作用的原因! 如果要修复这个问题,我们需要双斜杠,因为引用会把\\变为\:
let regStr = "\\d\\.\\d";
alert(regStr); // \d\.\d(现在对了)
let regexp = new RegExp(regStr);
alert( "Chapter 5.1".match(regexp) ); // 5.1集合和范围 [...]
在方括号 […] 中的几个字符或者字符类表示“搜索给定字符中的任意一个”。
- 集合
- 例如,
[eao]表示以下 3 个字符中的任何一个:'a'、'e'或'o'。 - 这就是所谓的 集合。在正则表达式中,可以将集合和常规字符一起使用。
- 例如,
// 查找 [t 或 m],然后匹配 "op"
alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top"请注意,虽然集合中有多个字符,但它们在匹配中只会对应其中的一个。 所以在下面的示例中并没有匹配项:
// 查找 "V",然后匹配 [o 或 i],之后匹配 "la"
alert( "Voila".match(/V[oi]la/) ); // null,无匹配项这个模式会搜索:
V,- 然后匹配其中的 一个字符
[oi], - 然后匹配
la。 所以可以匹配上Vola或者Vila。
- 范围
- 方括号也可以包含 字符范围。
- 例如,
[a-z]表示从a到z范围内的字符,[0-5]表示从0到5的数字。 - 在下面的示例中,我们将搜索首先是
"x",然后有两位数或两个在A到F范围内的字符紧随其后的字符串。
alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); // xAF[0-9A-F] 中有两个范围:它搜索一个字符,该字符要么是在 0 到 9 范围内的数字,要么是从 A 到 F 的字母。 如果我们还想查找小写字母,则可以添加范围 a-f:[0-9A-Fa-f]。或添加标志 i。 我们也可以在 […] 中使用字符类。 例如,如果我们想查找单词字符 \w 或连字符 -,则集合可以写为 [\w-]。 也可以组合多个类,例如 [\s\d] 表示“空格字符或数字”。 字符类是某些字符集合的简写
- \d —— 和
[0-9]相同, - \w —— 和
[a-zA-Z0-9_]相同, - \s —— 和
[\t\n\v\f\r ]外加少量罕见的 Unicode 空格字符相同。
- 示例:多语言 \w
- 由于字符类
\w是简写的[a-zA-Z0-9_],因此无法找到中文象形文字,西里尔字母等。 - 我们可以编写一个更通用的模式,该模式可以查找任何语言中的单词字符。借助 Unicode 属性很容易实现:
[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]。 - 让我们理解一下。类似于
\w,我们正在制作一组属于我们自己的包含具有以下 Unicode 属性的字符:Alphabetic(Alpha) —— 字母,Mark(M) —— 音调,Decimal_Number(Nd) —— 数字,Connector_Punctuation(Pc) —— 下划线'_'和类似的字符,Join_Control(Join_C) —— 两个特殊代码200c和200d,用于连字,例如阿拉伯语。
- 由于字符类
let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu;
let str = `Hi 你好 12`;
// 找出所有字母和数字:
alert( str.match(regexp) ); // H,i,你,好,1,2当然,我们可以编辑这个模式:添加 Unicode 属性或删除它们。Unicode:修饰符 "u" 和类 \p{...} 一文更详细地介绍了 Unicode 属性。 IE 浏览器不支持 Unicode 属性 IE 浏览器未实现 Unicode 属性 p{...}。如果我们真的需要它们,可以使用库 XRegExp。 或者只是使用我们感兴趣的语言中的字符范围,例如西里尔字母范围 [а-я]。
- 排除范围
- 除了普通的范围匹配,还有像这样
[^…]的“排除”范围匹配。 - 通过在开头添加插入符号
^来表示匹配所有 除了给定的字符 之外的任意字符。[^aeyo]—— 匹配除了'a'、'e'、'y'或'o'之外的任何字符。[^0-9]—— 匹配除了数字之外的任何字符,与\D作用相同。[^\s]—— 匹配任何非空格字符,与\S作用相同。
- 除了普通的范围匹配,还有像这样
[...]中的转义- 通常当我们想要准确地找到一个特殊字符时,我们需要像
\.这样对其进行转义。如果我们需要反斜杠,那么我们需要使用\\,等等。 - 在方括号,我们可以使用绝大多数特殊字符而无需转义:
- 符号
. + ( )无需转义。 - 在开头或结尾(未定义范围)的连字符
-不会被转义。 - 插入符号
^仅在开头会被转义(表示排除)。 - 右方括号
]总是会被转义(如果我们需要寻找那个符号)。
- 符号
- 除了在方括号中有特殊含义的字符外,其它所有特殊字符都是允许不转义的。
- 方括号中的点
.表示的就是一个点。模式[.,]将会搜索字符之一:点或逗号。 - 在下面的示例中,正则表达式
[-().^+]查找-().^+中的任何字符:
- 通常当我们想要准确地找到一个特殊字符时,我们需要像
// 不需要转义
let reg = /[-().^+]/g;
alert( "1 + 2 - 3".match(reg) ); // 匹配 +,-……但是如果你为了“以防万一”转义了它们,这也不会有任何问题:
// 转义其中的所有字符
let reg = /[\-\(\)\.\^\+]/g;
alert( "1 + 2 - 3".match(reg) ); // 仍能正常工作:+,-- 范围和修饰符 “u”
- 如果集合中有代理对(surrogate pairs),则需要标志
u才能使它们正常工作。 - 让我们在字符串
𝒳中查找[𝒳𝒴]:
- 如果集合中有代理对(surrogate pairs),则需要标志
alert( '𝒳'.match(/[𝒳𝒴]/) ); // 显示了一个奇怪的字符,像 [?]
//(搜索执行不正确,返回了半个字符)结果不正确,因为默认情况下正则表达式“不知道”代理对。 正则表达式引擎认为 [𝒳𝒴] —— 不是两个字符,而是四个字符: 1. 𝒳 的左半部分 (1), 2. 𝒳 的右半部分 (2), 3. 𝒴 的左半部分 (3), 4. 𝒴 的右半部分 (4)。
for(let i=0; i<'𝒳𝒴'.length; i++) {
alert('𝒳𝒴'.charCodeAt(i)); // 55349, 56499, 55349, 56500
};*量词 +, , ? 和 {n}
假设我们有一个像这样 +7(903)-123-45-67 的字符串,并想要找到其中所有数字。但与之前不同的是,我们对单个数字不感兴趣,只对全数感兴趣:7, 903, 123, 45, 67。 数字是一个或多个数字 \d 的序列。为了标记我们需要的数量,我们需要加一个 量词。
- 数量 {n}
- 最简单的量词便是大括号中的数字:
{n}。 - 在一个字符(或一个字符类,或
[...]等)后附加一个量词,用来指出我们具体需要的数量。 - 它有一些高级的形式,让我们看一些例子:
- 确切的位数:
{5}\d{5}表示 5 位数,与\d\d\d\d\d相同。
- 范围:
{3,5},匹配 3-5 个- 要查找 3-5 位的数字,我们可以将限制写在花括号中:
\d{3,5}
- 要查找 3-5 位的数字,我们可以将限制写在花括号中:
- 确切的位数:
- 最简单的量词便是大括号中的数字:
- 缩写
- 大多数常用的量词都有简写形式:
+。代表“一个或多个”,与{1,}相同。
JavaScriptlet str = "+7(903)-123-45-67"; alert( str.match(/\d+/g) ); // 7,903,123,45,67?:代表“零个或一个”,与{0,1}相同。换句话说,它使得符号变得可选。- 所以
colou?r会找到color和colour:
JavaScriptlet str = "Should I write color or colour?"; alert( str.match(/colou?r/g) ); // color, colour*:代表“零个及以上”,与{0,}相同。也就是说,字符可以出现任何次数或者不出现。
- 大多数常用的量词都有简写形式:
alert( "100 10 1".match(/\d0*/g) ); // 100, 10, 1将其与 +(一个或更多)做比较:
alert( "100 10 1".match(/\d0+/g) ); // 100, 10
// 1 没有被匹配出来,因为 0+ 要求至少有一个 0- 更多示例
- 量词是很常用的。它们是构成复杂正则表达式的主要“模块”,所以让我们看更多示例。
- 小数的正则表达式(带浮点的数字):
\d+\.\d+
lert( "0 1 12.345 7890".match(/\d+\.\d+/g) ); // 12.345”没有特性(attribute)的打开的 HTML 标签(例如 <span> 或 <p>)“的正则表达式。 最简单的:/<[a-z]+>/i:
alert( "<body> ... </body>".match(/<[a-z]+>/gi) ); // <body>正则表达式查找的匹配项是字符 '<' 后跟一个或多个拉丁字母,然后是 '>'。 进阶版:/<[a-z][a-z0-9]*>/i:
alert( "<h1>Hi!</h1>".match(/<[a-z][a-z0-9]*>/gi) ); // <h1>“打开或关闭的不带特性的 HTML 标签”的正则表达式:/<\/?[a-z][a-z0-9]*>/i 我们在模式开头附近添加了一个可选的斜杠 /?。必须用一个反斜杠转义它,否则 JavaScript 会认为它是这个模式的结束符。
alert( "<h1>Hi!</h1>".match(/<\/?[a-z][a-z0-9]*>/gi) ); // <h1>, </h1>为了使正则表达式更精确,我们通常需要使其更复杂 我们能够从这些例子中看到一个共同的规则:正则表达式越精确 —— 它就越长越复杂。 例如,对于 HTML 标签,我们可以使用更简单的正则表达式:<\w+>。但是由于 HTML 对标签名称有更严格的限制,所以 <[a-z][a-z0-9]*> 更可靠 我们可以使用 <\w+> 还是需要 <[a-z][a-z0-9]*>? 在实际开发中,这两种变体都是可以接受的。具体用哪个取决于我们对于“额外”匹配的宽容程度,以及通过其他方式将它们从结果中删除的困难程度。
贪婪量词和惰性量词
量词乍一看非常简单,但实际上它们可能很棘手。 如果我们打算寻找比 /\d+/ 更复杂的东西,就需要理解搜索的工作原理。 有一个文本,我们需要用书名号:«...» 来代替所有的引号 "..."。在许多国家,书名号是排版的首选。 例如:"Hello, world" 应该变成 «Hello, world»。还有其他引用,例如 „Witam, świat!”(波兰语)或 「你好,世界」(中文),但对于我们的任务,让我们选择 «...» 吧。 像 /".+"/g(一个引号,然后是一些内容,然后是另一个引号)这样的正则表达式看起来可能很合适,但事实并非如此!
- 贪婪搜索
- 为了查找到一个匹配项,正则表达式引擎采用了以下算法:
- 对于字符串中的每一个位置
- 尝试匹配该位置的模式。
- 如果未匹配,则转到下一个位置。
- 对于字符串中的每一个位置
- 该模式的第一个字符是一个引号
"。- 正则表达式引擎尝试在源字符串
a "witch" and her "broom" is one的位置 0 找到它,但那里有a,所以匹配失败。 - 然后继续前进:移至源字符串中的下一个位置,并尝试匹配模式中的第一个字符,再次失败,最终在第三个位置匹配到了引号
- 正则表达式引擎尝试在源字符串
- 找到引号后,引擎就尝试去匹配模式中的剩余字符。它尝试查看剩余的字符串是否符合
.+"。- 在我们的用例中,模式中的下一个字符为
.(一个点)。它表示匹配除了换行符之外的任意字符,所以将会匹配下一个字符'w'
- 在我们的用例中,模式中的下一个字符为
- 然后由于量词
.+,点会重复。正则表达式引擎一个接一个字符地进行匹配。- ……什么时候会不匹配?点(.)能够匹配所有字符,所以只有在移至字符串末尾时才停止匹配
- 现在引擎完成了对重复模式
.+的搜索,并且试图寻找模式中的下一个字符。是引号"。但是有一个问题:对字符串的遍历已经结束,没有更多字符了!- 正则表达式引擎知道它为
.+匹配太多项了,所以开始 回溯。 - 现在它假设
.+的匹配在字符串的倒数第一个字符前的位置结束,并尝试从该位置匹配模式的剩余部分。 - 如果那里有引号,则搜索将结束,但最后一个字符是
'e',所以不匹配。
- 正则表达式引擎知道它为
- ……所以引擎会将
.+的重复次数减少一个字符- 引号
'"'与'n'不匹配。
- 引号
- 引擎不断进行回溯:它减少
'.'的重复次数,直到模式的其余部分(在我们的用例中是'"')匹配到结果 - 所以,第一次匹配项是
"witch" and her "broom"。如果正则表达式具有修饰符g,则搜索将从第一个匹配结束的地方继续。字符串is one的剩余部分不再有引号,因此没有更多匹配项。 - 在贪婪模式下(默认情况),量词都会尽可能多地重复。
- 正则表达式引擎尝试用
.+去匹配尽可能多的字符,然后在模式的其余部分不匹配时再将其逐一缩短。
- 正则表达式引擎尝试用
- 为了查找到一个匹配项,正则表达式引擎采用了以下算法:
- 惰性模式
- 惰性模式中的量词与贪婪模式中的是相反的。它表示:“重复最少的次数”。
- 我们可以通过在量词后面添加一个问号
'?'来启用它,这样匹配模式就变成了*?或+?,甚至将'?'变成??。 - 这么说吧:通常问号
?本身就是一个量词(0 或 1),但如果将其放到 另一个量词(甚至是它自己)后面,就会有不同的含义 —— 它将匹配的模式从贪婪转为惰性。 - 正则表达式
/".+?"/g能够按预期工作了:它找到了"witch"和"broom":JavaScriptlet regexp = /".+?"/g; let str = 'a "witch" and her "broom" is one'; alert( str.match(regexp) ); // "witch", "broom"
'"'2. 下一步也是类似的:引擎为'.'找到了一个匹配项 3. 接下来的搜索就有些不同了。因为我们对+?启用了惰性模式,引擎不会去尝试多匹配一个点的匹配字符,而会停止并立即尝试对剩余的模式'"'进行匹配 4. 接着,正则表达式引擎增加对点的重复搜索次数,并且再次尝试 5. ……直到找到了模式中的剩余部分的匹配项 6. 接下来的搜索从当前匹配的结尾开始,并产生了下一个匹配项 5. 惰性模式仅对带有?的量词启用 1. 其它量词依旧保持贪婪模式。 1. 模式\d+尝试匹配尽可能多的数字(贪婪模式),因此在它找到123时停止,因为下一个字符为空格' '。 2. 然后模式中有一个空格,正好匹配。 3. 然后是\d+?。此量词处于惰性模式,所以它匹配一个数字4后开始尝试去检查模式的剩余部分是否匹配。 4. ……但是在\d+?之后没有其它内容了。 - 替代方法
- 使用正则表达式,通常有不止一种方式可以做相同的事。
- 在例子中,我们可以在不启用惰性模式的情况下使用正则表达式
"[^"]+"找到带引号的字符串
let regexp = /"[^"]+"/g;
let str = 'a "witch" and her "broom" is one';
alert( str.match(regexp) ); // "witch", "broom"正则表达式 "[^"]+" 给出了正确答案,因为它查找一个引号 '"' 后跟一个或更多非引号 [^"] 的字符,然后是结束的引号。 当引擎寻找 [^"]+ 时,它会在匹配到结束的引号时停止重复,这样就完成了。 这个逻辑并不能取代惰性量词! 它们是不同的。我们在不同情况下可能会需要使用到其中的一个或另一个。 让我们再来看一个使用惰性量词失败而使用这种变体能获得预期结果的例子。
let str = '...<a href="link" class="doc">...';
let regexp = /<a href=".*" class="doc">/g;
// 有效!
alert( str.match(regexp) ); // <a href="link" class="doc">……但如果文本中有多个链接呢?
let str = '...<a href="link1" class="doc">... <a href="link2" class="doc">...';
let regexp = /<a href=".*" class="doc">/g;
// 蛤!一个匹配项中有两个链接!
alert( str.match(regexp) ); // <a href="link1" class="doc">... <a href="link2" class="doc">现在这个结果是错的,原因与我们的 “witches” 示例相同。量词 .* 占用了太多字符。 让我们启用惰性量词 .*? 来修改模式:
let str = '...<a href="link1" class="doc">... <a href="link2" class="doc">...';
let regexp = /<a href=".*?" class="doc">/g;
// 正确了!
alert( str.match(regexp) ); // <a href="link1" class="doc">, <a href="link2" class="doc">捕获组
模式的一部分可以用括号括起来 (...)。这被称为“捕获组(capturing group)”。 这有两个影响: 1. 它允许将匹配的一部分作为结果数组中的单独项。 2. 如果我们将量词放在括号后,则它将括号视为一个整体。
- 示例
- 示例:gogogo
- 不带括号,模式
go+表示g字符,其后o重复一次或多次。例如goooo或gooooooooo。 - 括号将字符组合,所以
(go)+匹配go,gogo,gogogo等。
JavaScriptalert( 'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo" - 不带括号,模式
- 示例:域名JavaScript
mail.com users.mail.com smith.users.mail.com //正如我们所看到的,一个域名由重复的单词组成,每个单词后面有一个点,除了最后一个单词。 //在正则表达式中是 `(\w+\.)+\w+` let regexp = /(\w+\.)+\w+/g; alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com - 示例:电子邮件
- 电子邮件的格式为:
name@domain。名称可以是任何单词,允许使用连字符和点。在正则表达式中为[-.\w]+
JavaScriptlet regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g; alert("my@mail.com @ his@site.com.uk".match(regexp)); // my@mail.com, his@site.com.uk - 电子邮件的格式为:
- 示例:gogogo
- 匹配中的括号的内容
- 括号被从左到右编号。正则引擎会记住它们各自匹配的内容,并允许在结果中获取它。
- 方法
str.match(regexp),如果regexp没有修饰符g,将查找第一个匹配项,并将它作为数组返回:- 在索引
0处:完整的匹配项。
- 在索引
- 在索引
1处:第一个括号的内容。 - 在索引
2处:第二个括号的内容。 - ……等等……
let str = '<h1>Hello, world!</h1>';
let tag = str.match(/<(.*?)>/);
alert( tag[0] ); // <h1>
alert( tag[1] ); // h1- 嵌套组
- 括号可以嵌套。在这种情况下,编号也从左到右。
- 例如,在搜索标签
<span class="my">时,我们可能会对以下内容感兴趣:- 整个标签的内容:
span class="my"。 - 标签名称:
span。 - 标签特性:
class="my"。
- 整个标签的内容:
- 可选组
- 即使组是可选的并且在匹配项中不存在(例如,具有量词
(...)?),也存在相应的result数组项,并且等于undefined。 - 例如,让我们考虑正则表达式
a(z)?(c)?。它查找"a",后面是可选的"z",然后是可选的"c"。 - 如果我们在单个字母的字符串上运行
a,则结果为:
- 即使组是可选的并且在匹配项中不存在(例如,具有量词
let match = 'a'.match(/a(z)?(c)?/);
alert( match.length ); // 3
alert( match[0] ); // a(完整的匹配项)
alert( match[1] ); // undefined
alert( match[2] ); // undefined数组的长度为 3,但所有组均为空。 对字符串 ac 的匹配会更复杂:
let match = 'ac'.match(/a(z)?(c)?/)
alert( match.length ); // 3
alert( match[0] ); // ac(完整的匹配项)
alert( match[1] ); // undefined, 因为没有 (z)? 的匹配项
alert( match[2] ); // c数组长度依然是:3。但没有组 (z)? 的匹配项,所以结果是 ["ac", undefined, "c"]。 5. 带有组搜索所有匹配项:matchAll 1. matchAll 是一个新方法,可能需要使用 polyfill 1. 旧的浏览器不支持 matchAll。 2. 可能需要进行 polyfill,例如 https://github.com/ljharb/String.prototype.matchAll. 2. 为什么 matchAll 的结果是可迭代对象而不是数组? 1. 调用 matchAll 不会执行搜索。相反,它返回一个可迭代对象,最初没有结果。每次我们迭代它时才会执行搜索,例如在循环中。 2. 因此,这将根据需要找出尽可能多的结果,而不是全部。 3. 例如,文本中可能有 100 个匹配项,但在一个 for..of 循环中,我们找到了 5 个匹配项,然后觉得足够了并做出一个 break。这时引擎就不会花时间查找其他 95 个匹配。 6. 命名组 1. 用数字记录组很困难。对于简单的模式,它是可行的,但对于更复杂的模式,计算括号很不方便。我们有一个更好的选择:给括号命名。 2. 在左括号后紧跟着放置 ?<name> 即可完成对括号的命名。 3. 让我们查找 “year-month-day” 格式的日期:
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;
alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30- 替换中的捕获组
- 让我们能够替换
str中regexp的所有匹配项的方法str.replace(regexp, replacement)允许我们在replacement字符串中使用括号中的内容。这使用$n来完成,其中n是组号。
- 让我们能够替换
let str = "John Bull";
let regexp = /(\w+) (\w+)/;
alert( str.replace(regexp, '$2, $1') ); // Bull, John对于命名的括号,引用为 $<name>。 例如,让我们将日期格式从 “year-month-day” 更改为 “day.month.year”:
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30, 2020-01-01";
alert( str.replace(regexp, '$<day>.$<month>.$<year>') );
// 30.10.2019, 01.01.2020- 非捕获组 ?:
- 有时我们需要用括号才能正确应用量词,但我们不希望它们的内容出现在结果中。
- 可以通过在开头添加
?:来排除组。 - 例如,如果我们要查找
(go)+,但不希望括号内容(go)作为一个单独的数组项,则可以编写:(?:go)+。 - 在下面的示例中,我们仅将名称
John作为匹配项的单独成员:
let str = "Gogogo John!";
// ?: 从捕获组中排除 'go'
let regexp = /(?:go)+ (\w+)/i;
let result = str.match(regexp);
alert( result[0] ); // Gogogo John(完整的匹配项)
alert( result[1] ); // John
alert( result.length ); // 2(在数组中没有其他数组项)模式中的反向引用:\N 和 \k<name>
我们不仅可以在结果或替换字符串中使用捕获组 (...) 的内容,还可以在模式本身中使用它们。
- 按编号反向引用:\N
- 可以使用
\N在模式中引用一个组,其中N是组号。 - 我们需要找到带引号的字符串:单引号
'...'或双引号"..."—— 应匹配这两种变体。 - 我们可以将两种引号都放在方括号中:
['"](.*?)['"],但它会找到带有混合引号的字符串,例如"...'和'..."。当一种引号出现在另一种引号内,比如在字符串"She's the one!"中时,便会导致不正确的匹配:
- 可以使用
let str = `He said: "She's the one!".`;
let regexp = /['"](.*?)['"]/g;
// 不是我们想要的结果
alert( str.match(regexp) ); // "She'正如我们所看到的,该模式找到了一个开头的引号 ",然后文本被匹配,直到另一个引号 ',该匹配结束。 为了确保模式查找的结束引号与开始的引号完全相同,我们可以将其包装到捕获组中并对其进行反向引用:(['"])(.*?)\1。 这是正确的代码:
let str = `He said: "She's the one!".`;
let regexp = /(['"])(.*?)\1/g;
alert( str.match(regexp) ); // "She's the one!"现在可以了!正则表达式引擎会找到第一个引号 (['"]) 并记住其内容。那是第一个捕获组。 在模式中 \1 表示“找到与第一组相同的文本”,在我们的示例中为完全相同的引号。 与此类似,\2 表示第二组的内容,\3 —— 第三分组,依此类推。 不要搞混了:在模式中用 \1,在替换项中用:$1 在替换字符串中我们使用美元符号:$1,而在模式中 —— 使用反斜杠 \1。 2. 按命名反向引用:\k<name> 1. 如果一个正则表达式中有很多括号,给它们起个名字会便于引用。 2. 要引用命名的捕获组,我们可以使用:\k<name>。 3. 在下面的示例中,带引号的组被命名为 ?<quote>,因此反向引用为 \k<quote>:
let str = `He said: "She's the one!".`;
let regexp = /(?<quote>['"])(.*?)\k<quote>/g;
alert( str.match(regexp) ); // "She's the one!"选择 (OR) |
选择是正则表达式中的一个术语,实际上是一个简单的“或”。 在正则表达式中,它用竖线 | 表示。 例如,我们想要找出编程语言:HTML、PHP、Java 或 JavaScript。 对应的正则表达式为:html|php|java(script)?。
let regexp = /html|php|css|java(script)?/gi;
let str = "First HTML appeared, then CSS, then JavaScript";
alert( str.match(regexp) ); // 'HTML', 'CSS', 'JavaScript'我们看到过类似的东西 —— 方括号。它允许我们在多个字符中进行选择,例如 gr[ae]y 匹配 gray 或 grey。
方括号只允许字符或字符类。选择允许任何表达式。正则表达式 A|B|C 表示表达式 A、B 或 C 其一均可。 例如:
gr(a|e)y等同于gr[ae]y。gra|ey表示gra或ey。 要将选择应用于模式中一部分内容的选择,我们可以将其括在括号中:I love HTML|CSS匹配I love HTML或CSS。I love (HTML|CSS)匹配I love HTML或I love CSS。
- 示例:用于时间匹配的正则表达式
- 在之前的章节中有个任务是构建用于查找形如
hh:mm的时间字符串,例如12:00。但是简单的\d\d:\d\d太模糊了。它也会匹配25:99(因为 25 和 99 都与模式匹配,但这不是有效的时间)。 - 我们可以应用更精细的匹配。首先,对于时间:
- 如果第一位数是
0或1,那么下一位数可以是任何数值:[01]\d。 - 否则,如果第一位数是
2,那么下一位数必须是[0-3]。 - 不允许其他的首位数。
- 如果第一位数是
- 我们可以使用选择在正则表达式中编写这两种变体:
[01]\d|2[0-3]。 - 分钟必须为从
00到59的数。写成正则表达式即为[0-5]\d:第一个数字0-5,然后是任何数字。 - 如果我们将小时和分钟的正则表达式组合在一起,我们会得到:
[01]\d|2[0-3]:[0-5]\d - 我们差不多完成了,但有一个问题。选择
|现在恰好位于[01]\d和2[0-3]:[0-5]\d之间。
- 在之前的章节中有个任务是构建用于查找形如
前瞻断言与后瞻断言
有时我们只需要为一个模式找到那些在另一个模式之后或之前的匹配项。 有一种特殊的语法,称为“前瞻断言(lookahead)”和“后瞻断言(lookbehind)”。 首先,让我们从字符串中查找价格,例如 1 turkey costs 30€。即:一个数字,后跟€符号。
- 前瞻断言
- 语法为:
x(?=y),它表示“仅在后面是Y时匹配X”。这里的X和Y可以是任何模式。 - 那么对于一个后面跟着
€的整数,正则表达式应该为:\d+(?=€)。
JavaScriptlet str = "1 turkey costs 30€"; alert( str.match(/\d+(?=€)/) ); // 30,数字 1 被忽略了,因为它后面没有 €- 更复杂的测试也是可能的,例如
X(?=Y)(?=Z)表示:- 寻找
X。 - 检查
Y是否紧跟在X之后(如果不是则跳过)。 - 检查
Z是否也在X之后(如果不是则跳过)。 - 如果两个测试都通过了,那么
X是匹配的,否则继续搜索。
- 寻找
- 语法为:
- 否定的前瞻断言
- 假设我们想要一个数量,而不是来自同一字符串的价格。那是一个数字
\d+,后面不是€。 - 为此,我们可以使用否定的前瞻断言。
- 语法是:
X(?!Y),意思是“搜索X,但前提是后面没有Y”。
- 假设我们想要一个数量,而不是来自同一字符串的价格。那是一个数字
let str = "2 turkeys cost 60€";
alert( str.match(/\d+\b(?!€)/g) ); // 2(价格不匹配)- 后瞻断言
- 后瞻断言的浏览器兼容情况
- 非 V8 引擎的浏览器不支持后瞻断言,例如 Safari、Internet Explorer。
- 前瞻断言允许添加一个“后面要跟着什么”的条件判断。
- 语法为如下:
- 肯定的后瞻断言:
(?<=Y)X,匹配X,仅在前面是Y的情况下。 - 否定的后瞻断言:
(?<!Y)X,匹配X,仅在前面不是Y的情况下。
- 肯定的后瞻断言:
- 后瞻断言的浏览器兼容情况
- 捕获组
- 一般来说,前瞻断言和后瞻断言括号中的内容不会成为结果的一部分。
- 在模式
\d+(?!€)中,€符号就不会出现在匹配结果中。这是很自然的事:我们寻找一个数字\d+,而(?=€)只是一个测试,表示要匹配的数字后面应该紧跟着€字符。 - 但在某些情况下,我们可能还想捕获前瞻断言和后瞻断言所匹配的内容,或者部分内容。这也是可行的。只需要将该部分包装在额外的括号中。
let str = "1 turkey costs 30€";
let regexp = /\d+(?=(€|kr))/; // €|kr 两侧有额外的括号
alert( str.match(regexp) ); // 30, €
//后瞻断言也一样:
let str = "1 turkey costs $30";
let regexp = /(?<=(\$|£))\d+/;
alert( str.match(regexp) ); // 30, $灾难性回溯
有些正则表达式看起来很简单,但执行起来耗时却非常长,甚至会导致 JavaScript 引擎“挂起”。 大多数开发者迟早会遇到这样的情况。典型的症状就是 —— 正则表达式有时可以正常工作,但对于某些字符串,它会消耗 100% 的 CPU 算力,出现“挂起”的现象。 在这种情况下,Web 浏览器会建议终止脚本并重新加载页面。这显然不是我们愿意看到的。 对于服务器端 JavaScript,这样的正则表达式可能会挂起服务器进程,这甚至更糟。所以我们绝对应该研究研究它。
- 举例 假设,我们现在有一个字符串,我们想检查其中是否包含一些后面跟着可选空格
\s?的单词\w+。 构造此正则表达式最显而易见的方式是一个单词后跟着一个可选空格\w+\s?,然后用*重复它。 写成正则表达式即^(\w+\s?)*$,它指定 0 个及以上这样的词,从开头^开始,并在行的结尾$结束。 运行一下:
let regexp = /^(\w+\s?)*$/;
alert( regexp.test("A good string") ); // true
alert( regexp.test("Bad characters: $@#") ); // false
//,由于 JavaScript 会进导致“挂起”,所以你可能什么结果都看不到。此时浏览器会停止对事件的响应,UI 也会停止工作。一段时间之后,浏览器会建议重新加载页面。所以请谨慎对待:
let regexp = /^(\w+\s?)*$/;
let str = "An input string that takes a long time or even makes this regexp hang!";
// 会耗费很长时间
alert( regexp.test(str) );- 简化的例子 问题出在哪?为什么正则表达式会导致“挂起”? 为了理解它,我们来简化一下例子:移除空格符
\s?,使其简化为^(\w+)*$。 同时为了让问题更明显,再用\d替换掉\w。生成的新正则表达式执行时仍会导致挂起,例如:
let regexp = /^(\d+)*$/;
let str = "012345678901234567890123456789z";
// 会消耗很长时间(请小心!)
alert( regexp.test(str) );所以正则表达式哪里出了问题? 首先,有人可能会注意到这个正则表达式的 (\d+)* 部分有点奇怪。量词 * 看起来没什么必要。如果我们要匹配一个数字,那可以使用 \d+。 实际上,正则表达式很死板。我们通过简化前面的例子得到了一个简化版的正则表达式。但慢的原因是一样的。所以让我们来理解一下它的执行过程,然后问题的原因就会显而易见了。 在 123456789z 这行(清楚起见,这里缩短了字符串,请注意末尾的非数字字符 z,这很重要)中搜索 ^(\d+)*$ 时到底发生了什么,为什么耗时这么久? 下面是正则表达式引擎的执行过程: 首先,正则表达式引擎尝试查找括号中的内容:数字 \d+。加号 + 默认为贪婪模式,所以它消耗了所有数字。 消耗完所有数字后,认为找到了 \d+(如 123456789)。 然后它尝试应用星号量词,但此时已经没有更多数字了,所以星号没有给出任何信息。 模式中接下来的 $ 匹配字符串的结束,但是我们例子的文字中有 z,所以匹配失败 由于没有匹配结果,贪婪量词 + 的重复匹配次数会减一,并回溯一个字符。 现在 \d+ 会匹配除了最后一个数字之外的所有数字(12345678) 然后引擎尝试从新位置 (9) 继续搜索。 星号 (\d+)* 可以成功应用 —— 它匹配到了数字 9 没有匹配结果,所以引擎继续回溯,减少重复匹配次数。回溯通常是这样工作的:最后一个贪婪量词逐渐减少重复次数,直到达到最小值。然后前一个贪婪量词再减少重复次数,以此类推。
- 回到单词和字符串 在我们第一个例子中,当我们用
^(\w+\s?)*$这种模式在字符串An input that hangs!中查找单词时,就会发生类似的问题。 就是因为一个单词\w+可以被表示成很多种 以我们人的角度来看,很显然它无法匹配成功,因为示例中的字符串以叹号!结尾,然而正则表达式期望在的是一个单词\w末尾有或没有空格\s。但引擎理解不了这种状况。 它尝试了(\w+\s?)*的所有排列组合试图去囊括整个字符串,包括带空格(\w+\s)*的情形和不带空格(\w+)*的情形(因为空格\s?是可选的)。由于各种排列组合的数量(我们已经通过计算直观感受过了)太多了,所以耗费了大量时间去查询。 那怎么办? 我们应该改用懒惰模式吗? 不幸的是,这没用:如果我们用\w+?去替代\w+,还是会挂起。排列组合的顺序会变化,但是总数不变。 有些正则表达式引擎具有对棘手内容的测试和自动化有限处理,可以避免遍历所有排列组合来优化速度,但大多数引擎没有,而且也不是在所有情况下都有效果。 - 如何解决?
- 主要有 2 种解决方式。
- 第一种是减少可能的组合数量。
- 让我们把正则表达式重写为
^(\w+\s)*\w*$以使空格变为非可选的 —— 我们将查找任意数量的单词后跟空格(\w+\s)*,然后跟着最后一个单词\w*(可选)。 - 这个正则表达式等同于之前那个(匹配内容相同),并且运行起来也没问题:
- 让我们把正则表达式重写为
let regexp = /^(\w+\s)*\w*$/;
let str = "An input string that takes a long time or even makes this regex hang!";
alert( regexp.test(str) ); // false为什么问题消失了? 因为现在空格是强制性的。 前面的正则表达式,如果我们省略空格,就会变成 (\w+)*,导致单个单词中有很多 \w+ 组合 。 所以 input 可以匹配为 \w+ 的两次重复,如下所示:
\w+ \w+
(inp)(ut)新模式有所不同:(\w+\s)* 指定单词的重复后面跟着一个空格!input 字符串不能匹配为 \w+\s 的两次重复,因为空格是强制性的。 现在节省了尝试大量(实际上是大多数)组合所需的时间。
- 防止回溯 有时候重写正则表达式会比较麻烦。在上面的示例中,这很容易,但如何做到这一点并不总是很明显。 此外,重写的正则表达式通常更复杂,这并不好。在不做其他更改的情况下,正则表达式已经够复杂了。 幸运的是,还有另一种方式。我们可以禁止量词的回溯。 问题的根源在于正则表达式引擎尝试了许多对人类看来显然是错误的组合。 例如,正则表达式
(\d+)*$中+对于我们人类来说很明显不应去回溯。就算我们用两个单独的\d+\d+去替换一个\d+,也根本没变化 在原先的那个例子^(\w+\s?)*$中,我们可能希望在\w+中禁止回溯。即:\w+应该匹配一个完整的单词,并且具有最大可能的长度。无需降低\w+的重复次数或将其拆分为两个单词\w+\w+等等。 为此,现代正则表达式引擎支持占有型量词(Possessive Quantifiers)。如果我们在常规量词之后添加+,则常规量词就变成了占有型量词。也就是说,我们可以使用\d++替代\d+来阻止+回溯。 占有型量词实际上比“常规”量词更简单。它们只是尽可能多地匹配,没有任何回溯。没有回溯的搜索过程更简单。 还有所谓的“原子捕获组” —— 一种禁用括号内回溯的方法。 ……但坏消息是,JavaScript 并不支持它。 我们可以通过使用“前瞻变换(lookahead transform)”来模拟它们。 - 用前瞻视角解决问题 所以,我们来到了真正的高阶主题。我们希望量词,例如
+不要回溯,因为有时回溯没有意义。 在不回溯的情况下尽可能多地重复\w的模式可以写为:(?=(\w+))\1。当然,我们可以采用另一种模式来代替\w。 这可能看起来很奇怪,但它实际上是一个非常简单的转换。 让我们解读一下:- 前瞻断言
?=从当前位置开始,向前查找最长的单词\w+。 - 引擎不会去记住带有
?=...的括号中的内容。所以将\w+放入括号中,这样引擎就会记住这些内容了。 - ……然后用
\1来引用括号中的内容。 也就是说:我们先进行前瞻查找 —— 如果有符合\w+的单词,我们就可将其匹配为\1。 为什么?因为前瞻断言查找到一个单词\w+,将其作为一个整体,然后将其捕获为\1。所以我们最终实现了一种占有型加号+量词。它只捕获整个单词\w+,而不会只捕获一部分。 例如,在单词JavaScript中不仅可以匹配Java,而且可以忽略Script,以匹配模式的其余部分。 下面是 2 个模式的对比:
- 前瞻断言
alert( "JavaScript".match(/\w+Script/)); // JavaScript
alert( "JavaScript".match(/(?=(\w+))\1Script/)); // null粘性修饰符 "y",在位置处搜索
y 修饰符让我们能够在源字符串中的指定位置进行搜索。 为了掌握 y 修饰符的使用方式,让我们来看一个实际的例子。 正则表达式的常见任务之一就是“词法分析”:例如我们得到了一个代码文本,我们需要找到它的结构元素。例如,HTML 有标签和特性(attribute),JavaScript 代码有函数、变量等。 编写词法分析器是一个特殊的领域,有自己的工具和算法,所以我们不做过多的深入,但有一个共同的任务:在给定的位置读取一些内容。 例如,我们有一个代码字符串 let varName = "value",我们需要从中读取变量名,这个变量名从位置 4 开始。 我们将使用正则表达式 \w+ 来查找变量名。实际上,JavaScript 的变量名需要更复杂的正则表达式才能准确匹配,但在这里并不重要。
- 调用
str.match(/\w+/)将只会找到该行中的第一个单词 (let)。不是这个。 - 我们可以添加修饰符
g。但是调用str.match(/\w+/g)会查找文本中的所有单词,而我们需要位置4的一个单词。同样,不是我们需要的。 那么,如何在给定位置准确搜索正则表达式?。 让我们尝试使用方法regexp.exec(str)。 对于没有修饰符g和y的regexp,此方法仅查找第一个匹配项,就像str.match(regexp)一样。 ……但是如果有修饰符g,那么它就会从存储在regexp.lastIndex属性中的位置开始在字符串str中进行搜索。如果找到匹配项,则将在匹配后立即将regexp.lastIndex设置为索引。 换句话说,regexp.lastIndex作为搜索的起点,每个regexp.exec(str)调用都会将其重置为新值(“在最后一次匹配后”)。当然,这只是在有g修饰符的情况下才会这样。 因此,连续调用regexp.exec(str)会一个接一个地返回匹配。 以下是此类调用的示例:
let str = 'let varName'; // 让我们找出字符串中的所有单词
let regexp = /\w+/g;
alert(regexp.lastIndex); // 0 (初始值 lastIndex=0)
let word1 = regexp.exec(str);
alert(word1[0]); // let (第一个单词)
alert(regexp.lastIndex); // 3 (匹配后的位置)
let word2 = regexp.exec(str);
alert(word2[0]); // varName (第二个单词)
alert(regexp.lastIndex); // 11 (匹配后的位置)
let word3 = regexp.exec(str);
alert(word3); // null (没有更多匹配项)
alert(regexp.lastIndex); // 0 (搜索结束后重置索引)我们可以通过循环获取所有匹配。
let str = 'let varName';
let regexp = /\w+/g;
let result;
while (result = regexp.exec(str)) {
alert( `Found ${result[0]} at position ${result.index}` );
// 在位置 0 发现了 let,然后
// 在位置 4 发现 varName
}regexp.exec 的这种使用方式可以作为 str.matchAll 方法的替代,可以对匹配过程进行更多控制。 让我们回到我们的任务。 我们可以手动将 lastIndex 设置为 4,从给定的位置开始搜索! 像这样:
let str = 'let varName = "value"';
let regexp = /\w+/g; // 没有修饰符 "g",lastIndex 属性会被忽略
regexp.lastIndex = 4;
let word = regexp.exec(str);
alert(word); // varName哇塞!问题解决了! 我们从位置 regexp.lastIndex = 4 开始搜索 \w+。 结果是正确的。 ……但是等等,没那么快。 请注意:regexp.exec 调用从位置 lastIndex 开始搜索,然后继续搜索。如果 lastIndex 位置没有单词,但单词在这之后的某个位置,那么单词也会被找到:
let str = 'let varName = "value"';
let regexp = /\w+/g;
// 从位置 3 开始搜索
regexp.lastIndex = 3;
let word = regexp.exec(str);
// 在位置 4 找到匹配项
alert(word[0]); // varName
alert(word.index); // 4对于某些任务,包括词法分析,这是错误的。我们需要在文本的给定位置准确地找到匹配,而不是在它之后的某个位置。这就是修饰符 “y” 的用途。 修饰符 y 使 regexp.exec 精确搜索位置 lastIndex,而不是“从”它开始。 下面是带有修饰符 y 的相同搜索:
let str = 'let varName = "value"';
let regexp = /\w+/y;
regexp.lastIndex = 3;
alert( regexp.exec(str) ); // null(位置 3 有一个空格,不是单词)
regexp.lastIndex = 4;
alert( regexp.exec(str) ); // varName(在位置 4 的单词)正如我们所看到的,正则表达式 /\w+/y 在位置 3 处不匹配(不同于修饰符 g),但在位置 4 处匹配。 这不仅是我们所需要的,当使用修饰符 y 时,还有一个重要的性能提升。 想象一下,我们有一个很长的文本,其中根本没有匹配项。然后使用修饰符 g 进行搜索,会一直搜索到文本的末尾,并且什么也找不到,这将比使用修饰符 y 的搜索花费更多的时间,后者只检查确切的位置。 在像词法分析这样的任务中,通常会在一个确切的位置进行多次搜索,以检查我们在那里有什么。使用修饰符 y 是正确实现和良好性能的关键。
正则表达式和字符串的方法
- str.match(regexp)
str.match(regexp)方法在字符串str中查找regexp的匹配项。- 它有 3 种模式:
- 如果
regexp不带有修饰符g,则它以数组的形式返回第一个匹配项,其中包含捕获组和属性index(匹配项的位置)、input(输入字符串,等于str) - 如果
regexp带有修饰符g,则它将返回一个包含所有匹配项的数组,但不包含捕获组和其它详细信息。 - 如果没有匹配项,则无论是否带有修饰符
g,都将返回null。
- 如果
//1
let str = "I love JavaScript";
let result = str.match(/Java(Script)/);
alert( result[0] ); // JavaScript(完全匹配)
alert( result[1] ); // Script(第一个分组)
alert( result.length ); // 2
// 其他信息:
alert( result.index ); // 7(匹配位置)
alert( result.input ); // I love JavaScript(源字符串)
//2
let str = "I love JavaScript";
let result = str.match(/Java(Script)/g);
alert( result[0] ); // JavaScript
alert( result.length ); // 1
//3
let str = "I love JavaScript";
let result = str.match(/HTML/);
alert(result); // null
alert(result.length); // Error: Cannot read property 'length' of null- str.matchAll(regexp)
- 方法
str.matchAll(regexp)是str.match的“更新、改进”的变体。 - 它主要用来搜索所有组的所有匹配项。
- 与
match相比有 3 个区别:- 它返回一个包含匹配项的可迭代对象,而不是数组。我们可以用
Array.from将其转换为一个常规数组。 - 每个匹配项均以一个包含捕获组的数组形式返回(返回格式与不带修饰符
g的str.match相同)。 - 如果没有结果,则返回的是一个空的可迭代对象而不是
null。
- 它返回一个包含匹配项的可迭代对象,而不是数组。我们可以用
- 方法
let str = '<h1>Hello, world!</h1>';
let regexp = /<(.*?)>/g;
let matchAll = str.matchAll(regexp);
alert(matchAll); // [object RegExp String Iterator],不是数组,而是一个可迭代对象
matchAll = Array.from(matchAll); // 现在是数组了
let firstMatch = matchAll[0];
alert( firstMatch[0] ); // <h1>
alert( firstMatch[1] ); // h1
alert( firstMatch.index ); // 0
alert( firstMatch.input ); // <h1>Hello, world!</h1>- str.split(regexp|substr, limit)
- 使用正则表达式(或子字符串)作为分隔符来分割字符串。
- alert('12, 34, 56'.split(/,\s*/)) // 数组
['12', '34', '56']
- str.search(regexp)
- 方法
str.search(regexp)返回第一个匹配项的位置,如果没找到,则返回-1 - 重要限制:
search仅查找第一个匹配项。 - 如果我们需要其他匹配项的位置,则应使用其他方法,例如用
str.matchAll(regexp)查找所有位置。
- 方法
- str.replace(str|regexp, str|func)
- 这是用于搜索和替换的通用方法,是最有用的方法之一。它是搜索和替换字符串的瑞士军刀。
- 当
replace的第一个参数是字符串时,它只替换第一个匹配项。- 你可以在上面的示例中看到:只有第一个
"-"被替换为了":"。 - 如要找到所有的连字符,我们不应该用字符串
"-",而应使用带g修饰符的正则表达式/-/g
- 你可以在上面的示例中看到:只有第一个
- 对于需要“智能”替换的场景,第二个参数可以是一个函数。
- 每次匹配都会调用这个函数,并且返回的值将作为替换字符串插入。
- 该函数
func(match, p1, p2, ..., pn, offset, input, groups)带参数调用:match—— 匹配项,p1, p2, ..., pn—— 捕获组的内容(如有),offset—— 匹配项的位置,input—— 源字符串,groups—— 具有命名的捕获组的对象。
- str.replaceAll(str|regexp, str|func)
- 这个方法与
str.replace本质上是一样的,但有两个主要的区别:- 如果第一个参数是一个字符串,它会替换 所有出现的 和第一个参数相同的字符串,而
replace只会替换 第一个。
- 如果第一个参数是一个字符串,它会替换 所有出现的 和第一个参数相同的字符串,而
- 如果第一个参数是一个没有修饰符
g的正则表达式,则会报错。带有修饰符g,它的工作方式与replace相同。
- 这个方法与
- regexp.exec(str)
regexp.exec(str)方法返回字符串str中的regexp匹配项。与以前的方法不同,它是在正则表达式而不是在字符串上调用的。 它的行为取决于正则表达式是否具有修饰符g。 如果没有修饰符g,则regexp.exec(str)会返回与 第一个匹配项,就像str.match(regexp)那样。这种行为并没有带来任何新的东西。 但是,如果有修饰符g,那么:- 调用
regexp.exec(str)会返回第一个匹配项,并将紧随其后的位置保存在属性regexp.lastIndex中。 - 下一次这样的调用会从位置
regexp.lastIndex开始搜索,返回下一个匹配项,并将其后的位置保存在regexp.lastIndex中。 - ……以此类推。
- 如果没有匹配项,则
regexp.exec返回null,并将regexp.lastIndex重置为0。 因此,重复调用会一个接一个地返回所有匹配项,使用属性regexp.lastIndex来跟踪当前搜索位置。 过去,在将str.matchAll方法添加到 JavaScript 之前,会在循环中调用regexp.exec来获取组的所有匹配项:
- 调用
let str = 'More about JavaScript at https://javascript.info';
let regexp = /javascript/ig;
let result;
while (result = regexp.exec(str)) {
alert( `Found ${result[0]} at position ${result.index}` );
// 在位置 11 找到了 JavaScript,然后
// 在位置 33 找到了 javascript
}这现在也有效,尽管对于较新的浏览器 str.matchAll 通常更方便。 我们可以通过手动设置 lastIndex,用 regexp.exec 从给定位置进行搜索。
- regexp.test(str)
- 方法
regexp.test(str)查找匹配项,然后返回true/false表示是否存在。
- 方法
let str = "I love JavaScript";
// 这两个测试相同
alert( /love/i.test(str) ); // true
alert( str.search(/love/i) != -1 ); // true相同的全局正则表达式在不同的源字符串上测试可能会失败 如果我们在不同的源字符串上应用相同的全局正则表达式,可能会出现错误的结果,因为 regexp.test 的调用会增加 regexp.lastIndex 属性值,因此在另一个字符串中的搜索可能是从非 0 位置开始的。
