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)) |