ECMAScript6-基础5-扩展正则

RegExp构造函数

概念

在之前,有2种方式来创建RegExp:

①第一个参数是字符串,第二个参数表示正则表达式的修饰符;

②就一个正则表示式;

到了ES6新增了1种方式:如果RegExp构造函数第一个参数是一个正则对象,那么还可以使用第二个参数指定修饰符,且返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符;

案例讲解/注意事项

new RegExp('xyz', 'i');

这是ES5的一种方式,该方式需要2个参数,该表达式等价于var regex = /xyz/i;

 

var regex = new RegExp(/xyz/i);

这是ES5的一种方式,该方式需要1个参数,该表达式等价于var regex = /xyz/i;

 

new RegExp(/abc/ig, 'i');

这是ES6新增的方式;

字符串的正则方法

概念

字符串对象共有4个方法可以使用正则表达式:match()、replace()、search()和split();

ES6将这4个方法做了调整,其内部实现都是基于RegExp实例方法来实现:

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]

新增u修饰符

概念

ES6对正则表达式添加了u修饰符,含义为“Unicode 模式”,即用来正确处理大于\uFFFF的Unicode字符,也就是正确处理四个字节的UTF-16编码;

案例讲解/注意事项

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

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

\uD83D\uDC2A是一个四个字节的UTF-16编码,代表一个字符。ES5不支持四个字节的UTF-16 编码,因此,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为false。

注意:

一旦加上u修饰符号,就会修改如下正则表达式的行为:

①点字符

点(.)字符在正则表达式中含义是:匹配除了换行符以外的任意单个字符,但是对于码点大于0xFFFF的Unicode字符,点字符不能识别,必须加上u修饰符;

如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败;原因之前讲过了,因为JS默认一个字符需要2个字节,而码点大于0xFFFF的Unicode字符需要4个字节,即JS会默认其有2个字符;

②Unicode字符表示法

ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词;

如果不加u修饰符,正则表达式无法识别\u{61}这种表示法,只会认为这匹配 61 个连续的u;

③量词

使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符;

④预定义模式

u修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF的Unicode字符;

\S是预定义模式,匹配所有非空白字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的 Unicode 字符,利用这一点,可以写出一个正确返回字符串长度的函数:

一个汉字,由于其是Unicode编码,应该是4字节,即变量s应该是4*2=8个字节,按照JS每2个字节算一个字符的惯例,即变量s应该是8/2=4个字符,所以,s.length = 4;

Length是按照字节数/2的方式来计算字符的个数,面对需要4个字节才能存储的汉字时,则无法反应真实情况;

⑤i修饰符

有些Unicode字符的编码不同,但字型很相近,比如\u004B与\u212A都是大写的K。

/[a-z]/i.test('\u212A') // false

/[a-z]/iu.test('\u212A') // true

不加u修饰符,就无法识别非规范的K字符

⑥转义

没有u修饰符的情况下,正则中没有定义的转义(如逗号的转义\,)无效,而在u模式会报错。

/\,/ // /\,/

/\,/u // 报错

没有u修饰符时,逗号前面的反斜杠是无效的,加了u修饰符就报错

RegExp.prototype.unicode 属性

概念

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

案例讲解/注意事项

const r1 = /hello/;

const r2 = /hello/u;

r1.unicode // false

r2.unicode // true

正则表达式是否设置了u修饰符,可以从unicode属性看出来;

新增y修饰符

概念

ES6 新增了y修饰符,又叫做“粘连”修饰符;

y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始;

不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义;

通俗来讲来讲就是:g只关心是否有匹配的字符,至于字符在什么位置,不关心;y既要关心是否有匹配的字符,且字符的位置必须是从剩余的第一个位置开始,否则y匹配失败;

案例讲解/注意事项

var s = 'aaa_aa_a';

var r1 = /a+/g;

var r2 = /a+/y;

 

r1.exec(s) // ["aaa"]

r2.exec(s) // ["aaa"]

 

