跳转到内容

第五章 正则的扩展

ES6 为正则扩展了很多方法和修饰符,本章节主要对这些方法做详细地讲述。

RegExp 构造函数

在 ES5 中,RegExp 构造函数有两种情况的参数处理方式:

  1. 当参数是字符串时,第一个参数表示正则表达式的模式,第二个参数表示正则表达式的修饰符。

  2. 当参数是一个正则表达式时,会返回一个原有正则表达式的拷贝。

js
var regex = new RegExp('xyz', 'l');
// 等价于
var regex = /xyz/l;
js
var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;

然而,ES5 不允许在第一个参数为正则表达式时,使用第二个参数添加修饰符,否则会报错。例如:

js
var regex = new RegExp(/xyz/, 'l');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another

在 ES6 中,这种行为发生了变化。如果 RegExp 构造函数的第一个参数是一个正则对象,那么可以使用第二个参数来指定修饰符,并且返回的正则表达式会忽略原有正则表达式的修饰符,只使用新指定的修饰符。例如:

js
new RegExp(/abc/g, 'i');
// 等价于 /abc/i;

总结一下,ES6 允许在使用正则对象作为第一个参数时,通过第二个参数来指定新的修饰符,并忽略原有正则对象的修饰符。

字符串的正则方法

在 ES6 中,字符串对象的方法 match(), replace(), search()split() 全部调用了 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] 方法。

这意味着在 ES6 中,对字符串对象调用这些方法时,实际上是通过调用相应的 RegExp 实例方法来实现的。这种变化使得正则表达式相关的方法统一定义在了 RegExp 对象上,提高了代码的一致性和可读性。

u 修饰符

在 ES6 中,添加了 "u" 修饰符,代表 "Unicode 模式",用来正确处理大于 \uFFFF 的 Unicode 字符,即能正确处理超过两个字节的 UTF-16 编码字符。

举例来说:

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

在上面的代码中,\uD83D\uDC2A 是一个超过两个字节的 UTF-16 编码字符,代表一个字符。在 ES5 中不支持超过两个字节的 UTF-16 编码,会将其识别为两个字符,导致第一行代码结果为 true。而在 ES6 中使用了 "u" 修饰符,就会正确识别为一个字符,因此第一行代码结果为 false

通过添加 "u" 修饰符,可以修改正则表达式的行为,使其能正确处理超过两个字节的 UTF-16 编码字符,确保对 Unicode 字符的处理更加准确和全面。

除此之外,以下的正则表达式行为也会得到修正:

  1. 点字符

    点字符 (.) 表示除换行符以外任意单个字符,但是对于码点大于 0xFFFF 的 Unicode 字符无法识别,加上 u 修饰符后就可以修正。

    js
    let a = '𠮷'
    
    /^.$/.test(a) // false
    /^.$/u.test(a) // true
  2. Unicode 字符表示法

    ES6 新增大括号内使用 Unicode 字符的表示法,如果不使用 u 则无法识别大括号。

    js
    /\u{61}/.test('a') // false
    /\u{61}/u.test('a') // true
  3. 量词

    修正后,所有量词都能识别大于 0xFFFF 的Unicode 字符。

    js
    /a{2}/.test('aa') // true
    /a{2}/u.test('aa') // true
    /𠮷{2}/.test('𠮷𠮷') // false
    /𠮷{2}/u.test('𠮷𠮷') // true
  4. 预定义模式

    同样的,u 也能修正预定义模式识别大于 0xFFFF 的 Unicode 字符。

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

    由此可以封装一个函数,用于返回包含大于 0xFFFF 字符的字符串长度。

    js
    function codePointLength(text) {
      let len = text.match(/[\s\S]/gu)
      return len ? len.length : 0
    }
  5. i 修饰符

    有些字符有多个编码,通过 u 可以修正无法识别非规范字符的问题。

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

y 修饰符

