ECMAScript 6 学习笔记(四):正则的扩展

u 修饰符

“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。

/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true

正则实例对象新增 unicode 属性,表示是否设置了 u 修饰符。

y 修饰符

“粘连”(sticky)修饰符,y 修饰符的作用与 g 修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g 修饰符只要剩余位置中存在匹配就可,而 y 修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。y 修饰符的设计本意,就是让头部匹配的标志 ^ 在全局匹配中都有效。

const REGEX = /a/y;

// 指定从2号位置开始匹配
REGEX.lastIndex = 2;

// 不是粘连,匹配失败
REGEX.exec('xaya') // null

// 指定从3号位置开始匹配
REGEX.lastIndex = 3;

// 3号位置是粘连,匹配成功
const match = REGEX.exec('xaya');
match.index // 3
REGEX.lastIndex // 4

ES6 的正则实例对象多了 sticky 属性,表示是否设置了 y 修饰符。

s 修饰符

ES6 之前,正则中的“.”无法匹配四个字节的 UTF-16 字符与行终止符,因此 ES6 引入了 s 修饰符,也被称为 dotAll 模式,即点(dot)代表一切字符。同时,正则表达式还引入了一个 dotAll 属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。

/foo.bar/.test('foo\nbar') // false
/foo.bar/s.test('foo\nbar') // true

flag 属性

ES6 为正则表达式新增了 flags 属性,会返回正则表达式的修饰符。

// ES5 的 source 属性,返回正则表达式的正文
/abc/ig.source
// "abc"

// ES6 的 flags 属性,返回正则表达式的修饰符
/abc/ig.flags
// 'gi'

后行断言

JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。ES2018 引入后行断言,V8 引擎 4.9 版(Chrome 62)已经支持。

至此,JavaScript 终于完美支持这四种匹配方法:

// 先行断言,匹配匹配项前面的内容
/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them')                 // ["44"]

// 后行断言,匹配匹配项后面的内容
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90')                // ["90"]

与其他正则操作相反,后行断言的匹配方式是“先右后左”,即后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成匹配,因此可能会导致不符合预期的行为:

/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]

Unicode 属性类

ES2018 引入了一种新的类:\p{}\P{},允许正则表达式匹配符合 Unicode 某种属性的所有字符。其中 \P{}\p{} 的反向匹配,即匹配不满足条件的字符。需要注意的是,这两种类只对 Unicode 有效,所以使用的时候一定要加上 u 修饰符。

const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true

具名组匹配

正则表达式使用圆括号进行组匹配,使用 exec() 就可以将匹配结果提取出来。

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
const matchObj = RE_DATE.exec('2019-12-31');
const year = matchObj[1]; // 2019
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31

为了方便使用,采用具名组匹配,可以为每一个组指定名字

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\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

有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。

let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one  // foo
two  // bar

如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法。

const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false

数字引用(\1)依然有效。

const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false

字符串取得所有匹配

使用 String.prototype.matchAll 方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。

const string = 'test1test2test3';

// g 修饰符加不加都可以
const regex = /t(e)(st(\d?))/g;

for (const match of string.matchAll(regex)) {
  console.log(match);
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]

上面代码中,由于 string.matchAll(regex) 返回的是遍历器,所以可以用 for…of 循环取出。相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源。除此之外,额可以使用 … 运算符和 Array.from 方法将遍历器转为数组。

点赞

发表评论

电子邮件地址不会被公开。