正则一直是块难啃的骨头,乍一看就好复杂,各种符号字母交叉也不知道什么意思。编写一个正则,使用的时候是需要适应多种情况的,所以在掌握的不够深的时候,可能写出来的正则就容易出问题了。于是乎,大家就更倾向于复制粘贴大法咯,毕竟有些通用的正则,是能保证正确且足够可靠的。除了校验手机号码、邮箱这些常用的功能之外,其实正则是足够强大应用在很多方面的。正则很深奥,同时又很枯燥,要学好正则,可谓任重而道远啊。

1. RegExp 构造函数

通常使用 RegExp 构造函数有两种情况 第一种情况就是参数为字符串,这个时候第二个参数就是正则表达式的修饰符(flag)

var regexp = new RegExp('[A-Z]', 'i')

另一种情况,参数是一个正则表达式,返回的是这个正则表达式的拷贝

var regexp = new RegExp(/A-Z/i)

上面的这种情况,是没有没办法传正则表达式的修饰符作为第二个参数,ES6 则允许了这种情况

var regexp = new RegExp(/A-z/i, 'g')
regexp.flags // g

上面的代码中,第二个参数指定的修饰符,会覆盖掉原有的正则表达式的修饰符

2. 字符串的正则方法

to do …

3. u 修饰符

在字符串的扩展里也知道了很多 ES6 之前 JavaScript 是没办法识别大于 0xFFFF 的 Unicode 字符的,所以正则表达式也不能正确的处理大于 0xFFFF 的 Unicode 字符的,ES6 增加了 u 修饰符来解决这个问题。

/\ud848\udd04/.test('\ud848') // true
/\ud848\udd04/u.test('\ud848') // false

出了上面代码的情况,加了 u 修饰符之后还会改变下面这些代码的行为

  • 点标识符

    原本的(.)字符是没办法识别大于 0xFFFF 的 Unicode 字符的,ES6 中可以加上 u 修饰符

// "𢄄" 的 UNICODE 编码是 /\ud848\udd04/
var str = '𢄄'
/^.$/.test(str) // false
/^.$/u.test(str) // true
  • Unicode 字符表示法

    ES6 新增了使用大括号表示 Unicode 字符的方法,正则表达式中必须加上 u 修饰符才能正确识别这种表示方法

    /\u61/.test('a') // false
    /\u61/u.test('a') // true
    /\u{22104}/u.test('𢄄') // true
    
  • 量词

    /𢄄{2}/.test('𢄄𢄄') // false
    /𢄄{2}/u.test('𢄄𢄄') // true
    
  • 预定义模式

    /^\S$/.test('𢄄') // false
    /^\S$/u.test('𢄄') // true
    

    利用这一点可以写个正确返回字符串长度的函数

    function codePointLength(text) {
      const result = text.match(/\s\S/gu)
      return result ? result.length : 0
    }
    
    '𢄄𢄄'.length // 4
    codePointLength('𢄄𢄄') // 2
    
  • i 修饰符 有些 Unicode 字符的编码不同,但是字型很相近,比如,\u004B与\u212A都是大写的K

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

4. y 修饰符

ES6 还为正则表达式添加了 y 修饰符,叫做“粘连”(sticky)修饰符。

和 g 修饰符类似,也是全局匹配,不同点在于, y 修饰符规定后一次匹配必须从剩余位置的第一位开始。看下例子就明白了。

var s = 'aaa_aa_a'
var r1 = /a+/g
var r1 = /a+/y

r1.exec(s) // ['aaa']
r2.exec(s) // ['aaa']

// 两者匹配完第一次以后,剩余的字符串为 _aa_a
r1.exec(s) // ['aa']
r2.exec(s) // null

使用 lastIndex 属性

var s = 'abab'
var r = /a/y

r.lastIndex = 1
r.exec(s) // null

r.lastIndex = 2
r.exec(s) // ['a']

单单一个 y 修饰符对 match 方法,只能返回第一个匹配,必须与 g 修饰符配合使用,才能返回所有匹配。

'a1b1c1'.match(/a\d/y) // ['a1']
'a1b1c1'.match(/a\d/gy) // ['a1', 'b1', 'c1']

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

tokenize(TOKEN_Y, '3x + 4')
// [ '3' ]
tokenize(TOKEN_G, '3x + 4')
// [ '3', '+', '4' ]

function tokenize(TOKEN_REGEX, str) {
  let result = [];
  let match;
  while (match = TOKEN_REGEX.exec(str)) {
    result.push(match[1]);
  }
  return result;
}

上面代码中,使用 g 修饰符的正则表达式会忽略非法字符,而 y 修饰符不会,这样就很容易发现错误。

5. sticky 属性

ES6 新增的 sticky 属性用来表示是否设置了 y 修饰符。

6. flags 属性

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

var r = /a/ig