ES6 引入的粘连修饰符 "y" 要求匹配必须从剩余的第一个位置开始,并且遵守 lastIndex 属性的指定,只有在 lastIndex 指定的位置发现匹配时才会成功,否则返回 null

粘连修饰符隐含了头部匹配的标志,只有紧跟前面的分隔符才会被识别。单独使用粘连修饰符对 match 方法只能返回第一个匹配,必须与全局修饰符 "g" 联用才能返回所有匹配。

粘连修饰符在从字符串中提取词元时很有用。

示例代码:

javascript
const REGEX = /a/y ; 
// 指定从 号位直开始匹配
REGEX.lastindex = 2 ; 
// 不是粘连, 匹配失败
REGEX.exec('xaya') // null 
// 指定从3号位置开始 匹配
REGEX.lastindex = 3 ; 
// 3号位置是粘连,匹配成功
const match= REGEX.exec('xaxa'); 
match.index // 3 
REGEX.lastindex // 4

// 后续的分隔符只有紧跟前面的分隔符才会被识别
const REGEX = /a/gy;
console.log('aaxa'.replace(REGEX, '-')); // 输出:'--xa'

// 单独 修饰符对 match 方法只能返回第 个匹配,必须与 修饰符联用才能返回所有匹配
const str = 'a1a2a3';
console.log(str.match(/a\d/y)); // 输出:['a1']
console.log(str.match(/a\d/gy)); // 输出:["a1", "a2", "a3"]

