正则表达式是搜索和替换字符串的一种强大方式。
在 JavaScript 中,正则表达式通过内建的“RegExp”类的对象来实现,并与字符串集成。
请注意,在各编程语言之间,正则表达式是有所不同的。在本教程中,我们只专注于 JavaScript。
本文旨在汇总整理,非原创。
正则表达式
语法一:
regexp = new RegExp("pattern", "flags");
语法二:
regexp = /pattern/; // 没有修饰符
regexp = /pattern/gmi; // 伴随修饰符 g、m 和 i
区别:
/***/语法简练,但不接受传参,一般在确定匹配规则的情况下使用,new RegExp 允许从字符串中动态地构造模式
修饰符
在 JavaScript 中,有 6 个修饰符:
i
使用此修饰符后,搜索时不区分大小写: A 和 a 没有区别。
g
使用此修饰符后,搜索时会查找所有的匹配项,而不只是第一个。
m
多行模式。
s
启用 “dotall” 模式,允许点 . 匹配换行符 \n。
u
开启完整的 unicode 支持。该修饰符能够修正对于代理对的处理。
y
粘滞模式
字符类
字符类(Character classes) 是一个特殊的符号,匹配特定集中的任何符号。
\d(“d” 来自 “digit”)
数字:从 0 到 9 的字符。
\s(“s” 来自 “space”)
空格符号:包括空格,制表符 \t,换行符 \n 和其他少数稀有字符,例如 \v,\f 和 \r。
\w(“w” 来自 “word”)
“单字”字符:拉丁字母或数字或下划线 _。非拉丁字母(如西里尔字母或印地文)不属于 \w。
反向类
\D
非数字:除 \d 以外的任何字符,例如字母。
\S
非空格符号:除 \s 以外的任何字符,例如字母。
\W
非单字字符:除 \w 以外的任何字符,例如非拉丁字母或空格。
小结
\d —— 数字。
\D —— 非数字。
\s —— 空格符号,制表符,换行符。
\S —— 除了 \s 。
\w —— 拉丁字母,数字,下划线 '_'。
\W —— 除了 \w。
. —— 任何带有 's' 标志的字符,否则为除换行符 \n之外的任何字符。
锚点
插入符号 ^ 匹配文本开头,而美元符号 $ - 则匹配文本末尾。
什么字符串可以匹配模式 ^$?
空字符串 ""
"m" — 多行模式
通过 flag /.../m 可以开启多行模式。
这仅仅会影响 ^ 和 $ 锚符的行为。
在多行模式下,它们不仅仅匹配文本的开始与结束,还匹配每一行的开始与结束。
let str = `1st aaa
2nd bbb
33rd ccc`;
alert( str.match(/^\d+/gm) ); // 1, 2, 33
词边界:\b
当正则表达式引擎(实现搜索正则表达式的程序模块)遇到 \b 时,它会检查字符串中的位置是否是词边界。
alert( "Hello, Java!".match(/\bJava\b/) ); // Java
alert( "Hello, JavaScript!".match(/\bJava\b/) ); // null
集合
比如说,[eao] 意味着查找在 3 个字符 'a'、'e' 或者 ‘o’ 中的任意一个。
范围
方括号也可以包含字符范围。
比如说,[a-z] 会匹配从 a 到 z 范围内的字母,[0-9] 表示从 0 到 9 的数字。
\d —— 和 [0-9] 相同,
\w —— 和 [a-zA-Z0-9_] 相同,
\s —— 和 [\t\n\v\f\r ] 外加少量罕见的 unicode 空格字符相同。
排除范围
除了普通的范围匹配,还有类似 [^…] 的“排除”范围匹配。
[^aeyo] —— 匹配任何除了 'a'、'e'、'y' 或者 'o' 之外的字符。
[^0-9] —— 匹配任何除了数字之外的字符,也可以使用 \D 来表示。
[^\s] —— 匹配任何非空字符,也可以使用 \S 来表示。
量词 +,*,? 和 {n}
数量 {n} 明确的n次
某个范围的位数:{n,m} n-m次匹配区间
+
代表“一个或多个”,相当于 {1,}。
?
代表“零个或一个”,相当于 {0,1}。
*
代表着“零个或多个”,相当于 {0,}。也就是说,这个字符可以多次出现或不出现。
贪婪模式
在贪婪模式下(默认情况下),量词都会尽可能地重复多次。
懒惰模式
懒惰模式中的量词与贪婪模式中的是相反的。它想要“重复最少次数”。
我们能够通过在量词之后添加一个问号 '?' 来启用它,所以匹配模式变为 *? 或 +?,甚至将 '?' 变为 ??。
捕获组
模式的一部分可以用括号括起来 (...)。这称为“捕获组(capturing group)”。
这有两个影响:
- 它允许将匹配的一部分作为结果数组中的单独项。
- 如果我们将量词放在括号后,则它将括号视为一个整体。
alert( 'Gogogo now!'.match(/(go)+/i) ); // "Gogogo"
命名组
用数字记录组很困难。对于简单模式,它是可行的,但对于更复杂的模式,计算括号很不方便。我们有一个更好的选择:给括号起个名字。
这是通过在开始括号之后立即放置 ?<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;
alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30
替换捕获组
方法 str.replace(regexp, replacement) 用 replacement 替换 str 中匹配 regexp 的所有捕获组。这使用 $n 来完成,其中 n 是组号。
let str = "John Bull";
let regexp = /(\w+) (\w+)/;
alert( str.replace(regexp, '$2, $1') ); // Bull, John
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
非捕获组 ?:
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>
let str = `He said: "She's the one!".`;
let regexp = /(['"])(.*?)\1/g;
alert( str.match(regexp) ); // "She's the one!"
不要搞混了: 在模式中用 \1,在替换项中用:$1
在替换字符串中我们使用美元符号:$1,而在模式中 – 使用反斜杠 \1。
let str = `He said: "She's the one!".`;
let regexp = /(?<quote>['"])(.*?)\k<quote>/g;
alert( str.match(regexp) ); // "She's the one!"
选择(OR)|
let reg = /html|php|css|java(script)?/gi;
let str = "First HTML appeared, then CSS, then JavaScript";
alert( str.match(reg) ); // 'HTML', 'CSS', 'JavaScript'
gr(a|e)y 严格等同 gr[ae]y。
gra|ey 匹配 “gra” or “ey”。
前瞻断言与后瞻断言
前瞻断言
语法为:x(?=y),它表示“仅在后面是 y 的情况匹配 x”。
那么对于一个后面跟着 € 的整数金额,它的正则表达式应该为:\d+(?=€)。
前瞻否定断言
语法为:x(?!y),意思是 “查找 x, 但是仅在不被 y 跟随的情况下匹配成功”。
后瞻肯定断言
(?<=y)x, 匹配 x, 仅在前面是 y 的情况。
后瞻否定断言
(?<!y)x, 匹配 x, 仅在前面不是 y 的情况。
let str = "1 turkey costs 30€";
let reg = /\d+(?=(€|kr))/; // €|kr 两边有额外的括号
alert( str.match(reg) ); // 30, €
let str = "1 turkey costs $30";
let reg = /(?<=(\$|£))\d+/;
alert( str.match(reg) ); // 30, $
正则表达式(RegExp)和字符串(String)的方法
str.match(regexp)
如果 regexp 不带有 g 标记,则它以数组的形式返回第一个匹配项,其中包含分组和属性 index(匹配项的位置)、input(输入字符串,等于 str):
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(源字符串)
如果 regexp 带有 g 标记,则它将所有匹配项的数组作为字符串返回,而不包含分组和其他详细信息。
let str = "I love JavaScript";
let result = str.match(/Java(Script)/g);
alert( result[0] ); // JavaScript
alert( result.length ); // 1
如果没有匹配项,则无论是否带有标记 g ,都将返回 null。
这是一个重要的细微差别。如果没有匹配项,我们得到的不是一个空数组,而是 null。忘记这一点很容易出错,例如:
let str = "I love JavaScript";
let result = str.match(/HTML/);
alert(result); // null
alert(result.length); // Error: Cannot read property 'length' of null
如果我们希望结果是一个数组,我们可以这样写:
let result = str.match(regexp) || [];
str.matchAll(regexp)
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)
let str = "A drop of ink may make a million think";
alert( str.search( /ink/i ) ); // 10(第一个匹配位置)
str.replace(str|regexp, str|func)
// 用冒号替换连字符
alert('12-34-56'.replace("-", ":")) // 12:34-56
// 将连字符替换为冒号
alert( '12-34-56'.replace( /-/g, ":" ) ) // 12:34:56
第二个参数是一个替代字符串。我们可以在其中使用特殊字符:
| 符号 | 替换字符串中的操作 |
|---|---|
$& | 插入整个匹配项 |
| $` | 在匹配项之前插入字符串的一部分 |
$' | 在匹配项之后插入字符串的一部分 |
$n | 如果 n 是一个 1 到 2 位的数字,则插入第 n 个分组的内容,详见 捕获组 |
$<name> | 插入带有给定 name 的括号内的内容,详见 捕获组 |
$$ | 插入字符 $ |
let str = "John Smith";
// 交换名字和姓氏
alert(str.replace(/(john) (smith)/i, '$2, $1')) // Smith, John
对于需要“智能”替换的场景,第二个参数可以是一个函数。
每次匹配都会调用这个函数,并且返回的值将作为替换字符串插入。
该函数 func(match, p1, p2, ..., pn, offset, input, groups) 带参数调用:
match - 匹配项,
p1, p2, ..., pn - 分组的内容(如有),
offset - 匹配项的位置,
input - 源字符串,
groups - 所指定分组的对象。
如果正则表达式中没有括号,则只有 3 个参数:func(str, offset, input)。
let str = "html and css";
let result = str.replace(/html|css/gi, str => str.toUpperCase());
alert(result); // HTML and CSS
如果有许多组,用 rest 参数(…)可以很方便的访问:
let str = "John Smith";
let result = str.replace(/(\w+) (\w+)/, (...match) => `${match[2]}, ${match[1]}`);
alert(result); // Smith, John
或者,如果我们使用的是命名组,则带有它们的 groups 对象始终是最后一个对象,因此我们可以这样获得它:
let str = "John Smith";
let result = str.replace(/(?<name>\w+) (?<surname>\w+)/, (...match) => {
let groups = match.pop();
return `${groups.surname}, ${groups.name}`;
});
alert(result); // Smith, John
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}` );
// Found JavaScript at position 11,然后
// Found javascript at position 33
}
let str = 'Hello, world!';
let regexp = /\w+/g; // 不带标记 "g",lastIndex 属性会被忽略
regexp.lastIndex = 5; // 从第 5 个位置搜索(从逗号开始)
alert( regexp.exec(str) ); // world
regexp.test(str)
let str = "I love JavaScript";
// 这两个测试相同
alert( /love/i.test(str) ); // true
alert( str.search(/love/i) != -1 ); // true
let str = "Bla-bla-bla";
alert( /love/i.test(str) ); // false
alert( str.search(/love/i) != -1 ); // false
let regexp = /love/gi;
let str = "I love JavaScript";
// 从位置 10 开始:
regexp.lastIndex = 10;
alert( regexp.test(str) ); // false(无匹配)