r1.exec(s) // ["aa"]

r2.exec(s) // null

一个使用g修饰符,另一个使用y修饰符,r1与r2都是匹配多个a,然后这两个正则表达式各执行了两次:

第一次执行的时候,两者行为相同,剩余字符串都是“_aa_a”(注意这里以_作为开头)。由于g修饰没有位置要求,所以第二次执行会返回结果,而y修饰符要求匹配必须从头部开始,所以返回null;

 

 

 

 

 

'a1a2a3'.match(/a\d/y) // ["a1"]

'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]

每次匹配都是从剩余字符串的头部开始,实际上,y修饰符号隐含了头部匹配的标志^,y修饰符的设计本意,就是让头部匹配的标志^在全局匹配中都有效,因此,单单一个y修饰符对match方法,只能返回第一个匹配,必须与g修饰符联用,才能返回所有匹配;

每次匹配都是从剩余字符串的头部开始,实际上,y修饰符号隐含了头部匹配的标志^,y修饰符的设计本意,就是让头部匹配的标志^在全局匹配中都有效,因此,单单一个y修饰符对match方法,只能返回第一个匹配,必须与g修饰符联用,才能返回所有匹配;

扩展

y修饰符的一个应用,是从字符串提取 token(词元),y修饰符确保了匹配之间不会有漏掉的字符;

const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;

const TOKEN_G  = /\s*(\+|[0-9]+)\s*/g;

 

tokenize(TOKEN_Y, '3 + 4')

// [ '3', '+', '4' ]

tokenize(TOKEN_G, '3 + 4')

// [ '3', '+', '4' ]

 

function tokenize(TOKEN_REGEX, str) {

  let result = [];

  let match;

  while (match = TOKEN_REGEX.exec(str)) {

    result.push(match[1]);

  }

  return result;

}

如果字符串里面没有非法字符,y修饰符与g修饰符的提取结果是一样的。但是,一旦出现非法字符,两者的行为就不一样了。

tokenize(TOKEN_Y, '3x + 4')

// [ '3' ]

tokenize(TOKEN_G, '3x + 4')

// [ '3', '+', '4' ]

上面代码中,g修饰符会忽略非法字符,而y修饰符不会,这样就很容易发现错误;

RegExp.prototype.sticky属性

概念

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

案例讲解/注意事项

var r = /hello\d/y;

r.sticky // true

RegExp.prototype.flags属性

概念

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

案例讲解/注意事项

//ES5 的source属性,返回正则表达式的正文

/abc/ig.source

//"abc"

 

//ES6 的flags属性,返回正则表达式的修饰符

/abc/ig.flags

//'gi'

s修饰符dotAll模式

在正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外:

①四个字节的UTF-16字符,这个可以用u修饰符解决;

②行终止符,即该字符表示一行的终结,行终止符有:

U+000A换行符(\n)、U+000D回车符(\r)、

U+2028行分隔符、U+2029段分隔符;

 

为了解决这个问题,ES2018 引入s修饰符,从而让.可以匹配任意单个字符,这被称为dotAll模式,即点(dot)代表一切字符,对应着,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式;

案例讲解/注意事项

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

这里是普通的点(.)

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

这里是使用点(.)的s修饰符

 

 

const re = /foo.bar/s;

// 另一种写法  const re = new RegExp('foo.bar', 's');

re.test('foo\nbar') // true

re.dotAll // true

re.flags // 's'

/s修饰符和多行修饰符/m不冲突,两者一起使用的情况下,.匹配所有字符,而^和$匹配每一行的行首和行尾;

后行断言

概念

之前的正则表达式,只支持先行断言和先行否定断言,不支持后行断言和后行否定断言,到了ES2018 引入了后行断言;

查看w3school中关于JS的教程,链接地址如下:https://www.w3school.com.cn/jsref/jsref_obj_regexp.asp,在“量词”模块,

?=n:匹配任何其后紧接指定字符串 n 的字符串;

