ES6标准入门 第五章:正则的扩展
1、RegExp 构造函数
ES5的缺陷:
ES5中构造函数的参数有两种情况:
(1)参数是字符串,这时第二个参数表示正则表达式的的修饰符(flag)。
var reg = new RegExp( "xyz", "i" ); 等价于 var reg = /xyz/i;
(2)参数是一个正则表达式,返回一个原有正则表达式的拷贝。
var reg = new RegExp( /xyz/i ); 等价于 var reg = /xyz/i;
不允许使用第二个参数指定修饰符,否则会报错。
var regex = new RegExp(/xyz/, 'i'); // 报错
ES6的改进:
ES6中RegExp构造函数 第一个参数是一个正则表达式,可以使用第二个参数只当修饰符。 指定的修饰符会覆盖原有正则表达式的修饰符。
new RegExp(/abc/ig, 'i').flags; // i
2、字符串的正则方法
字符串对象有四个方法可以调用正则表达式: match()、replace()、search()、split()
ES6内部,将与正则相关的方法全部定义在 RegExp对象上;语言内部全部调用 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]
3、u修饰符
ES5不支持四字节的UTF-16编码,将其识别为两个字符。
/^\uD83D/.test('\uD83D\uDC2A'); // true
ES6对正则表达式添加了u修饰符,含义为“Unicode”模式;用来正确处理 大于 /uFFFF 的字符【四字节的UTF-16编码】。将其识别为一个字符。
/^\uD83D/u.test('\uD83D\uDC2A'); // false
加上u修饰符,以下正则表达式的行为就会被修改:
(1)点字符
点字符(.
)在正则中的含义:除了换行符以外的任意单个字符。
点字符不能识别大于0xFFFF 的Unicode字符,必须加上u修饰符才能识别。
var s = '𠮷'; /^.$/.test(s) // false -- 将s识别为两个字符 /^.$/u.test(s) // true
(2)Unicode字符表示法
ES6新增使用大括号表示Unicode字符,此写法在正则表达式中必须加u修饰符才能识别大括号,否则被解读为量词。
/\u{61}/.test('a') // false 误认为匹配连续的61个u /\u{61}/u.test('a') // true /\u{20BB7}/u.test('𠮷') // true
(3)量词
使用u修饰符后,所有的量词都能识别 码点大于 /uFFFF 的Unicode字符。
/a{2}/.test('aa') // true /a{2}/u.test('aa') // true /𠮷{2}/.test('𠮷𠮷') // false /𠮷{2}/u.test('𠮷𠮷') // true
(4)预定义模式
u修饰符也会影响到预定义模式 使其能够正确匹配码点大于 /uFFFF 的Unicode字符。
\S是预定义模式,匹配所有的非空白字符。
/^\S$/.test('𠮷') // false /^\S$/u.test('𠮷') // true
附加: 利用这一点,写出一个正确返回字符串长度的函数。
function codePointLength (text) { var result = text.match( /[\s\S]/gu ); return result ? result.length : 0; } var s = '𠮷𠮷'; s.length // 4 codePointLength(s) // 2 转换为数组['𠮷','𠮷']
(5)i 修饰符
有些Unicode 字符的编码不同,但是字型很接近,\u004B
与\u212A
都是表示大写的K。
/[a-z]/i.test('\u004') // true
/[a-z]/i.test('\u212A') // false /[a-z]/iu.test('\u212A') // true
上面代码中,不加u 修饰符,无法识别非规范的k字符。
4、y修饰符(粘连修饰符)
y修饰符的作用 与 g修饰符类似,都是全局匹配;后一次匹配都是从上一次匹配成功的下一个位置开始。
不同之处在于:g修饰符只要剩余位置中存在匹配即可。y修饰符确保匹配从剩余的第一个位置开始(这就是粘连sticky的含义)。
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
以上代码对 y修饰符的改进:
var s = 'aaa_aa_a'; var r = /a+_/y; r.exec(s) // ["aaa_"] r.exec(s) // ["aa_"]
lastIndex
属性 指定每次搜索的开始位置。
g修饰符的情况:
const REGEX = /a/g; // 指定从2号位置(y)开始匹配 REGEX.lastIndex = 2; // 匹配成功 const match = REGEX.exec('xaya'); // 在3号位置匹配成功 match.index // 3 // 下一次匹配从4号位开始 REGEX.lastIndex // 4 // 4号位开始匹配失败 REGEX.exec('xaya') // null
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
实际上,y修饰符隐藏了头部匹配的标志(^); y修饰符设计的本意就是让头部匹配的标志(^)在全局匹配中有效。
split方法中使用y 修饰符,原字符串必须以分隔符开头。匹配成功第一个数组元素一定是空字符串。
//没有找到匹配 "x##".split(/#/y); ['x##'] //找到两个匹配 "##x".split(/#/y); ['', '', 'x'] //找到一个匹配 "#x#".split(/#/y); ['', 'x#']
字符串对象的replace方法。
const REG = /a/gy;
'aaxa'.replace(REG, '-'); //'--xa'
const REG = /a/g;
'aaxa'.replace(REG, '-'); //'--x-'
const REG = /a/y;
'aaxa'.replace(REG, '-'); //'-axa'
单单一个y
修饰符对replace方法,只能返回第一个匹配,必须与g
修饰符联用,才能返回所有匹配。
字符串对象的match 方法。
'a1a2a3'.match(/a\d/y) // ["a1"] 'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]
单单一个y
修饰符对match
方法,只能返回第一个匹配,必须与g
修饰符联用,才能返回所有匹配。
y修饰符的一个应用:从字符串提取token(词元),确保匹配之间不会有漏掉的字符。
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; }
tokenize(TOKEN_Y, '3x + 4')
// [ '3' ]
tokenize(TOKEN_G, '3x + 4')
// [ '3', '+', '4' ]
5、sticky属性
ES6新增的 sticky属性与y修饰符是对应的;表示是否设置了y修饰符。
var r = /hello\d/y; r.sticky // true
6、flags 属性
ES6为正则表达式新增了flags 属性,返回正则表达式的修饰符。
// ES5 的 source 属性 // 返回正则表达式的正文 /abc/ig.source // "abc" // ES6 的 flags 属性 // 返回正则表达式的修饰符 /abc/ig.flags // 'gi'
7、s修饰符:dotALL模式 --> ES2018
正则表达式中,点(.
)代表任意单个字符。但是有两个例外:
(1)四字节的UTF-16字符。【可以用 u修饰符解决】
(2)行终止符。
行终止符:该字符表示一行的终结。 有以下四种终止符:
- U+000A 换行符(
\n
) - U+000D 回车符(
\r
) - U+2028 行分隔符(line separator)
- U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar') // false
匹配任意单个字符的变通方法:
/foo[^]bar/.test('foo\nbar') // true
ES2018 的s修饰符可以匹配任意字符:
/foo.bar/s.test('foo\nbar') // true
这被称为dotAll模式; 即(dot)点代表一切字符。
正则表达式还引入了dotAll 属性,返回一个布尔值,表示该正则表达式是否处于 dotAll 模式。
const re = /foo.bar/s; // 另一种写法 // const re = new RegExp('foo.bar', 's'); re.test('foo\nbar') // true re.dotAll // true -- 处于 dotAll 模式 re.flags // 's'
/s
修饰符和多行修饰符/m
不冲突,两者一起使用的情况下,.
匹配所有字符,而^
和$
匹配每一行的行首和行尾。
8、后行断言 --> ES2018
JavaScript的正则表达式,只支持先行断言 和 先行否定断言。
ES2018引入了后行断言 和 后行否定断言。
(1)ES5中:
先行断言:x只有在y前面才匹配,必须写成 /x(?=y)/
例如:只匹配百分号之前的数字,/\d+(?=%)/
先行否定断言:x只有不在y前面才匹配,必须写成 /x(?!y)/
例如:只匹配不在百分号之前的数字, /\d+(?!y)/
/\d+(?=%)/.exec('100% of US presidents have been male') // ["100"] /\d+(?!%)/.exec('that’s all 44 of them') // ["44"]
”先行断言“括号之中的部分((?=%)
),是不计入返回结果的。
(2)ES2018新增:
后行断言:x 只有在y 后面才匹配,必须写成 /(?<=y)x/
例如:只匹配美元符号之后的数字, /(?<=\$)\d+/
后行否定断言:x 只有不在y 后面才匹配,必须写成 /(?<!y)x/
例如:只匹配不在美元符号后的数字, /(?<!\$)\d+/
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"] /(?<!\$)\d+/.exec('it’s is worth about €90') // ["90"]
“后行断言”的括号之中的部分((?<=\$)
),也是不计入返回结果
例如: 使用后行断言进行字符串的替换。
const RE_DOLLAR_PREFIX = /(?<=\$)foo/g; '$foo %foo foo'.replace(RE_DOLLAR_PREFIX, 'bar'); // '$bar %foo foo'
后行断言的实现,需要先匹配/(?<=y)x/
的x
,然后再回到左边,匹配y
的部分。 这种“先右后左”的执行顺序,导致了一些不符合预期的情况:
(1)“后行断言”的组匹配,与正常情况下的结果是不一样的。?????
/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"] /^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]
上面代码中,需要捕捉两个组匹配。
没有“后行断言”时,第一个括号是贪婪模式,第二个括号只能捕获一个字符,所以结果是105
和3
。
而“后行断言”时,由于执行顺序是从右到左,第二个括号是贪婪模式,第一个括号只能捕获一个字符,所以结果是1
和053
。
(2)“后行断言”的反斜杠的引用,也与正常情况相反。?????
/(?<=(o)d\1)r/.exec('hodor') // null /(?<=\1d(o))r/.exec('hodor') // ["r", "o"]
上面代码中,如果后行断言的反斜杠引用(\1
)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。
因为后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成反斜杠引用。
9、Unicode 属性类 --> ES2018
ES2018 引入了一种新的类的写法 \p{...} 和 \P{...} 。
\p{...} 匹配符合 Unicode 某种属性的所有字符。 \P{...} 为反向匹配,即匹配不符合条件的字符。
注意:这两种类只对Unicode 有效,所有使用时要加上 u 修饰符; 如果不加 u 修饰符,正则表达式使用 \p 或 \P 会报错。
Unicode 属性类要指定属性名和属性值。
\p{UnicodePropertyName=UnicodePropertyValue}
对于某些属性,可以只写属性名,或者只写属性值。
\p{UnicodePropertyName}
\p{UnicodePropertyValue}
例如: 希腊文字幕的匹配
const regexGreekSymbol = /\p{Script=Greek}/u; regexGreekSymbol.test('π') // true
例如:十进制字符的匹配
const regex = /^\p{Decimal_Number}+$/u; regex.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
10、具名组匹配
(1)简介
正则表达式使用圆括号 () 进行组匹配。
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
// 使用exec 方法就可以将三组圆括号中的三组匹配结果提取出来
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 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const matchObj = RE_DATE.exec('1992-09-12');
const year = matchObj.groups.year;
const month = matchObj.groups.month;
const day = matchObj.groups.day;
“具名组匹配”在圆括号内部,模式的头部添加 “问号 + 尖括号 + 组名”(?<year>
),然后就可以在exec 方法返回结果的 groups属性 上引用改组名。
数字序号 matchObj[1] 仍然有效!!
如果具名组 没有匹配,那么对应的 groups 对象属性会是 undefined。
const REO = /^(?<as>a+)?$/;
const matchObj = REO.exec('');
matchObj.groups.as; // undefined
'as' in matchObj.groups; //true
(2)解构赋值 和 替换
有了具名匹配之后,就可以使用解构赋值直接从匹配结果上为变量赋值。
let {groups: {one, two} } = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one; // foo
two; // bar
字符串替换的时候:
1、replace 方法的第二个参数是一个字符串;可以使用 $<组名> 来引用具名组。
let re = /(?<year>\d{4})-(?<month>\d{2}-(?<day>\d{2}))/u; '2011-01-01'.replace(re, '$<day>/$<month>/$<year>'); // 第二个参数是字符串,不是正则!!
2、replace 方法的第二个参数是函数;该参数的参数序列如下。
'2011-01-01'.replace(re, (
matched, //整个匹配结果 2011-01-01
capture1, //第一个组匹配 2011
capture2, //第二个组匹配 01
caprure3, //第三个组匹配 01
position, //匹配开始的位置 0
s, // 原字符串 2011-01-01
groups // 局名组构成的一个对象 {year, month, day}
) => {
let {year, month, year} = args[args.length - 1];
return `${day}/${month}/${year}`; //模板字符串
})
(3)引用
如果要在正则表达式中引用某个具名组匹配,可以使用 \k<组名> 的写法。
let RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test('abc!abc'); //true
RE_TWICE.test('abc!ab'); //false
引用数字 \1 依然有效。
let 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
11、String.prototype.matchAll
若一个正则表达式在字符串里面有多个匹配,一般使用 g 修饰符 或者 y 修饰符,然后循环逐一取出。
var regex = /t(e)(st(\d?))/g; var string = 'test1test2test3'; var matches = []; var match; while (match = regex.exec(string)) { matches.push(match); } matches // [ // ["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.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"]
for...of... 循环可以取出遍历器的内容。相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源。
遍历器转换为数组的方法:
(1)...运算符
[...string.matchAll(regex)]
(2)Array.from方法
Array.from(string.matchAll(regex));