// y修饰符确保了匹配之间不会有漏掉的字符
const TOKEN_Y = /\s*(\+\d+)\s*ly;
const TOKEN_G = /\s*(\+\d+)\s*lg;
console.log(tokenize(TOKEN_Y, '3 + 4 ')); // 输出:['3', '+', '4']
console.log(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" 的作用,包括替换字符串中的内容、匹配数字等操作,以及在提取词元时的应用。

sticky 属性

y 修饰符相匹配,用于表示正则表达式是否设置了 y 修饰符,返回一个布尔值。

js
let reg = /a/y

console.log(reg.sticky) // true

flags 属性

ES5 中正则表达式有 source 属性,用于获取正则表达式的正文;ES6 新增了 flags 属性,用于获取正则表达式的修饰符。

js
let reg = /abc/gi

reg.source // 'abc'
reg.flags // 'gi'

s 修饰符:dotAll 模式

ES5 正则表达式 点符号无法识别以下4种 “行终止符”:

  • U+000A 换行符 (\n)
  • U+000D 回车符 (\r)
  • U+2028 行分隔符 (line separator)
  • U+2029 段分隔符 (paragraph separator)

ES6 有一个提案用于解决该问题。通常情况下,点(.)可以匹配任意单个字符,但不包括行终止符。为了解决这个问题,提案引入了 dotAll 修饰符(s),使得点(.)可以匹配任何单个字符,包括行终止符。

使用 dotAll 模式和 dotAll 属性,可以让正则表达式处于 dotAll 模式下,使点(.)代表任意单个字符。示例代码如下:

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

console.log(re.test('foo\nbar')); // 输出 true
console.log(re.dotAll); // 输出 true
console.log(re.flags); // 输出 's'

同时,dotAll 修饰符(s)和多行修饰符(m)不会有冲突,两者可以同时使用。在这种情况下,“.”将匹配所有字符,而"^"和"$"将匹配每一行的行首和行尾。

后行断言

后行断言是 JavaScript 中正则表达式的一种特性,用于匹配字符串中某个位置的左侧内容。它的写法为 /(?<=y)x/,其中 x 表示要匹配的内容,y 表示后行断言的条件。

下面是一些使用后行断言的具体示例代码:

javascript
const regex = /(?<=\$)\d+/;
const result = regex.exec('The price is $50');
console.log(result); // 输出 ["50"]
javascript
const regex2 = /(?<!\$)\d+/;
const result2 = regex2.exec('The price is $50');
console.log(result2); // 输出 null
javascript
const text = '$foo foo foo';
const regex3 = /(?<=\$)foo/g;
const replacedText = text.replace(regex3, 'bar');
console.log(replacedText); // 输出 "$bar foo foo"

需要注意的是,后行断言的执行顺序与其他正则操作相反。同时,在后行断言中,组匹配和反斜杠引用的使用方式与通常的顺序相反,需要将反斜杠引用放在对应的括号之前。

js
/(?<=(o)d\1)r/.exec('hodor') // null 表示只有在字母 "o" 后面跟着一个重复的字母 "d" 时才匹配字母 "r"。由于字符串 "hodor" 中没有符合这个条件的位置,所以匹配结果为 null。

/(?<=\1d(o))r/.exec('hodor'// ['r', 'o'] 表示只有在字母 "o" 前面是一个重复的字母 "d" 时才匹配字母 "r"。在字符串 "hodor" 中,字母 "o" 前面确实是一个重复的字母 "d",所以匹配结果为 ['r', 'o'],其中第一个元素是匹配到的字符 "r",第二个元素是后行断言中的子表达式 \1d(o) 匹配到的内容 "o"。

Unicode 属性类

有一个提案 引入了 Unicode 属性类(\p{...})和反向 Unicode 属性类(\P{...})写法,来匹配符合特定 Unicode 属性的字符,以及一些例子来展示不同属性类的用法。

  1. 匹配希腊文字母:

    js
    const regexGreekSymbol = /\p{Script=Greek}/u;
    console.log(regexGreekSymbol.test('pai')); // true
  2. 匹配所有十进制字符:

    js
    const regexDecimalNumber = /<\p{Decimal Number}+/u;
    console.log(regexDecimalNumber.test('12341-$~7890123456')); // true
  3. 匹配所有数字,包括罗马数字:

    js
    const regexNumber = /<\p{Number}+/u;
    console.log(regexNumber.test('231')); // true
    console.log(regexNumber.test('I II III IV V VI VII VIII IX')); // true
  4. 匹配各种文字的所有字母:

    js
    const regexAlphabetic = /[<\p{Alphabetic}\p{Mark}\p{Decimal Number}.-]\p{Connector_Punctuation}\p{Join_Control}/;
  5. 匹配所有的箭头字符:

    js
    const regexArrows = /<\p{Block=Arrows}+$/u;
    console.log(regexArrows.test('⬅️➡️⬆️⬇️')); // true

⚠ 注意

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

具名组匹配

正则表达式有一个组匹配的功能,使用方法如下:

js
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj[1]; //1999
const month = matchObj[2]; // 12
const day = match0bj[3]; // 31

但是有一个缺点,组的匹配含义不容易看出来 而且只能用数字序号引用,要是 组的顺序 变了,引用的时候就必须修改序号。

具名组匹配(Named Capture Groups)来为正则表达式中的组匹配添加 ID,使得在处理匹配结果时更加清晰和方便。具名组匹配可以让我们使用组的名称而不是数字序号来引用匹配结果。

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

定义具名组匹配的正则表达式:

js
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

通过给组匹配添加名称,我们可以更清晰地描述每个组匹配的含义,并且在处理匹配结果时可以直接通过组名来引用,而不需要关心组的顺序是否改变。

另外,如果具名组没有找到匹配,对应的 groups 对象属性会是 undefined,但是组名仍然会存在于 groups 对象中。

示例代码演示了具名组匹配未找到匹配时的情况:

js
const RE_OPT_A = /^(?<as>a+)?$/
const matchObj = RE_OPT_A.exec('');
console.log(matchObj.groups.as); // undefined
console.log('as' in matchObj.groups); // true

在这个例子中,具名组 as 没有找到匹配,因此 matchObj.groups.as 的值是 undefined,但是 as 这个键名仍然存在于 groups 对象中。

具名组匹配的优势之一是可以通过解构赋值直接从匹配结果中为变量赋值。下面是一些关于具名组匹配和字符串替换的示例代码:

  1. 使用解构赋值从匹配结果中为变量赋值:

    js
    let {groups: {one, two}} = /(?<one>foo):(?<two>bar)/.exec('foo:bar');
    console.log(one); // foo
    console.log(two); // bar
  2. 字符串替换时使用具名组引用:

    js
    let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
    console.log('2015-01-02'.replace(re, '$<day>/$<month>/$<year>')); // '02/01/2015'

    在这个例子中,replace 方法的第二个参数是一个字符串,其中使用了具名组的引用来进行字符串替换。

  3. replace 方法的第二个参数也可以是一个函数,该函数的参数序列包括整个匹配、各个捕获组的值以及具名组构成的对象。下面是一个示例代码:

    js
    '2015-01-02'.replace(re, (
      matched, capture1, capture2, capture3, position, wholeString, groups
    ) => {
      let {day, month, year} = groups;
      return `${day}/${month}/${year}`;
    });

    在这个例子中,我们可以直接对传入的 groups 对象进行解构赋值,从而方便地获取具名组的值进行处理。

通过具名组匹配和字符串替换,可以做到更加灵活和方便地处理正则表达式的匹配结果和字符串替换操作。

在正则表达式中,可以使用具名组匹配来给子表达式命名,并且可以在正则表达式内部通过 \k<groupName> 的方式引用这些具名组匹配。同时,也可以使用传统的数字引用(如\1\2)来引用捕获到的子表达式。这些引用语法可以单独或者结合使用,从而实现复杂的匹配逻辑,包括要求字符串以重复的单词结尾或者包含多次重复的单词等。这些灵活的引用方式使得正则表达式能够更加强大和灵活地进行匹配操作。

js
const RE_TWICE= /"(?<word>[a-z]+)!\k<word>$/ ; 
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false
js
const RE_TWICE = /^(?<word>[a-z]+)!\1$/; 
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false
js
RE_TWICE.test('abc!abc!abc') // true 
RE_TWICE.test('abc!abc!ab') // false

总结

ES6 在正则表达式方面确实做了很多扩展和改进,让正则表达式的使用变得更加方便和强大。主要的改进包括:

  1. RegExp 构造函数的参数改进:ES6 允许在使用正则对象作为第一个参数时,通过第二个参数来指定新的修饰符,并忽略原有正则对象的修饰符。
  2. 字符串的正则方法改进:ES6 中字符串对象的方法 match(), replace(), search()split() 全部调用了 RegExp 对象的实例方法,使得所有与正则表达式相关的方法都定义在 RegExp 对象上,提高了代码的一致性和可读性。
  3. 添加 "u" 修饰符:用于正确处理大于 \uFFFF 的 Unicode 字符,使得正则表达式能够正确识别超过两个字节的 UTF-16 编码字符。
  4. 添加 "y" 修饰符(粘连修饰符):要求匹配必须从剩余的第一个位置开始,并且遵守 lastIndex 属性的指定,只有在 lastIndex 指定的位置发现匹配时才会成功,否则返回 null
  5. 添加 sticky 属性:用于表示正则表达式是否设置了 "y" 修饰符,返回一个布尔值。
  6. 添加 flags 属性:用于获取正则表达式的修饰符。
  7. 添加 "s" 修饰符(dotAll 模式):使得点(.)可以匹配任意单个字符,包括行终止符。
  8. 添加后行断言:用于匹配字符串中某个位置的左侧内容,可以更清晰地描述每个组匹配的含义,并且在处理匹配结果时可以直接通过组名来引用。
  9. 添加 Unicode 属性类:用于匹配符合特定 Unicode 属性的字符,提高了对 Unicode 字符的处理能力。

以上这些改进和扩展使得 ES6 中的正则表达式更加强大和灵活,可以更好地满足开发者在处理复杂字符串匹配和处理方面的需求。