本章解释 ECMAScript 6 中正则表达式的新特性。如果您熟悉 ES5 正则表达式特性和 Unicode,将有所帮助。如有必要,请参阅“Speaking JavaScript”的以下两章
/y
(粘性匹配)RegExp.prototype.exec(str)
RegExp.prototype.test(str)
String.prototype.search(regex)
String.prototype.match(regex)
String.prototype.split(separator, limit)
String.prototype.replace(search, replacement)
/u
(Unicode).
) 匹配代码点,而不是代码单元flags
RegExp()
可用作复制构造函数exec()
的可迭代版本以下正则表达式特性是 ECMAScript 6 中新增的
/y
(粘性匹配)将正则表达式的每个匹配项锚定到上一个匹配项的末尾。/u
(Unicode)将代理对(例如 \uD83D\uDE80
)作为代码点处理,并允许您在正则表达式中使用 Unicode 代码点转义符(例如 \u{1F680}
)。flags
允许您访问正则表达式的标志,就像 source
在 ES5 中已经允许您访问模式一样
> /abc/ig.source // ES5
'abc'
> /abc/ig.flags // ES6
'gi'
RegExp()
来复制正则表达式
> new RegExp(/abc/ig).flags
'gi'
> new RegExp(/abc/ig, 'i').flags // change flags
'i'
/y
(粘性匹配) 新标志 /y
在将正则表达式 re
与字符串匹配时会改变两件事
re.lastIndex
:匹配必须从 re.lastIndex
(上一个匹配项之后的索引)开始。此行为类似于 ^
锚点,但使用该锚点时,匹配必须始终从索引 0 开始。re.lastIndex
设置为匹配项之后的索引。此行为类似于 /g
标志。与 /g
一样,/y
通常用于多次匹配。此匹配行为的主要用例是词法分析,您希望每个匹配项紧跟在其前一个匹配项之后。稍后将给出一个通过粘性正则表达式和 exec()
进行词法分析的示例。
让我们看看各种正则表达式操作如何对 /y
标志做出反应。下表概述了这些内容。我将在后面提供更多详细信息。
正则表达式的方法(re
是调用方法的正则表达式)
标志 | 开始匹配 | 锚定到 | 匹配时的结果 | 不匹配 | re.lastIndex | |
---|---|---|---|---|---|---|
exec() | – | 0 | – | 匹配对象 | null | 不变 |
/g | re.lastIndex |
– | 匹配对象 | null | 匹配后的索引 | |
/y | re.lastIndex |
re.lastIndex |
匹配对象 | null | 匹配后的索引 | |
/gy | re.lastIndex |
re.lastIndex |
匹配对象 | null | 匹配后的索引 | |
test() | (任意) | (类似于 exec()) | (类似于 exec()) | true | false | (类似于 exec()) |
字符串的方法(str
是调用方法的字符串,r
是正则表达式参数)
标志 | 开始匹配 | 锚定到 | 匹配时的结果 | 不匹配 | r.lastIndex | |
---|---|---|---|---|---|---|
search() | –, /g | 0 | – | 匹配项的索引 | -1 | 不变 |
/y, /gy | 0 | 0 | 匹配项的索引 | -1 | 不变 | |
match() | – | 0 | – | 匹配对象 | null | 不变 |
/y | r.lastIndex |
r.lastIndex |
匹配对象 | null | 之后的索引 | |
匹配 | ||||||
/g | 上一个之后 | – | 包含匹配项的数组 | null | 0 | |
匹配(循环) | ||||||
/gy | 上一个之后 | 上一个之后 | 包含匹配项的数组 | null | 0 | |
匹配(循环) | 匹配 | |||||
split() | –, /g | 上一个之后 | – | 包含字符串的数组 | [str] |
不变 |
匹配(循环) | 匹配项之间 | |||||
/y, /gy | 上一个之后 | 上一个之后 | 包含空字符串的数组 | [str] |
不变 | |
匹配(循环) | 匹配 | 匹配项之间 | ||||
replace() | – | 0 | – | 替换第一个匹配项 | 无替换 | 不变 |
/y | 0 | 0 | 替换第一个匹配项 | 无替换 | 不变 | |
/g | 上一个之后 | – | 替换所有匹配项 | 无替换 | 不变 | |
匹配(循环) | ||||||
/gy | 上一个之后 | 上一个之后 | 替换所有匹配项 | 无替换 | 不变 | |
匹配(循环) | 匹配 |
RegExp.prototype.exec(str)
如果未设置 /g
,则匹配始终从开头开始,但会跳过直到找到匹配项。 REGEX.lastIndex
不变。
const
REGEX
=
/a/
;
REGEX
.
lastIndex
=
7
;
// ignored
const
match
=
REGEX
.
exec
(
'xaxa'
);
console
.
log
(
match
.
index
);
// 1
console
.
log
(
REGEX
.
lastIndex
);
// 7 (unchanged)
如果设置了 /g
,则匹配从 REGEX.lastIndex
开始,并跳过直到找到匹配项。 REGEX.lastIndex
设置为匹配项之后的位置。这意味着,如果您循环直到 exec()
返回 null
,您将收到所有匹配项。
const
REGEX
=
/a/g
;
REGEX
.
lastIndex
=
2
;
const
match
=
REGEX
.
exec
(
'xaxa'
);
console
.
log
(
match
.
index
);
// 3
console
.
log
(
REGEX
.
lastIndex
);
// 4 (updated)
// No match at index 4 or later
console
.
log
(
REGEX
.
exec
(
'xaxa'
));
// null
如果仅设置了 /y
,则匹配从 REGEX.lastIndex
开始,并锚定到该位置(不会跳过直到找到匹配项)。 REGEX.lastIndex
的更新方式与设置 /g
时的更新方式类似。
const
REGEX
=
/a/y
;
// No match at index 2
REGEX
.
lastIndex
=
2
;
console
.
log
(
REGEX
.
exec
(
'xaxa'
));
// null
// Match at index 3
REGEX
.
lastIndex
=
3
;
const
match
=
REGEX
.
exec
(
'xaxa'
);
console
.
log
(
match
.
index
);
// 3
console
.
log
(
REGEX
.
lastIndex
);
// 4
同时设置 /y
和 /g
与仅设置 /y
相同。
RegExp.prototype.test(str)
test()
的工作方式与 exec()
相同,但它在匹配成功或失败时返回 true
或 false
(而不是匹配对象或 null
)。
const
REGEX
=
/a/y
;
REGEX
.
lastIndex
=
2
;
console
.
log
(
REGEX
.
test
(
'xaxa'
));
// false
REGEX
.
lastIndex
=
3
;
console
.
log
(
REGEX
.
test
(
'xaxa'
));
// true
console
.
log
(
REGEX
.
lastIndex
);
// 4
String.prototype.search(regex)
search()
忽略标志 /g
和 lastIndex
(它也不会改变)。它从字符串的开头开始,查找第一个匹配项并返回其索引(如果没有匹配项,则返回 -1
)。
const
REGEX
=
/a/
;
REGEX
.
lastIndex
=
2
;
// ignored
console
.
log
(
'xaxa'
.
search
(
REGEX
));
// 1
如果设置了标志 /y
,则仍然会忽略 lastIndex
,但正则表达式现在锚定到索引 0。
const
REGEX
=
/a/y
;
REGEX
.
lastIndex
=
1
;
// ignored
console
.
log
(
'xaxa'
.
search
(
REGEX
));
// -1 (no match)
String.prototype.match(regex)
match()
有两种模式
/g
,则其工作方式类似于 exec()
。/g
,则它返回一个包含匹配字符串部分的数组,或者返回 null
。如果未设置标志 /g
,则 match()
像 exec()
一样捕获组
{
const
REGEX
=
/a/
;
REGEX
.
lastIndex
=
7
;
// ignored
console
.
log
(
'xaxa'
.
match
(
REGEX
).
index
);
// 1
console
.
log
(
REGEX
.
lastIndex
);
// 7 (unchanged)
}
{
const
REGEX
=
/a/y
;
REGEX
.
lastIndex
=
2
;
console
.
log
(
'xaxa'
.
match
(
REGEX
));
// null
REGEX
.
lastIndex
=
3
;
console
.
log
(
'xaxa'
.
match
(
REGEX
).
index
);
// 3
console
.
log
(
REGEX
.
lastIndex
);
// 4
}
如果仅设置了标志 /g
,则 match()
在数组中返回所有匹配的子字符串(或 null
)。匹配始终从位置 0 开始。
const
REGEX
=
/a|b/g
;
REGEX
.
lastIndex
=
7
;
console
.
log
(
'xaxb'
.
match
(
REGEX
));
// ['a', 'b']
console
.
log
(
REGEX
.
lastIndex
);
// 0
如果还设置了标志 /y
,则仍然会重复执行匹配,同时将正则表达式锚定到上一个匹配项之后的索引(或 0)。
const
REGEX
=
/a|b/gy
;
REGEX
.
lastIndex
=
0
;
// ignored
console
.
log
(
'xab'
.
match
(
REGEX
));
// null
REGEX
.
lastIndex
=
1
;
// ignored
console
.
log
(
'xab'
.
match
(
REGEX
));
// null
console
.
log
(
'ab'
.
match
(
REGEX
));
// ['a', 'b']
console
.
log
(
'axb'
.
match
(
REGEX
));
// ['a']
String.prototype.split(separator, limit)
split()
的完整详细信息在 Speaking JavaScript 中有解释。
对于 ES6,有趣的是看看如果使用标志 /y
会发生什么变化。
使用 /y
时,字符串必须以分隔符开头
> 'x##'.split(/#/y) // no match
[ 'x##' ]
> '##x'.split(/#/y) // 2 matches
[ '', '', 'x' ]
只有当后续分隔符紧跟在第一个分隔符之后时,才会识别它们
> '#x#'.split(/#/y) // 1 match
[ '', 'x#' ]
> '##'.split(/#/y) // 2 matches
[ '', '', '' ]
这意味着第一个分隔符之前的字符串和分隔符之间的字符串始终为空。
像往常一样,您可以使用组将分隔符的部分放入结果数组中
> '##'.split(/(#)/y)
[ '', '#', '', '#', '' ]
String.prototype.replace(search, replacement)
如果没有标志 /g
,则 replace()
仅替换第一个匹配项
const
REGEX
=
/a/
;
// One match
console
.
log
(
'xaxa'
.
replace
(
REGEX
,
'-'
));
// 'x-xa'
如果仅设置了 /y
,则最多也只能获得一个匹配项,但该匹配项始终锚定到字符串的开头。 lastIndex
被忽略且不变。
const
REGEX
=
/a/y
;
// Anchored to beginning of string, no match
REGEX
.
lastIndex
=
1
;
// ignored
console
.
log
(
'xaxa'
.
replace
(
REGEX
,
'-'
));
// 'xaxa'
console
.
log
(
REGEX
.
lastIndex
);
// 1 (unchanged)
// One match
console
.
log
(
'axa'
.
replace
(
REGEX
,
'-'
));
// '-xa'
设置 /g
后,replace()
会替换所有匹配项
const
REGEX
=
/a/g
;
// Multiple matches
console
.
log
(
'xaxa'
.
replace
(
REGEX
,
'-'
));
// 'x-x-'
设置 /gy
后,replace()
会替换所有匹配项,但每个匹配项都锚定到上一个匹配项的末尾
const
REGEX
=
/a/gy
;
// Multiple matches
console
.
log
(
'aaxa'
.
replace
(
REGEX
,
'-'
));
// '--xa'
参数 replacement
也可以是一个函数,有关详细信息,请参阅“Speaking JavaScript”。
粘性匹配的主要用例是*词法分析*,即将文本转换为一系列标记。词法分析的一个重要特征是标记是文本的片段,并且它们之间不能有间隙。因此,粘性匹配在这里非常完美。
function
tokenize
(
TOKEN_REGEX
,
str
)
{
const
result
=
[];
let
match
;
while
(
match
=
TOKEN_REGEX
.
exec
(
str
))
{
result
.
push
(
match
[
1
]);
}
return
result
;
}
const
TOKEN_GY
=
/\s*(\+|[0-9]+)\s*/gy
;
const
TOKEN_G
=
/\s*(\+|[0-9]+)\s*/g
;
在合法的标记序列中,粘性匹配和非粘性匹配产生相同的输出
> tokenize(TOKEN_GY, '3 + 4')
[ '3', '+', '4' ]
> tokenize(TOKEN_G, '3 + 4')
[ '3', '+', '4' ]
但是,如果字符串中存在非标记文本,则粘性匹配会停止词法分析,而非粘性匹配会跳过非标记文本
> tokenize(TOKEN_GY, '3x + 4')
[ '3' ]
> tokenize(TOKEN_G, '3x + 4')
[ '3', '+', '4' ]
词法分析期间粘性匹配的行为有助于错误处理。
如果要手动实现粘性匹配,可以按如下方式进行:函数 execSticky()
的工作方式类似于粘性模式下的 RegExp.prototype.exec()
。
function
execSticky
(
regex
,
str
)
{
// Anchor the regex to the beginning of the string
let
matchSource
=
regex
.
source
;
if
(
!
matchSource
.
startsWith
(
'^'
))
{
matchSource
=
'^'
+
matchSource
;
}
// Ensure that instance property `lastIndex` is updated
let
matchFlags
=
regex
.
flags
;
// ES6 feature!
if
(
!
regex
.
global
)
{
matchFlags
=
matchFlags
+
'g'
;
}
const
matchRegex
=
new
RegExp
(
matchSource
,
matchFlags
);
// Ensure we start matching `str` at `regex.lastIndex`
const
matchOffset
=
regex
.
lastIndex
;
const
matchStr
=
str
.
slice
(
matchOffset
);
let
match
=
matchRegex
.
exec
(
matchStr
);
// Translate indices from `matchStr` to `str`
regex
.
lastIndex
=
matchRegex
.
lastIndex
+
matchOffset
;
match
.
index
=
match
.
index
+
matchOffset
;
return
match
;
}
/u
(Unicode) 标志 /u
为正则表达式启用特殊的 Unicode 模式。该模式具有两个特性
\u{1F42A}
)通过代码点指定字符。普通的 Unicode 转义序列(例如 \u03B1
)只有四个十六进制数字的范围(等于基本多语言平面)。Unicode 章节中的一个部分包含有关转义序列的更多信息。接下来我将解释特性 2 的结果。我使用两个 UTF-16 代码单元(例如 \uD83D\uDE80
)而不是 Unicode 代码点转义符(例如 \u{1F680}
)。这清楚地表明代理对在 Unicode 模式下是分组的,并且在 Unicode 模式和非 Unicode 模式下都可以工作。
> '\u{1F680}' === '\uD83D\uDE80' // code point vs. surrogate pairs
true
在非 Unicode 模式下,即使在(编码代理对的)代码点内部也能找到正则表达式中的孤立代理项
> /\uD83D/.test('\uD83D\uDC2A')
true
在 Unicode 模式下,代理对成为原子单元,并且在它们“内部”找不到孤立代理项
> /\uD83D/u.test('\uD83D\uDC2A')
false
仍然可以找到实际的孤立代理项
> /\uD83D/u.test('\uD83D \uD83D\uDC2A')
true
> /\uD83D/u.test('\uD83D\uDC2A \uD83D')
true
在 Unicode 模式下,您可以将代码点放入字符类中,并且它们将不再被解释为两个字符。
> /^[\uD83D\uDC2A]$/u.test('\uD83D\uDC2A')
true
> /^[\uD83D\uDC2A]$/.test('\uD83D\uDC2A')
false
> /^[\uD83D\uDC2A]$/u.test('\uD83D')
false
> /^[\uD83D\uDC2A]$/.test('\uD83D')
true
.
) 匹配代码点,而不是代码单元 在 Unicode 模式下,点运算符匹配代码点(一个或两个代码单元)。在非 Unicode 模式下,它匹配单个代码单元。例如
> '\uD83D\uDE80'.match(/./gu).length
1
> '\uD83D\uDE80'.match(/./g).length
2
在 Unicode 模式下,量词应用于代码点(一个或两个代码单元)。在非 Unicode 模式下,它们应用于单个代码单元。例如
> /\uD83D\uDE80{2}/u.test('\uD83D\uDE80\uD83D\uDE80')
true
> /\uD83D\uDE80{2}/.test('\uD83D\uDE80\uD83D\uDE80')
false
> /\uD83D\uDE80{2}/.test('\uD83D\uDE80\uDE80')
true
flags
在 ECMAScript 6 中,正则表达式具有以下数据属性
source
flags
global
、ignoreCase
、multiline
、sticky
、unicode
lastIndex
顺便提一下,lastIndex
现在是唯一的实例属性,所有其他数据属性都通过内部实例属性和 getter 实现,例如 get RegExp.prototype.global
。
属性 source
(在 ES5 中已存在)包含作为字符串的正则表达式模式。
> /abc/ig.source
'abc'
属性 flags
是新增的,它包含作为字符串的标志,每个标志对应一个字符。
> /abc/ig.flags
'gi'
您不能更改现有正则表达式的标志(ignoreCase
等一直是不可变的),但 flags
允许您创建标志已更改的副本。
function
copyWithIgnoreCase
(
re
)
{
return
new
RegExp
(
re
.
source
,
re
.
flags
.
includes
(
'i'
)
?
re
.
flags
:
re
.
flags
+
'i'
);
}
下一节将解释另一种创建正则表达式修改副本的方法。
RegExp()
可用作复制构造函数 在 ES6 中,构造函数 RegExp()
有两种变体(第二种是新增的)。
new RegExp(pattern : string, flags = '')
pattern
指定的内容创建一个新的正则表达式。如果缺少 flags
,则使用空字符串 ''
。new RegExp(regex : RegExp, flags = regex.flags)
regex
。如果提供了 flags
,则它将确定副本的标志。以下交互演示了后一种变体。
> new RegExp(/abc/ig).flags
'gi'
> new RegExp(/abc/ig, 'i').flags // change flags
'i'
因此,RegExp
构造函数为我们提供了另一种更改标志的方法。
function
copyWithIgnoreCase
(
re
)
{
return
new
RegExp
(
re
,
re
.
flags
.
includes
(
'i'
)
?
re
.
flags
:
re
.
flags
+
'i'
);
}
exec()
的可迭代版本 以下函数 execAll()
是 exec()
的可迭代版本,它修复了使用 exec()
检索正则表达式的所有匹配项时出现的几个问题。
exec()
直到它返回 null
)。exec()
会改变正则表达式,这意味着副作用可能会成为一个问题。/g
。否则,将仅返回第一个匹配项。
function
*
execAll
(
regex
,
str
)
{
// Make sure flag /g is set and regex.index isn’t changed
const
localCopy
=
copyAndEnsureFlag
(
regex
,
'g'
);
let
match
;
while
(
match
=
localCopy
.
exec
(
str
))
{
yield
match
;
}
}
function
copyAndEnsureFlag
(
re
,
flag
)
{
return
new
RegExp
(
re
,
re
.
flags
.
includes
(
flag
)
?
re
.
flags
:
re
.
flags
+
flag
);
}
使用 execAll()
const
str
=
'"fee" "fi" "fo" "fum"'
;
const
regex
=
/"([^"]*)"/
;
// Access capture of group #1 via destructuring
for
(
const
[,
group1
]
of
execAll
(
regex
,
str
))
{
console
.
log
(
group1
);
}
// Output:
// fee
// fi
// fo
// fum
以下字符串方法现在将其部分工作委托给正则表达式方法。
String.prototype.match
调用 RegExp.prototype[Symbol.match]
。String.prototype.replace
调用 RegExp.prototype[Symbol.replace]
。String.prototype.search
调用 RegExp.prototype[Symbol.search]
。String.prototype.split
调用 RegExp.prototype[Symbol.split]
。有关更多信息,请参阅字符串章节中的“将正则表达式工作委托给其参数的字符串方法”部分。