?!n:匹配任何其后没有紧接指定字符串 n 的字符串;

 

先行断言:x只有在y前面才能匹配,必须写成/x(?=y)/,例如只匹配百分号之前的数字,要写成/\d+(?=%)/;

先行否定断言:x只有不在y前面才能匹配,必须写成/x(?!y)/,例如只匹配不在百分号之前的数字,要写成/\d+(?!%)/。

后行断言:与先行断言相反,x只有在y后面才匹配,必须写成/(?<=y)x/,比如只匹配美元符号之后的数字,要写成/(?<=\$)\d+/。

后行否定断言:与先行否定断言相反,x只有不在y后面才匹配,必须写成/(?<!y)x/,比如只匹配不在美元符号后面的数字,要写成/(?<!\$)\d+/。

案例讲解/注意事项

①/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]

②/\d+(?!%)/.exec('that’s all 44 of them')                // ["44"]

①是先行断言,匹配%前面的数字,即100;

②是先行否定断言,匹配的后面没有%的数字,即44;

 

 

③/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]

④/(?<!\$)\d+/.exec('it’s is worth about €90')              // ["90"]

③是后行断言,匹配的$后面的数字,即100;

④是后行否定断言,匹配的是非$后面的数字,即90;

注意:

无论是先行断言(x只有在y前面才能匹配)、先行否定断言(x只有不在y前面才能匹配)、后行否定断言(x只有不在y后面才匹配)都是从左往右匹配;

而后行断言(x只有在y后面才匹配)则是从右往左匹配,即先匹配/(?<=y)x/的x,然后再回到左边,匹配y的部分;

JS的正则表达式有2大类:贪婪模式、非贪婪模式;正常情况下的使用量词即可进入贪婪模式,即尽可能多的匹配内容;非贪婪模式匹配尽可能少的内容,在量词后加上?即可进入该模式下;

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

②/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]

表达式②进入了贪婪模式,第一个括号是贪婪模式,第二个括号只能捕获一个字符,所以结果是105和3。

①是后行断言,由于执行顺序是从右到左,第二个括号是贪婪模式,第一个括号只能捕获一个字符,所以结果是1和053;

 

 

/(?<=(o)d\1)r/.exec('hodor')  // null

/(?<=\1d(o))r/.exec('hodor')  // ["r", "o"]

其次,“后行断言”的反斜杠引用,也与通常的顺序相反,必须放在对应的那个括号之前。

上面代码中,如果后行断言的反斜杠引用(\1)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。因为后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成反斜杠引用;

Unicode属性类

概念

ES2018引入了一种新的类的写法\p{...}和\P{...},允许正则表达式匹配符合Unicode某种属性的所有字符;

那么该如何指定Unicode的某种属性呢,格式如下:

\p{UnicodePropertyName=UnicodePropertyValue},即由Unicode 属性类来指定属性名、属性值;对于某些属性,可以只写属性名,或者只写属性值:\p{UnicodePropertyName}、\p{UnicodePropertyValue};

\P{…}是\p{…}的反向匹配,即匹配不满足条件的字符;

这两种类只对Unicode有效,所以使用的时候一定要加上u修饰符,如果不加u修饰符,正则表达式使用\p和\P会报错,ECMAScript 预留了这两个类;

正是由于结合Unicdoe,Unicode的各种属性非常多,所以这种新的类的表达能力非常强;

案例讲解/注意事项

const regexGreekSymbol = /\p{Script=Greek}/u;

regexGreekSymbol.test('π') // true

 

 

// 匹配所有数字

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

具名组匹配

概念

正则表达式使用圆括号()来进行组匹配的,但组匹配存在2个缺陷:

①每一组的匹配含义不容易看出来,特别是接手新项目的时候,往往不知道什么意思;

②由于只能用数字序号引用,如果组的顺序变了,对应的代码必须修改引用的序号;

