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"]

上面代码中,需要捕捉两个组匹配。

没有“后行断言”时,第一个括号是贪婪模式,第二个括号只能捕获一个字符,所以结果是1053

而“后行断言”时,由于执行顺序是从右到左,第二个括号是贪婪模式,第一个括号只能捕获一个字符,所以结果是1053

 

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

 

posted @ 2018-03-19 19:53  见证LBJ  阅读(369)  评论(0编辑  收藏  举报