RegExp)/u 使用 Unicode 模式regExp.test(str):是否存在匹配? [ES3]str.search(regExp):匹配位于哪个索引? [ES3]regExp.exec(str):捕获组 [ES3]str.match(regExp):获取所有组 0 捕获 [ES3]str.matchAll(regExp):获取所有匹配对象的迭代器 [ES2020]regExp.exec() vs. str.match() vs. str.matchAll()str.replace() 和 str.replaceAll() 替换/g 和 /y,以及属性 .lastIndex(高级)/g 和 /y/g 和 /y 影响?/g 和 /y 的四个陷阱以及如何处理它们.lastIndex 的用例:从给定索引开始匹配.lastIndex 的缺点.global (/g) 和 .sticky (/y) 功能可用性
除非另有说明,否则每个正则表达式功能自 ES3 起均可用。
创建正则表达式的两种主要方式是
字面量:静态编译(加载时)。
/abc/ui构造函数:动态编译(运行时)。
new RegExp('abc', 'ui')两种正则表达式都具有相同的两部分
abc – 实际的正则表达式。u 和 i。标志配置模式的解释方式。例如,i 启用不区分大小写的匹配。可用标志列表在本章稍后给出。构造函数 RegExp() 有两种变体
new RegExp(pattern : string, flags = '') [ES3]
根据通过 pattern 指定的内容创建一个新的正则表达式。如果缺少 flags,则使用空字符串 ''。
new RegExp(regExp : RegExp, flags = regExp.flags) [ES6]
克隆 regExp。如果提供了 flags,则它确定克隆的标志。
第二种变体对于克隆正则表达式很有用,可以选择同时修改它们。标志是不可变的,这是更改它们的唯一方法 – 例如
function copyAndAddFlags(regExp, flagsToAdd='') {
// The constructor doesn’t allow duplicate flags;
// make sure there aren’t any:
const newFlags = Array.from(
new Set(regExp.flags + flagsToAdd)
).join('');
return new RegExp(regExp, newFlags);
}
assert.equal(/abc/i.flags, 'i');
assert.equal(copyAndAddFlags(/abc/i, 'g').flags, 'gi');在正则表达式的顶层,以下语法字符是特殊的。它们通过在前面加上反斜杠 (\) 来转义。
\ ^ $ . * + ? ( ) [ ] { } |
在正则表达式字面量中,我们必须转义斜杠
> /\//.test('/')
true在 new RegExp() 的参数中,我们不必转义斜杠
> new RegExp('/').test('/')
true原子是正则表达式的基本构建块。
^、$ 等)之外的所有字符。模式字符匹配自身。示例:A b %. 匹配任何字符。我们可以使用标志 /s (dotAll) 来控制点是否匹配行终止符(更多内容见下文)。\f:换页 (FF)\n:换行 (LF)\r:回车 (CR)\t:字符制表符\v:行制表符\cA (Ctrl-A)、…、\cZ (Ctrl-Z)\u00E4/u):\u{1F44D}\d:数字(与 [0-9] 相同)\D:非数字\w:“单词”字符(与 [A-Za-z0-9_] 相同,与编程语言中的标识符相关)\W:非单词字符\s:空白(空格、制表符、行终止符等)\S:非空白\p{White_Space}、\P{White_Space} 等。/u。在 Unicode 标准中,每个字符都有属性 – 描述它的元数据。属性在定义字符的性质方面起着重要作用。引用Unicode 标准,第 3.3 节,D3
字符的语义由其标识、规范属性和行为决定。
以下是属性的一些示例
Name:由大写字母、数字、连字符和空格组成的唯一名称 – 例如Name = LATIN CAPITAL LETTER A🙂:Name = SLIGHTLY SMILING FACEGeneral_Category:对字符进行分类 – 例如General_Category = Lowercase_LetterGeneral_Category = Currency_SymbolWhite_Space:用于标记不可见的空格字符,例如空格、制表符和换行符 – 例如White_Space = TrueWhite_Space = FalseAge:引入字符的 Unicode 标准版本 – 例如:欧元符号 € 是在 Unicode 标准的 2.1 版本中添加的。Age = 2.1Block:连续的代码点范围。块不重叠,并且它们的名称是唯一的。例如Block = Basic_Latin(范围 U+0000..U+007F)🙂:Block = Emoticons(范围 U+1F600..U+1F64F)Script:是一个或多个书写系统使用的字符集合。Script = GreekScript = CyrillicUnicode 属性转义如下所示
\p{prop=value}:匹配属性 prop 的值为 value 的所有字符。\P{prop=value}:匹配属性 prop 的值不是 value 的所有字符。\p{bin_prop}:匹配二进制属性 bin_prop 为 True 的所有字符。\P{bin_prop}:匹配二进制属性 bin_prop 为 False 的所有字符。注释
仅当设置了标志 /u 时,我们才能使用 Unicode 属性转义。如果没有 /u,则 \p 与 p 相同。
如果属性是 General_Category,则可以使用形式 (3) 和 (4) 作为缩写。例如,以下两个转义是等效的
\p{Uppercase_Letter}
\p{General_Category=Uppercase_Letter}示例
检查空白
> /^\p{White_Space}+$/u.test('\t \n\r')
true检查希腊字母
> /^\p{Script=Greek}+$/u.test('μετά')
true删除任何字母
> '1π2ü3é4'.replace(/\p{Letter}/ug, '')
'1234'删除小写字母
> 'AbCdEf'.replace(/\p{Lowercase_Letter}/ug, '')
'ACE'进一步阅读
字符类将字符范围括在方括号中。字符范围指定一组字符
[«字符范围»] 匹配集合中的任何字符。[^«字符范围»] 匹配集合中没有的任何字符。字符范围规则
非语法字符代表自身:[abc]
只有以下四个字符是特殊的,必须通过斜杠转义
^ \ - ]
^ 仅当它出现在第一个位置时才必须转义。- 如果它出现在第一个或最后一个位置,则不需要转义。字符转义(\n、\u{1F44D} 等)具有通常的含义。
\b 代表退格。在正则表达式的其他位置,它匹配单词边界。字符类转义(\d、\p{White_Space} 等)具有通常的含义。
字符范围通过破折号指定:[a-z]
(#+)\1、\2 等。(?<hashes>#+)\k<hashes>(?:#+)默认情况下,以下所有量词都是贪婪的(它们匹配尽可能多的字符)
?:匹配零次或一次*:匹配零次或多次+:匹配一次或多次{n}:匹配 n 次{n,}:匹配 n 次或更多次{n,m}:至少匹配 n 次,最多匹配 m 次。要使它们不情愿(以便它们匹配尽可能少的字符),请在它们后面加上问号 (?)
> /".*"/.exec('"abc"def"')[0] // greedy
'"abc"def"'
> /".*?"/.exec('"abc"def"')[0] // reluctant
'"abc"'^ 仅在输入的开头匹配$ 仅在输入的结尾匹配\b 仅在单词边界匹配\B 仅在不在单词边界时匹配正先行断言: (?=«模式») 如果 模式 匹配后面的内容,则匹配。
示例:后跟 X 的小写字母序列。
> 'abcX def'.match(/[a-z]+(?=X)/g)
[ 'abc' ]请注意,X 本身不是匹配子字符串的一部分。
负先行断言: (?!«模式») 如果 模式 不匹配后面的内容,则匹配。
示例:后跟 X 的小写字母序列。
> 'abcX def'.match(/[a-z]+(?!X)/g)
[ 'ab', 'def' ]正向断言: (?<=«pattern») 如果 pattern 与前面的内容匹配,则匹配。
示例:以 X 开头的连续小写字母序列。
> 'Xabc def'.match(/(?<=X)[a-z]+/g)
[ 'abc' ]负向断言: (?<!«pattern») 如果 pattern 与前面的内容不匹配,则匹配。
示例:不以 X 开头的连续小写字母序列。
> 'Xabc def'.match(/(?<!X)[a-z]+/g)
[ 'bc', 'def' ]示例:将“.js”替换为“.html”,但“Node.js”中的“.js”除外。
> 'Node.js: index.js and main.js'.replace(/(?<!Node)\.js/g, '.html')
'Node.js: index.html and main.html'|)注意:此运算符的优先级较低。如有必要,请使用组
^aa|zz$ 匹配以 aa 开头和/或以 zz 结尾的所有字符串。请注意,| 的优先级低于 ^ 和 $。^(aa|zz)$ 匹配两个字符串 'aa' 和 'zz'。^a(a|z)z$ 匹配两个字符串 'aaz' 和 'azz'。| 字面标志 | 属性名称 | ES | 描述 |
|---|---|---|---|
d |
hasIndices |
ES2022 | 开启匹配索引 |
g |
global |
ES3 | 多次匹配 |
i |
ignoreCase |
ES3 | 不区分大小写匹配 |
m |
multiline |
ES3 | ^ 和 $ 匹配每行 |
s |
dotAll |
ES2018 | 点匹配行终止符 |
u |
unicode |
ES6 | Unicode 模式(推荐) |
y |
sticky |
ES6 | 匹配之间没有字符 |
以下正则表达式标志在 JavaScript 中可用(表 21 提供了一个简要概述)
/d (.hasIndices):一些与 RegExp 相关的方法返回描述正则表达式在输入字符串中匹配位置的匹配对象。如果此标志打开,则每个匹配对象都包含匹配索引,这些索引告诉我们每个组捕获的开始和结束位置。更多信息:§43.5.1 “匹配对象中的匹配索引 [ES2022]”。
/g (.global) 从根本上改变了以下方法的工作方式。
RegExp.prototype.test()RegExp.prototype.exec()String.prototype.match()如何,在 §43.7 “标志 /g 和 /y,以及属性 .lastIndex” 中进行了解释。简而言之,如果没有 /g,则这些方法仅考虑正则表达式在输入字符串中的第一个匹配项。使用 /g,它们会考虑所有匹配项。
/i (.ignoreCase) 开启不区分大小写的匹配
> /a/.test('A')
false
> /a/i.test('A')
true/m (.multiline):如果此标志打开,则 ^ 匹配每行的开头,$ 匹配每行的结尾。如果关闭,则 ^ 匹配整个输入字符串的开头,$ 匹配整个输入字符串的结尾。
> 'a1\na2\na3'.match(/^a./gm)
[ 'a1', 'a2', 'a3' ]
> 'a1\na2\na3'.match(/^a./g)
[ 'a1' ]/u (.unicode):此标志为正则表达式开启 Unicode 模式。该模式在 下一小节 中进行了解释。
/y (.sticky):此标志主要与 /g 结合使用才有意义。当两者都打开时,任何匹配都必须紧跟前一个匹配(即,它必须从正则表达式对象的索引 .lastIndex 处开始)。因此,第一个匹配必须在索引 0 处。
> 'a1a2 a3'.match(/a./gy)
[ 'a1', 'a2' ]
> '_a1a2 a3'.match(/a./gy) // first match must be at index 0
null
> 'a1a2 a3'.match(/a./g)
[ 'a1', 'a2', 'a3' ]
> '_a1a2 a3'.match(/a./g)
[ 'a1', 'a2', 'a3' ]/y 的主要用例是标记化(在解析期间)。有关此标志的更多信息:§43.7 “标志 /g 和 /y,以及属性 .lastIndex”。
/s (.dotAll):默认情况下,点不匹配行终止符。使用此标志,它会匹配
> /./.test('\n')
false
> /./s.test('\n')
true解决方法:如果不支持 /s,我们可以使用 [^] 代替点。
> /[^]/.test('\n')
true请考虑以下正则表达式:/“([^”]+)”/udg
我们应该按什么顺序列出其标志?有两种选择
/dgu/u 是最基本的,等等):/ugd鉴于 (2) 不明显,(1) 是更好的选择。JavaScript 也将其用于 RegExp 属性 .flags
> /a/ismudgy.flags
'dgimsuy'/u 使用 Unicode 模式标志 /u 为正则表达式开启了一种特殊的 Unicode 模式。该模式启用了几个功能
在模式中,我们可以使用 Unicode 代码点转义符(例如 \u{1F42A})来指定字符。代码单元转义符(例如 \u03B1)的范围只有四个十六进制数字(对应于基本多语言平面)。
在模式中,我们可以使用 Unicode 属性转义符,例如 \p{White_Space}。
现在禁止使用许多转义符。例如:\a \- \:
模式字符始终匹配自身
> /pa-:/.test('pa-:')
true如果没有 /u,则某些模式字符如果我们使用反斜杠对其进行转义,则仍然可以匹配自身
> /\p\a\-\:/.test('pa-:')
true使用 /u
\p 表示 Unicode 属性转义符的开头。匹配的原子单位是 Unicode 字符(代码点),而不是 JavaScript 字符(代码单元)。
以下小节更详细地解释了最后一项。它们使用以下 Unicode 字符来解释何时原子单位是 Unicode 字符,何时是 JavaScript 字符
const codePoint = '🙂';
const codeUnits = '\uD83D\uDE42'; // UTF-16
assert.equal(codePoint, codeUnits); // same string!我只是在 🙂 和 \uD83D\uDE42 之间切换,以说明 JavaScript 如何看待事物。两者是等效的,可以在字符串和正则表达式中互换使用。
使用 /u,🙂 的两个代码单元被视为单个字符
> /^[🙂]$/u.test('🙂')
true如果没有 /u,则 🙂 被视为两个字符
> /^[\uD83D\uDE42]$/.test('\uD83D\uDE42')
false
> /^[\uD83D\uDE42]$/.test('\uDE42')
true请注意,^ 和 $ 要求输入字符串只有一个字符。这就是第一个结果为 false 的原因。
.) 匹配 Unicode 字符,而不是 JavaScript 字符使用 /u,点运算符匹配 Unicode 字符
> '🙂'.match(/./gu).length
1.match() 加上 /g 返回一个包含正则表达式所有匹配项的数组。
如果没有 /u,则点运算符匹配 JavaScript 字符
> '\uD83D\uDE80'.match(/./g).length
2使用 /u,量词应用于整个前面的 Unicode 字符
> /^🙂{3}$/u.test('🙂🙂🙂')
true如果没有 /u,则量词仅应用于前面的 JavaScript 字符
> /^\uD83D\uDE80{3}$/.test('\uD83D\uDE80\uDE80\uDE80')
true值得注意的是
.lastIndex 是真正的实例属性。所有其他属性都是通过 getter 实现的。.lastIndex 是唯一可变的属性。所有其他属性都是只读的。如果我们想更改它们,我们需要复制正则表达式(有关详细信息,请参阅 §43.1.2 “克隆和非破坏性修改正则表达式”)。每个正则表达式标志都作为一个具有更长、更具描述性名称的属性存在
> /a/i.ignoreCase
true
> /a/.ignoreCase
false以下是标志属性的完整列表
.dotAll (/s).global (/g).hasIndices (/d).ignoreCase (/i).multiline (/m).sticky (/y).unicode (/u)每个正则表达式还具有以下属性
.source [ES3]:正则表达式模式
> /abc/ig.source
'abc'.flags [ES6]:正则表达式的标志
> /abc/ig.flags
'gi'.lastIndex [ES3]:在标志 /g 打开时使用。有关详细信息,请参阅 §43.7 “标志 /g 和 /y,以及属性 .lastIndex”。
几个与正则表达式相关的方法返回所谓的匹配对象,以提供有关正则表达式匹配输入字符串的位置的详细信息。这些方法是
RegExp.prototype.exec() 返回 null 或单个匹配对象。String.prototype.match() 返回 null 或单个匹配对象(如果未设置标志 /g)。String.prototype.matchAll() 返回一个匹配对象的迭代器(必须设置标志 /g;否则,将引发异常)。这是一个例子
assert.deepEqual(
/(a+)b/d.exec('ab aaab'),
{
0: 'ab',
1: 'a',
index: 0,
input: 'ab aaab',
groups: undefined,
indices: {
0: [0, 2],
1: [0, 1],
groups: undefined
},
}
);.exec() 的结果是第一个匹配的匹配对象,具有以下属性
[0]:正则表达式匹配的完整子字符串[1]:编号组 1 的捕获(等等).index:匹配发生在哪里?.input:匹配的字符串.groups:命名组的捕获(请参阅 §43.6.4.2 “命名捕获组 [ES2018]”).indices:捕获组的索引范围/d 打开时才会创建此属性。匹配索引是匹配对象的一个功能:如果我们通过正则表达式标志 /d(属性 .hasIndices)将其打开,则它们会记录捕获组的开始和结束索引。
以下是我们如何访问编号组的捕获
const matchObj = /(a+)(b+)/d.exec('aaaabb');
assert.equal(
matchObj[1], 'aaaa'
);
assert.equal(
matchObj[2], 'bb'
);由于正则表达式标志 /d,matchObj 还具有一个属性 .indices,该属性记录每个编号组在输入字符串中的捕获位置
assert.deepEqual(
matchObj.indices[1], [0, 4]
);
assert.deepEqual(
matchObj.indices[2], [4, 6]
);命名组的捕获访问方式如下
const matchObj = /(?<as>a+)(?<bs>b+)/d.exec('aaaabb');
assert.equal(
matchObj.groups.as, 'aaaa');
assert.equal(
matchObj.groups.bs, 'bb');它们的索引存储在 matchObj.indices.groups 中
assert.deepEqual(
matchObj.indices.groups.as, [0, 4]);
assert.deepEqual(
matchObj.indices.groups.bs, [4, 6]);匹配索引的一个重要用例是解析器,它们指向语法错误的确切位置。以下代码解决了相关问题:它指向引号内容的开始和结束位置(请参阅末尾的演示)。
const reQuoted = /“([^”]+)”/dgu;
function pointToQuotedText(str) {
const startIndices = new Set();
const endIndices = new Set();
for (const match of str.matchAll(reQuoted)) {
const [start, end] = match.indices[1];
startIndices.add(start);
endIndices.add(end);
}
let result = '';
for (let index=0; index < str.length; index++) {
if (startIndices.has(index)) {
result += '[';
} else if (endIndices.has(index+1)) {
result += ']';
} else {
result += ' ';
}
}
return result;
}
assert.equal(
pointToQuotedText(
'They said “hello” and “goodbye”.'),
' [ ] [ ] '
);默认情况下,正则表达式匹配字符串中的任何位置
> /a/.test('__a__')
true我们可以通过使用断言(例如 ^)或使用标志 /y 来更改它
> /^a/.test('__a__')
false
> /^a/.test('a__')
trueregExp.test(str):是否有匹配项? [ES3]如果 regExp 匹配 str,则正则表达式方法 .test() 返回 true
> /bc/.test('ABCD')
false
> /bc/i.test('ABCD')
true
> /\.mjs$/.test('main.mjs')
true使用 .test() 时,我们通常应避免使用 /g 标志。如果我们使用它,我们通常不会在每次调用该方法时都得到相同的结果
> const r = /a/g;
> r.test('aab')
true
> r.test('aab')
true
> r.test('aab')
false结果是由于 /a/ 在字符串中有两个匹配项。在找到所有匹配项后,.test() 返回 false。
str.search(regExp):匹配项位于哪个索引? [ES3]字符串方法 .search() 返回 str 中 regExp 匹配的第一个索引
> '_abc_'.search(/abc/)
1
> 'main.mjs'.search(/\.mjs$/)
4regExp.exec(str):捕获组 [ES3]如果没有标志 /g,则 .exec() 返回 str 中 regExp 的第一个匹配的 匹配对象
assert.deepEqual(
/(a+)b/.exec('ab aab'),
{
0: 'ab',
1: 'a',
index: 0,
input: 'ab aab',
groups: undefined,
}
);前面的示例包含一个编号组。以下示例演示了命名组
assert.deepEqual(
/(?<as>a+)b/.exec('ab aab'),
{
0: 'ab',
1: 'a',
index: 0,
input: 'ab aab',
groups: { as: 'a' },
}
);在 .exec() 的结果中,我们可以看到命名组也是一个编号组 - 它的捕获存在两次
'1')。groups.as)。 检索所有匹配项的更好选择:
str.matchAll(regExp) [ES2020]
从 ECMAScript 2020 开始,JavaScript 还有另一种检索所有匹配项的方法:str.matchAll(regExp)。此方法更易于使用,并且注意事项更少。
如果我们想检索正则表达式的所有匹配项(而不仅仅是第一个),则需要打开标志 /g。然后我们可以多次调用 .exec(),每次获取一个匹配项。在最后一个匹配项之后,.exec() 返回 null。
> const regExp = /(a+)b/g;
> regExp.exec('ab aab')
{ 0: 'ab', 1: 'a', index: 0, input: 'ab aab', groups: undefined }
> regExp.exec('ab aab')
{ 0: 'aab', 1: 'aa', index: 3, input: 'ab aab', groups: undefined }
> regExp.exec('ab aab')
null因此,我们可以按如下方式循环遍历所有匹配项
const regExp = /(a+)b/g;
const str = 'ab aab';
let match;
// Check for null via truthiness
// Alternative: while ((match = regExp.exec(str)) !== null)
while (match = regExp.exec(str)) {
console.log(match[1]);
}
// Output:
// 'a'
// 'aa' 与
/g 共享正则表达式时要小心!
使用 /g 共享正则表达式有一些陷阱,这些陷阱将在稍后解释。
练习:通过
.exec() 提取引用的文本
exercises/regexps/extract_quoted_test.mjs
str.match(regExp):获取所有组 0 捕获 [ES3]如果没有 /g,.match() 的工作方式与 .exec() 相同 - 它返回一个匹配对象。
使用 /g,.match() 返回与 regExp 匹配的 str 的所有子字符串
> 'ab aab'.match(/(a+)b/g)
[ 'ab', 'aab' ]如果没有匹配项,.match() 返回 null
> 'xyz'.match(/(a+)b/g)
null我们可以使用空值合并运算符 (??) 来防止 null
const numberOfMatches = (str.match(regExp) ?? []).length;str.matchAll(regExp):获取所有匹配对象的迭代器 [ES2020]以下是调用 .matchAll() 的方式
const matchIterable = str.matchAll(regExp);
给定一个字符串和一个正则表达式,.matchAll() 返回所有匹配项的匹配对象的迭代器。
在以下示例中,我们使用Array.from() 将迭代器转换为数组,以便更好地比较它们。
> Array.from('-a-a-a'.matchAll(/-(a)/ug))
[
{ 0:'-a', 1:'a', index: 0, input: '-a-a-a', groups: undefined },
{ 0:'-a', 1:'a', index: 2, input: '-a-a-a', groups: undefined },
{ 0:'-a', 1:'a', index: 4, input: '-a-a-a', groups: undefined },
]必须设置标志 /g
> Array.from('-a-a-a'.matchAll(/-(a)/u))
TypeError: String.prototype.matchAll called with a non-global
RegExp argument.matchAll() 不受 regExp.lastIndex 的影响,也不会更改它。
.matchAll().matchAll() 可以通过 .exec() 实现,如下所示
function* matchAll(str, regExp) {
if (!regExp.global) {
throw new TypeError('Flag /g must be set!');
}
const localCopy = new RegExp(regExp, regExp.flags);
let match;
while (match = localCopy.exec(str)) {
yield match;
}
}创建本地副本可确保两件事
regex.lastIndex 不会更改。localCopy.lastIndex 为零。使用 matchAll()
const str = '"fee" "fi" "fo" "fum"';
const regex = /"([^"]*)"/g;
for (const match of matchAll(str, regex)) {
console.log(match[1]);
}
// Output:
// 'fee'
// 'fi'
// 'fo'
// 'fum'regExp.exec() 与 str.match() 与 str.matchAll()下表总结了三种方法之间的区别
没有 /g |
使用 /g |
|
|---|---|---|
regExp.exec(str) |
第一个匹配对象 | 下一个匹配对象或 null |
str.match(regExp) |
第一个匹配对象 | 组 0 捕获的数组 |
str.matchAll(regExp) |
TypeError |
匹配对象的迭代器 |
str.replace() 和 str.replaceAll() 替换两种替换方法都有两个参数
str.replace(searchValue, replacementValue)str.replaceAll(searchValue, replacementValue)searchValue 可以是
replacementValue 可以是
$ 具有特殊含义,让我们可以插入组的捕获等等(稍后将详细解释)。这两种方法的区别如下
.replace() 替换字符串或没有 /g 的正则表达式的第一次出现。.replaceAll() 替换字符串或带有 /g 的正则表达式的每次出现。下表总结了它是如何工作的
搜索: → |
字符串 | 没有 /g 的 RegExp |
带有 /g 的 RegExp |
|---|---|---|---|
.replace |
第一次出现 | 第一次出现 | (所有出现) |
.replaceAll |
所有出现 | TypeError |
所有出现 |
.replace() 的最后一列括在括号中,因为此方法在 .replaceAll() 之前很久就已存在,因此支持现在应该由后者方法处理的功能。如果我们可以更改它,.replace() 将在此处抛出 TypeError。
我们首先探讨当 replacementValue 是一个简单字符串(不带字符 $)时,.replace() 和 .replaceAll() 如何单独工作。然后,我们检查两者如何受更复杂的替换值的影响。
str.replace(searchValue, replacementValue) [ES3].replace() 的操作方式受其第一个参数 searchValue 的影响
没有 /g 的正则表达式:替换此正则表达式的第一个匹配项。
> 'aaa'.replace(/a/, 'x')
'xaa'字符串:替换此字符串的第一次出现(字符串按字面解释,而不是作为正则表达式)。
> 'aaa'.replace('a', 'x')
'xaa'带有 /g 的正则表达式:替换此正则表达式的所有匹配项。
> 'aaa'.replace(/a/g, 'x')
'xxx'建议:如果 .replaceAll() 可用,则最好在这种情况下使用该方法 - 其目的是替换多次出现。
如果我们要替换字符串的每次出现,我们有两个选择
我们可以使用 .replaceAll()(它是在 ES2021 中引入的)。
在本章的后面,我们将遇到 [工具函数 escapeForRegExp()),它将帮助我们将字符串转换为多次匹配该字符串的正则表达式(例如,'*' 变为 /\*/g)。
str.replaceAll(searchValue, replacementValue) [ES2021].replaceAll() 的操作方式受其第一个参数 searchValue 的影响
带有 /g 的正则表达式:替换此正则表达式的所有匹配项。
> 'aaa'.replaceAll(/a/g, 'x')
'xxx'字符串:替换此字符串的所有出现(字符串按字面解释,而不是作为正则表达式)。
> 'aaa'.replaceAll('a', 'x')
'xxx'没有 /g 的正则表达式:抛出 TypeError(因为 .replaceAll() 的目的是替换多次出现)。
> 'aaa'.replaceAll(/a/, 'x')
TypeError: String.prototype.replaceAll called with
a non-global RegExp argument.replace() 和 .replaceAll() 的参数 replacementValue到目前为止,我们只将参数 replacementValue 与简单字符串一起使用,但它可以做更多的事情。如果它的值是
字符串,则匹配项将替换为此字符串。字符 $ 具有特殊含义,让我们可以插入组的捕获等等(继续阅读以了解详细信息)。
函数,则匹配项将替换为通过此函数计算的字符串。
replacementValue 是一个字符串如果替换值是一个字符串,则美元符号具有特殊含义 - 它插入与正则表达式匹配的文本
| 文本 | 结果 |
|---|---|
$$ |
单个 $ |
$& |
完全匹配 |
$` |
匹配之前的文本 |
$' |
匹配之后的文本 |
$n |
编号组 n 的捕获(n > 0) |
$<name> |
命名组 name 的捕获 [ES2018] |
示例:插入匹配子字符串之前、之中和之后的文本。
> 'a1 a2'.replaceAll(/a/g, "($`|$&|$')")
'(|a|1 a2)1 (a1 |a|2)2'示例:插入编号组的捕获。
> const regExp = /^([A-Za-z]+): (.*)$/ug;
> 'first: Jane'.replaceAll(regExp, 'KEY: $1, VALUE: $2')
'KEY: first, VALUE: Jane'示例:插入命名组的捕获。
> const regExp = /^(?<key>[A-Za-z]+): (?<value>.*)$/ug;
> 'first: Jane'.replaceAll(regExp, 'KEY: $<key>, VALUE: $<value>')
'KEY: first, VALUE: Jane' 练习:通过
.replace() 和命名组更改引号
exercises/regexps/change_quotes_test.mjs
replacementValue 是一个函数如果替换值是一个函数,我们可以计算每个替换。在以下示例中,我们将找到的每个非负整数乘以 2。
assert.equal(
'3 cats and 4 dogs'.replaceAll(/[0-9]+/g, (all) => 2 * Number(all)),
'6 cats and 8 dogs'
);替换函数获取以下参数。请注意它们与匹配对象的相似之处。这些参数都是位置参数,但我已经包含了如何命名它们
all:完全匹配g1:编号组 1 的捕获index:匹配发生在哪里?input:我们正在替换的字符串groups [ES2018]:命名组的捕获(一个对象)。始终是最后一个参数。如果我们只对 groups 感兴趣,我们可以使用以下技术
const result = 'first=jane, last=doe'.replace(
/(?<key>[a-z]+)=(?<value>[a-z]+)/g,
(...args) => { // (A)
const groups = args.at(-1); // (B)
const {key, value} = groups;
return key.toUpperCase() + '=' + value.toUpperCase();
});
assert.equal(result, 'FIRST=JANE, LAST=DOE');由于 A 行中的剩余参数,args 包含一个包含所有参数的数组。我们通过 B 行中的数组方法 .at() 访问最后一个参数。
String.prototype.split() 在关于字符串的章节中进行了描述。String.prototype.split() 的第一个参数是字符串或正则表达式。如果是后者,则组的捕获将出现在结果中
> 'a:b : c'.split(':')
[ 'a', 'b ', ' c' ]
> 'a:b : c'.split(/ *: */)
[ 'a', 'b', 'c' ]
> 'a:b : c'.split(/( *):( *)/)
[ 'a', '', '', 'b', ' ', ' ', 'c' ]/g 和 /y 以及属性 .lastIndex(高级)在本节中,我们将研究 RegExp 标志 /g 和 /y 的工作原理,以及它们如何依赖于 RegExp 属性 .lastIndex。我们还将发现 .lastIndex 的一个有趣的用例,您可能会感到惊讶。
/g 和 /y每个方法对 /g 和 /y 的反应都不同;这给了我们一个粗略的总体思路
/g (.global,ES3):正则表达式应该在字符串中的任何位置多次匹配。/y (.sticky,ES6):字符串内的任何匹配项都应该紧跟在前一个匹配项之后(匹配项“粘”在一起)。如果正则表达式既没有标志 /g 也没有标志 /y,则匹配发生一次并从头开始。
使用 /g 或 /y,匹配将相对于输入字符串内的“当前位置”执行。该位置存储在正则表达式属性 .lastIndex 中。
与正则表达式相关的方法有三组
字符串方法 .search(regExp) 和 .split(regExp) 完全忽略 /g 和 /y(因此也忽略 .lastIndex)。
如果设置了 /g 或 /y,则 RegExp 方法 .exec(str) 和 .test(str) 会以两种方式发生变化。
首先,我们通过重复调用一个方法来获得多个匹配项。每次调用都会返回另一个结果(匹配对象或 true)或“结果结束”值(null 或 false)。
其次,正则表达式属性 .lastIndex 用于遍历输入字符串。一方面,.lastIndex 确定匹配的开始位置
/g 表示匹配必须从 .lastIndex 或之后开始。
/y 表示匹配必须从 .lastIndex 开始。也就是说,正则表达式的开头固定在 .lastIndex 处。
请注意,^ 和 $ 继续像往常一样工作:它们将匹配项固定在输入字符串的开头或结尾,除非设置了 .multiline。然后,它们固定在行的开头或结尾。
另一方面,.lastIndex 设置为前一个匹配项的最后一个索引加一。
所有其他方法都会受到以下影响
/g 会导致多个匹配项。/y 会导致必须从 .lastIndex 开始的单个匹配项。/yg 会导致多个没有间隙的匹配项。这是第一个概述。接下来的部分将更详细地介绍。
/g 和 /y 的影响?regExp.exec(str) [ES3]如果没有 /g 和 /y,.exec() 会忽略 .lastIndex 并始终返回第一个匹配项的匹配对象
> const re = /#/; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]使用 /g,匹配必须从 .lastIndex 或之后开始。.lastIndex 会更新。如果没有匹配项,则返回 null。
> const re = /#/g; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 1, input: '##-#' }, 2]
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 3, input: '##-#' }, 4]
> [re.exec('##-#'), re.lastIndex]
[null, 0]使用 /y,匹配必须从 .lastIndex 处开始。.lastIndex 会更新。如果没有匹配项,则返回 null。
> const re = /#/y; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 1, input: '##-#' }, 2]
> [re.exec('##-#'), re.lastIndex]
[null, 0]使用 /yg,.exec() 的行为与使用 /y 时相同。
regExp.test(str) [ES3]此方法的行为与 .exec() 相同,但它返回 true 而不是匹配对象,返回 false 而不是 null。
例如,如果没有 /g 或 /y,结果始终为 true
> const re = /#/; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex]
[true, 1]
> [re.test('##-#'), re.lastIndex]
[true, 1]使用 /g,有两个匹配项
> const re = /#/g; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex]
[true, 2]
> [re.test('##-#'), re.lastIndex]
[true, 4]
> [re.test('##-#'), re.lastIndex]
[false, 0]使用 /y,只有一个匹配项
> const re = /#/y; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex]
[true, 2]
> [re.test('##-#'), re.lastIndex]
[false, 0]使用 /yg,.test() 的行为与使用 /y 时相同。
str.match(regExp) [ES3]如果没有 /g,.match() 的工作方式与 .exec() 相同。没有 /y
> const re = /#/; re.lastIndex = 1;
> ['##-#'.match(re), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]
> ['##-#'.match(re), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]或使用 /y
> const re = /#/y; re.lastIndex = 1;
> ['##-#'.match(re), re.lastIndex]
[{ 0: '#', index: 1, input: '##-#' }, 2]
> ['##-#'.match(re), re.lastIndex]
[null, 0]使用 /g,我们在数组中获取所有匹配项(组 0)。.lastIndex 被忽略并重置为零。
> const re = /#/g; re.lastIndex = 1;
> '##-#'.match(re)
['#', '#', '#']
> re.lastIndex
0/yg 的工作方式与 /g 相同,但匹配项之间没有间隙
> const re = /#/yg; re.lastIndex = 1;
> '##-#'.match(re)
['#', '#']
> re.lastIndex
0str.matchAll(regExp) [ES2020]如果未设置 /g,.matchAll() 会抛出异常
> const re = /#/y; re.lastIndex = 1;
> '##-#'.matchAll(re)
TypeError: String.prototype.matchAll called with
a non-global RegExp argument如果设置了 /g,则匹配从 .lastIndex 开始,并且该属性不会更改
> const re = /#/g; re.lastIndex = 1;
> Array.from('##-#'.matchAll(re))
[
{ 0: '#', index: 1, input: '##-#' },
{ 0: '#', index: 3, input: '##-#' },
]
> re.lastIndex
1如果设置了 /yg,则行为与 /g 相同,但匹配项之间没有间隙
> const re = /#/yg; re.lastIndex = 1;
> Array.from('##-#'.matchAll(re))
[
{ 0: '#', index: 1, input: '##-#' },
]
> re.lastIndex
1str.replace(regExp, str) [ES3]如果没有 /g 和 /y,则只替换第一次出现
> const re = /#/; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'x#-#'
> re.lastIndex
1使用 /g,所有出现都将被替换。.lastIndex 被忽略但重置为零。
> const re = /#/g; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'xx-x'
> re.lastIndex
0使用 /y,只替换 .lastIndex 处的(第一次)出现。.lastIndex 会更新。
> const re = /#/y; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'#x-#'
> re.lastIndex
2/yg 的工作方式与 /g 相同,但不允许匹配项之间存在间隙
> const re = /#/yg; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'xx-#'
> re.lastIndex
0str.replaceAll(regExp, str) [ES2021].replaceAll() 的工作方式与 .replace() 相同,但如果未设置 /g,则会抛出异常
> const re = /#/y; re.lastIndex = 1;
> '##-#'.replaceAll(re, 'x')
TypeError: String.prototype.replaceAll called
with a non-global RegExp argument我们将首先了解 /g 和 /y 的四个陷阱,然后了解处理这些陷阱的方法。
带有 /g 的正则表达式不能内联。例如,在以下 while 循环中,每次检查条件时都会重新创建一个正则表达式。因此,它的 .lastIndex 始终为零,并且循环永远不会终止。
let matchObj;
// Infinite loop
while (matchObj = /a+/g.exec('bbbaabaaa')) {
console.log(matchObj[0]);
}使用 /y 时,问题是一样的。
如果代码需要一个带有 /g 的正则表达式,并且有一个循环遍历 .exec() 或 .test() 的结果,那么没有 /g 的正则表达式可能会导致无限循环
function collectMatches(regExp, str) {
const matches = [];
let matchObj;
// Infinite loop
while (matchObj = regExp.exec(str)) {
matches.push(matchObj[0]);
}
return matches;
}
collectMatches(/a+/, 'bbbaabaaa'); // Missing: flag /g为什么会出现无限循环?因为 .exec() 总是返回第一个结果,一个匹配对象,而不是 null。
使用 /y 时,问题是一样的。
对于 .test(),还有另一个需要注意的地方:它受 .lastIndex 的影响。因此,如果我们想精确地检查一次正则表达式是否匹配字符串,那么该正则表达式一定不能有 /g。否则,我们每次调用 .test() 通常都会得到不同的结果
> const regExp = /^X/g;
> [regExp.test('Xa'), regExp.lastIndex]
[ true, 1 ]
> [regExp.test('Xa'), regExp.lastIndex]
[ false, 0 ]
> [regExp.test('Xa'), regExp.lastIndex]
[ true, 1 ]第一次调用会产生匹配并更新 .lastIndex。第二次调用找不到匹配项,并将 .lastIndex 重置为零。
如果我们专门为 .test() 创建一个正则表达式,那么我们可能不会添加 /g。但是,如果我们使用相同的正则表达式进行替换和测试,那么遇到 /g 的可能性就会增加。
同样,/y 也存在这个问题
> const regExp = /^X/y;
> regExp.test('Xa')
true
> regExp.test('Xa')
false
> regExp.test('Xa')
true考虑到所有受 .lastIndex 影响的正则表达式操作,我们必须小心许多算法,在开始时 .lastIndex 为零。否则,我们可能会得到意想不到的结果
function countMatches(regExp, str) {
let count = 0;
while (regExp.test(str)) {
count++;
}
return count;
}
const myRegExp = /a/g;
myRegExp.lastIndex = 4;
assert.equal(
countMatches(myRegExp, 'babaa'), 1); // should be 3通常,.lastIndex 在新创建的正则表达式中为零,我们不会像在示例中那样显式更改它。但是,如果我们多次使用正则表达式,.lastIndex 最终仍然可能不为零。
作为处理 /g 和 .lastIndex 的一个例子,我们重新审视前面例子中的 countMatches()。我们如何防止错误的正则表达式破坏我们的代码?让我们看看三种方法。
首先,如果未设置 /g 或 .lastIndex 不为零,我们可以抛出一个异常
function countMatches(regExp, str) {
if (!regExp.global) {
throw new Error('Flag /g of regExp must be set');
}
if (regExp.lastIndex !== 0) {
throw new Error('regExp.lastIndex must be zero');
}
let count = 0;
while (regExp.test(str)) {
count++;
}
return count;
}其次,我们可以克隆参数。这样做还有一个好处,就是 regExp 不会被改变。
function countMatches(regExp, str) {
const cloneFlags = regExp.flags + (regExp.global ? '' : 'g');
const clone = new RegExp(regExp, cloneFlags);
let count = 0;
while (clone.test(str)) {
count++;
}
return count;
}一些正则表达式操作不受 .lastIndex 或标志的影响。例如,如果存在 /g,.match() 会忽略 .lastIndex
function countMatches(regExp, str) {
if (!regExp.global) {
throw new Error('Flag /g of regExp must be set');
}
return (str.match(regExp) ?? []).length;
}
const myRegExp = /a/g;
myRegExp.lastIndex = 4;
assert.equal(countMatches(myRegExp, 'babaa'), 3); // OK!在这里,countMatches() 可以工作,即使我们没有检查或修复 .lastIndex。
除了存储状态之外,.lastIndex 还可以用于从给定索引开始匹配。本节介绍如何实现。
鉴于 .test() 受 /y 和 .lastIndex 的影响,我们可以使用它来检查正则表达式 regExp 是否在给定的索引处匹配字符串 str
function matchesStringAt(regExp, str, index) {
if (!regExp.sticky) {
throw new Error('Flag /y of regExp must be set');
}
regExp.lastIndex = index;
return regExp.test(str);
}
assert.equal(
matchesStringAt(/x+/y, 'aaxxx', 0), false);
assert.equal(
matchesStringAt(/x+/y, 'aaxxx', 2), true);由于 /y 的原因,regExp 被锚定到 .lastIndex。
请注意,我们不能使用断言 ^,因为它会将 regExp 锚定到输入字符串的开头。
.search() 让我们找到正则表达式匹配的位置
> '#--#'.search(/#/)
0遗憾的是,我们无法更改 .search() 开始查找匹配项的位置。作为一种解决方法,我们可以使用 .exec() 进行搜索
function searchAt(regExp, str, index) {
if (!regExp.global && !regExp.sticky) {
throw new Error('Either flag /g or flag /y of regExp must be set');
}
regExp.lastIndex = index;
const match = regExp.exec(str);
if (match) {
return match.index;
} else {
return -1;
}
}
assert.equal(
searchAt(/#/g, '#--#', 0), 0);
assert.equal(
searchAt(/#/g, '#--#', 1), 3);当不带 /g 并带 /y 使用时,.replace() 会进行一次替换 - 如果在 .lastIndex 处有匹配项
function replaceOnceAt(str, regExp, replacement, index) {
if (!(regExp.sticky && !regExp.global)) {
throw new Error('Flag /y must be set, flag /g must not be set');
}
regExp.lastIndex = index;
return str.replace(regExp, replacement);
}
assert.equal(
replaceOnceAt('aa aaaa a', /a+/y, 'X', 0), 'X aaaa a');
assert.equal(
replaceOnceAt('aa aaaa a', /a+/y, 'X', 3), 'aa X a');
assert.equal(
replaceOnceAt('aa aaaa a', /a+/y, 'X', 8), 'aa aaaa X');正则表达式属性 .lastIndex 有两个明显的缺点
从好的方面来说,.lastIndex 也为我们提供了额外的有用功能:我们可以指定匹配应该从哪里开始(对于某些操作)。
以下两种方法完全不受 /g 和 /y 的影响
String.prototype.search()String.prototype.split()下表解释了其余与正则表达式相关的方法如何受这两个标志的影响
/ |
/g |
/y |
/yg |
|
|---|---|---|---|---|
r.exec(s) |
{i:0} |
{i:1} |
{i:1} |
{i:1} |
.lI 不变 |
.lI 更新 |
.lI 更新 |
.lI 更新 |
|
r.test(s) |
true |
true |
true |
true |
.lI 不变 |
.lI 更新 |
.lI 更新 |
.lI 更新 |
|
s.match(r) |
{i:0} |
["#","#","#"] |
{i:1} |
["#","#"] |
.lI 不变 |
.lI 重置 |
.lI 更新 |
.lI 重置 |
|
s.matchAll(r) |
TypeError |
[{i:1}, {i:3}] |
TypeError |
[{i:1}] |
.lI 不变 |
.lI 不变 |
|||
s.replace(r, 'x') |
"x#-#" |
"xx-x" |
"#x-#" |
"xx-#" |
.lI 不变 |
.lI 重置 |
.lI 更新 |
.lI 重置 |
|
s.replaceAll(r, 'x') |
TypeError |
"xx-x" |
TypeError |
"xx-#" |
.lI 重置 |
.lI 重置 |
变量
const r = /#/; r.lastIndex = 1;
const s = '##-#';缩写
{i:2}:匹配对象,其属性 .index 的值为 2.lI 更新:.lastIndex 被更新.lI 重置:.lastIndex 被重置为零.lI 不变:.lastIndex 保持不变 生成上表的 Node.js 脚本
上表是通过 一个 Node.js 脚本 生成的。
以下函数转义任意文本,以便如果我们将它放在正则表达式中,它将被逐字匹配
function escapeForRegExp(str) {
return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); // (A)
}
assert.equal(escapeForRegExp('[yes?]'), String.raw`\[yes\?\]`);
assert.equal(escapeForRegExp('_g_'), String.raw`_g_`);在 A 行中,我们转义了所有语法字符。我们必须有选择性,因为正则表达式标志 /u 禁止许多转义 - 例如:\a \: \-
escapeForRegExp() 有两个用例
.replace() 只允许我们替换一次纯文本。使用 escapeForRegExp(),我们可以解决这个限制
const plainText = ':-)';
const regExp = new RegExp(escapeForRegExp(plainText), 'ug');
assert.equal(
':-) :-) :-)'.replace(regExp, '🙂'), '🙂 🙂 🙂');有时,我们可能需要一个匹配所有内容或不匹配任何内容的正则表达式 - 例如,作为默认值。
匹配所有内容:/(?:)/
空组 () 匹配所有内容。我们将其设为非捕获组(通过 ?:),以避免不必要的工作。
> /(?:)/.test('')
true
> /(?:)/.test('abc')
true不匹配任何内容:/.^/
^ 只匹配字符串的开头。点将匹配移动到第一个字符之后,现在 ^ 就不再匹配了。
> /.^/.test('')
false
> /.^/.test('abc')
false