为了解决这2个缺陷,ES2018引入了具名组匹配,允许为每一个组匹配指定一个名字,这样既方便阅读代码,又便于引用;具名组匹配组成如下:

在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(?<year>),然后就可以在exec方法返回结果的groups属性上引用该组名,且数字序号依然有效;

变相的为每一组匹配加上了ID,便于描述匹配的目的,如果组的顺序变了,也不用改变匹配后的处理代码;

如果组匹配失败,则对应的groups对象属性会是undefined;

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

应用具名组匹配后:

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

 

 

匹配失败后,具名组匹配的行为:

const RE_OPT_A = /^(?<as>a+)?$/;

const matchObj = RE_OPT_A.exec('');

matchObj.groups.as // undefined

'as' in matchObj.groups // true

案例讲解/注意事项

解构赋值和替换

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

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

one  // foo

two  // bar

字符串替换时,使用$<组名>引用具名组

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;

'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')

// '02/01/2015'

 

 

 

引用

如果要在正则表达式内部引用某个“具名组匹配”,可以使用\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

这两种引用语法还可以同时使用:

const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;

RE_TWICE.test('abc!abc!abc') // true

RE_TWICE.test('abc!abc!ab') // false

正则匹配索引

概述

正则匹配结果的开始位置和结束位置,目前获取并不是很方便。正则实例的exec()方法,返回结果有一个index属性,可以获取整个匹配结果的开始位置,但是如果包含组匹配,每个组匹配的开始位置,很难拿到;

现在有一个第三阶段提案,为exec()方法的返回结果加上indices属性,在这个属性上面可以拿到匹配的开始位置和结束位置;

案例讲解/注意事项

const text = 'zabbcdef';

const re = /ab/;

const result = re.exec(text);

result.index // 1

result.indices // [ [1, 3] ]包头不包尾

exec()方法的返回结果result,它的index属性是整个匹配结果(ab)的开始位置,而它的indices属性是一个数组,成员是每个匹配的开始位置和结束位置的数组。由于该例子的正则表达式没有组匹配,所以indices数组只有一个成员,表示整个匹配的开始位置是1,结束位置是3;

注意,开始位置包含在匹配结果之中,但是结束位置不包含在匹配结果之中,即包头不包尾,比如,匹配结果为ab,分别是原始字符串的第1位和第2位,那么结束位置就是第3位。

 

 

const text = 'zabbcdef';

const re = /ab+(cd)/;

const result = re.exec(text);

result.indices // [ [ 1, 6 ], [ 4, 6 ] ]

如果正则表达式包含组匹配,那么indices属性对应的数组就会包含多个成员,提供每个组匹配的开始位置和结束位

上面例子中,正则表达式包含一个组匹配,那么indices属性数组就有两个成员,第一个成员是整个匹配结果(abbcd)的开始位置和结束位置,第二个成员是组匹配(cd)的开始位置和结束位置;

 

 

const text = 'zabbcdef';

const re = /ab+(?<Z>cd)/;

const result = re.exec(text);

result.indices.groups // { Z: [ 4, 6 ] }

如果正则表达式包含具名组匹配,indices属性数组还会有一个groups属性。该属性是一个对象,可以从该对象获取具名组匹配的开始位置和结束位置。

 

 

const text = 'zabbcdef';

const re = /ab+(?<Z>ce)?/;

const result = re.exec(text);

result.indices[1] // undefined

result.indices.groups['Z'] // undefined

如果获取组匹配不成功,indices属性数组的对应成员则为undefined,indices.groups属性对象的对应成员也是undefined;

String.prototype.matchAll()

概念

如果一个正则表达式在字符串里面有多个匹配,现在一般使用g修饰符或y修饰符,在循环里面逐一取出;ES2020增加了String.prototype.matchAll()方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组;

案例讲解/注意事项

const string = 'test1test2test3';

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()方法就可以了:、

// 转为数组的方法一

[...string.matchAll(regex)]

// 转为数组的方法二

Array.from(string.matchAll(regex))

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值