// ES5 的 source 属性返回正则表达式的正文
r.source // 'a'

// ES6 的 flags 属性返回正则表达式的正文
r.flags // 'a'

7. * s 修饰符: dotALL 模式

正则表达式中,(.)代表任意的单个字符,行终止符(line terminator character)除外。

现在有个提案,使用 s 修饰符的话,正则表达式中的(.)就能匹配包括行终止符的任意单个字符。

/foo.bar/.test('foo/nbar/') // false
// 一种变通的写法
/foo[^]bar/.test('foo/nbar/') // true

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

另外,还引入一个 dotAll 属性用来表示是否处在了 dotAll 模式。

8. * 后行断言

“先行断言”指的是,x 必须在 y 前面才匹配,写做 /x(?=y)/

“先行否定断言”指的是,x 只有不在 y 前面才匹配,写做 /x(?!y)/

括号中的部分不计入返回结果。

var s = '15% of 100 is 15'

s.match(/\d+(?=%)/g) // ["15"]

s.match(/\d+(?!%)/g) // ["1", "100", "15"]

目前有个提案是引入后行断言。

“后行断言”指的是,x 必须在 y 后面才匹配,写做 /(?<=y)x/

“后行否定断言”指的是,x 只有不在 y 后面才匹配,写做 /(?<!y)x/

var s = 'there are 4 shoes, they are worth about $60'

s.exec(/(?<=\$)d+/) // 60
s.exec(/(?<!\$)d+/) // 4

后行断言的匹配顺序和通常的数序是反过来的

正常情况下

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

上面正则匹配的顺序是,先是整个正则匹配成功的结果,然后是第一个括号、第二个括号对应的匹配成功的结果。

但如果是后行断言的话

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

todo… 再看个例子

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

从上面的代码可以看出反斜杠引用的 \1 也要按照相反的顺序来放(\1 的作用是在正则表达式内部获取分组匹配)

9. * Unicode 属性类

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

Unicode 属性类要指定属性名和属性值,因为这两种类只对 Unicode 有效,所以必须要加上前面学到的 u 修饰符

所以这两个类的正则表达式格式是这样子的

const r = /\p{UnicodePropertyName=UnicodePropertyValue}/
const r = /\P{UnicodePropertyName=UnicodePropertyValue}/

其中 \P{…} 是 \p 的反向匹配,即匹配所有不满足条件的字符。

对于某些属性,可以只写属性名

const r = /\p{UnicodePropertyName}/

各种应用

// 指定匹配一个希腊文字母
const regexGreekSymbol = /\p{Script=Greek}/u

const regex = /^\p{Decimal_Number}+$/u

// 匹配所有数字
const regex = /^\p{Number}+$/u

// 匹配所有的箭头字符
const regexArrows = /^\p{Block=Arrows}+$/u

10. * 具名组匹配

先来看一个分组匹配的例子

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/

const matchObj = RE_DATE.exec('2017-10-18')
const year = matchObj[0]
const month = matchObj[1]
const day = matchObj[2]

上面代码每一组的匹配是通过序号来获取的,如果组的顺序变了,引用的时候,还要更改序号。另外,每一组的匹配含义也不容易看出来。

现在则有个“具名组匹配”(Named Capture Groups)的提案,允许为每一组匹配指定一个名字,既便于阅读,又便于引用。

“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(?)。然后就可以在exec方法返回结果的groups属性上引用该组名。同时,数字序号(matchObj[1])依然有效。

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/

const matchObj = RE_DATE.exec('2017-10-18')
const year = matchObj.groups.year
const month = matchObj.groups.month
const day = matchObj.groups.day

如果具名组没有匹配,那么对应的 groups 对象属性会是 undefined。

** 解构赋值和替换 ** 利用具名组匹配可以使用解构赋值从匹配结果中为变量赋值。

const {group: {one, two}} = /^(?<one>.*):(?<two>.*)/.exec('bar:foo')

one // bar
two // foo

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

const r = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
'2017-10-18'.replace(r, '$<day>/$<month>/$<year>') // 18/10/2017

replace 方法的第二个参数可以是函数。

const r = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/

'2017-10-18'.replace(r, (
  matched, // 整个匹配结果
  capture1, //第一个组匹配
  capture2, //第二个组匹配
  capture3, //第三个组匹配
  position, // 匹配开始的位置 0
  S, // 原字符串
  groups // 具名组构成的一个对象 {year, month, day}
) => {
  let groups = {day, month, year} = arg[args.length - 1]
  return `${day}/${month}/${year}`
}

具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象,函数内部可以对其解构赋值。

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

const r = /(?<word>\w+)!\k<word>/
r.test('abc!abc') // true
r.test('abc!ab') // false

数字引用 \1 也依然有效

const r = /(?<word>\w+)!\1/
r.test('abc!abc') // true
r.test('abc!ab') // false