作者:阮一峰
本文仅用于学习记录,不存在任何商业用途,如侵删
ES2018 引入了 Unicode 属性类,允许使用\p{...}
和\P{...}
(\P
是\p
的否定形式)代表一类 Unicode 字符,匹配满足条件的所有字符。
const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true
上面代码中,\p{Script=Greek}
表示匹配一个希腊文字母,所以匹配π
成功。
Unicode 属性类的标准形式,需要同时指定属性名和属性值。
\p{UnicodePropertyName=UnicodePropertyValue}
但是,对于某些属性,可以只写属性名,或者只写属性值。
\p{UnicodePropertyName}
\p{UnicodePropertyValue}
\P{…}
是\p{…}
的反向匹配,即匹配不满足条件的字符。
注意,这两种类只对 Unicode 有效,所以使用的时候一定要加上u
修饰符。如果不加u
修饰符,正则表达式使用\p
和\P
会报错。
由于 Unicode 的各种属性非常多,所以这种新的类的表达能力非常强。
const regex = /^\p{Decimal_Number}+$/u;
regex.test('𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼') // true
上面代码中,属性类指定匹配所有十进制字符,可以看到各种字型的十进制字符都会匹配成功。
\p{Number}
甚至能匹配罗马数字。
// 匹配所有数字
const regex = /^\p{Number}+$/u;
regex.test('²³¹¼½¾') // true
regex.test('㉛㉜㉝') // true
regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true
下面是其他一些例子。
// 匹配所有空格
\p{White_Space}
// 匹配各种文字的所有字母,等同于 Unicode 版的 \w
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
[^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
// 匹配 Emoji
/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu
// 匹配所有的箭头字符
const regexArrows = /^\p{Block=Arrows}+$/u;
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true
有时,需要向某个 Unicode 属性类添加或减少字符,即需要对属性类进行运算。现在有一个提案,增加了 Unicode 属性类的运算功能。
它提供两种形式的运算,一种是差集运算(A 集合减去 B 集合),另一种是交集运算。
// 差集运算(A 减去 B)
[A--B]
// 交集运算(A 与 B 的交集)
[A&&B]
上面两种写法中,A 和 B 要么是字符类(例如[a-z]
),要么是 Unicode 属性类(例如\p{ASCII}
)。
而且,这种运算支持方括号之中嵌入方括号,即方括号的嵌套。
// 方括号嵌套的例子
[A--[0-9]]
这种运算的前提是,正则表达式必须使用新引入的v
修饰符。前面说过,Unicode 属性类必须搭配u
修饰符使用,这个v
修饰符等于代替u
,使用了它就不必再写u
了。
下面是一些例子。
// 十进制字符去除 ASCII 码的0到9
[\p{Decimal_Number}--[0-9]]
// Emoji 字符去除 ASCII 码字符
[\p{Emoji}--\p{ASCII}]
正则表达式使用圆括号进行组匹配。
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
上面代码中,正则表达式里面有三组圆括号。
使用exec
方法,就可以将这三组匹配结果提取出来。
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31
组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如matchObj[1]
)引用,要是组的顺序变了,引用的时候就必须修改序号。
ES2018 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。
const RE_DATE = /(?\d{4})-(?\d{2})-(?\d{2}) /;
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // "1999"
const month = matchObj.groups.month; // "12"
const day = matchObj.groups.day; // "31"
上面代码中,“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(?
),然后就可以在exec
方法返回结果的groups
属性上引用该组名。同时,数字序号(matchObj[1]
)依然有效。
具名组匹配等于为每一组匹配加上了 ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。
如果具名组没有匹配,那么对应的groups
对象属性会是undefined
。
const RE_OPT_A = /^(?a+)?$ /;
const matchObj = RE_OPT_A.exec('');
matchObj.groups.as // undefined
'as' in matchObj.groups // true
上面代码中,具名组as
没有找到匹配,那么matchObj.groups.as
属性值就是undefined
,并且as
这个键名在groups
是始终存在的。
有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。
let {groups: {one, two}} = /^(?.*):(?.*)$ /u.exec('foo:bar');
one // foo
two // bar
字符串替换时,使用$<组名>
引用具名组。
let re = /(?\d{4})-(?\d{2})-(?\d{2}) /u;
'2015-01-02'.replace(re, '$/$/$' )
// '02/01/2015'
上面代码中,replace
方法的第二个参数是一个字符串,而不是正则表达式。
replace
方法的第二个参数也可以是函数,该函数的参数序列如下。
'2015-01-02'.replace(re, (
matched, // 整个匹配结果 2015-01-02
capture1, // 第一个组匹配 2015
capture2, // 第二个组匹配 01
capture3, // 第三个组匹配 02
position, // 匹配开始的位置 0
S, // 原字符串 2015-01-02
groups // 具名组构成的一个对象 {year, month, day}
) => {
let {day, month, year} = groups;
return `${day}/${month}/${year}`;
});
具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。
如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>
的写法。
const RE_TWICE = /^(?[a-z]+)!\k$ /;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false
数字引用(\1
)依然有效。
const RE_TWICE = /^(?[a-z]+)!\1$ /;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false
这两种引用语法还可以同时使用。
const RE_TWICE = /^(?[a-z]+)!\k!\1$ /;
RE_TWICE.test('abc!abc!abc') // true
RE_TWICE.test('abc!abc!ab') // false