ECMAScript 6 入門——正則的擴展

五、正則的擴展

1.RegExp構造函數

  • ES5中,RegExp構造函數的參數有兩種情況
    (1)兩個參數且都是字符串,這時第二個參數表示正則表達式的修飾符(flag)
    (2)一個參數而是正則表達式,這時會返回一個原由正則表達式的拷貝。此時不允許使用第二個參數添加修飾符
(1)
var regex = new RegExp('xyz', 'i');
// 等價於
var regex = /xyz/i;
(2)
var regex = new RegExp(/xyz/i);
// 等價於
var regex = /xyz/i;
  • ES6中改進了ES5(2)的這種情況。在第一個參數是正則對象(表達式),那麼允許使用第二個參數指定修飾符,如參數1帶了修飾符,則參數2覆蓋參數1修飾符。
new RegExp(/abc/ig, 'i').flags
// "i"

2.字符串的正則方法

ES6將String對象的這四個方法,在語言內部全部調用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]
  • 例子
console.log('abciaa'.split(/i/i))// Array ["abc", "aa"]
console.log('abcd1234'.search(/23/g)) //5
console.log('abcd\n1234'.replace(/23/i,'good'))
//"abcd 換行 1good4"
console.log('a12345bcd'.match(/123|cd/g))//Array ["1"]

JS RegExp 對象:https://www.runoob.com/jsref/jsref-obj-regexp.html

3.u修飾符

  • ES5不支持4字節的UTF-16編碼,會將大於\uFFFF識別爲兩個字符
  • ES6支持4字節的UTF-16編碼,對正則表達式添加了u修飾符,含義爲“Unicode 模式”,用來正確處理大於\uFFFF的 Unicode 字符。
u模式下,查找以\uD83D開頭的字符串,目標字符串爲一個字符=>false
/^\uD83D/u.test('\uD83D\uDC2A') // false
非u模式下,查找以\uD83D開頭的字符串,目標字符串爲\uD83D開頭的兩個字符=>true
/^\uD83D/.test('\uD83D\uDC2A') // true

(1)點字符

點(.)字符爲匹配除了換行和行結束符的單個字符。則對大於0xFFFF的Unicode字符需使用u修飾符才能識別

var s = '𠮷';

/^.$/.test(s) // false //非u則連續兩個字符
/^.$/u.test(s) // true

(2)Unicode字符表示法

ES6新增了使用{}表示Unicode字符,這種表示法在正則中必須加上u修飾符才能識別,否則會被解讀爲量詞

/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true

(3)量詞

使用u修飾符後,所有量詞都會正確識別碼點大於0xFFFF的Unicode字符

/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/𠮷{2}/.test('𠮷𠮷') // false
/𠮷{2}/u.test('𠮷𠮷') // true

(4)預定義模式

u修飾符也影響到預定義模式,能否正確識別碼點大於0xFFFF的 Unicode 字符。

\S是預定義模式,匹配所有非空白字符,加了u修飾符才能正確匹配大於0xFFFF的Unicode字符
/^\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

不加u修飾符,就無法識別非規範的K字符
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true

(6)轉義

沒有u修身情況正則的沒有定義的轉義無效,而u模式則報錯

/\,/ // /\,/
/\,/u // 報錯

4.RegExp.prototype.unicode 屬性

RegExp增加unicode屬性,表示是否設置了u修飾符

const r1 = /hello/;
const r2 = /hello/u;

r1.unicode // false
r2.unicode // true

5.y修飾符

ES6還爲正則表達式添加了y修飾符,叫做“粘連”(sticky)修飾符。
y修飾符的作用域g修飾符類似,也是全局匹配,後一次匹配都從上一次匹配成功的下一個位置開始。

g和y的不同之處在於,g的後一次匹配只要存在匹配即可繼續匹配,而y則是後一次匹配必須從剩餘位置的第一個位置開始。

  • g和y區別例子
ar s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;
第一次匹配,g和y無區別
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
第二次匹配依然存在a+表達式的條件,但剩餘字符串是_aa_a。此時y模式就不匹配(第一個字符是a)
r1.exec(s) // ["aa"]
r2.exec(s) // null

可以理解y模式,就是每一個匹配的開頭都隱含了量詞(^意思是匹配任何開頭爲匹配目標的字符串)

/b/y.exec('aba')
// null
上面的例子頭部加b同樣null
var s = 'baaa_aa_a';
var r2 = /a+/y;
r2.exec(s) // null

y修飾符的應用

  1. 從字符串提取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;
}

此時非法字符x在g模式下回忽略,而y模式下則不會,這樣容易找到錯誤

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

6.RegExp.prototype.sticky屬性

與y修飾符相匹配,ES6 的正則實例對象多了sticky屬性,表示是否設置了y修飾符。

var r = /hello\d/y;
r.sticky // true

7.RegExp.prototype.flags屬性

ES6 爲正則表達式新增了flags屬性,會返回正則表達式的修飾符。

// ES5 的 source 屬性
// 返回正則表達式的正文
/abc/ig.source
// "abc"

// ES6 的 flags 屬性
// 返回正則表達式的修飾符
/abc/ig.flags
// 'gi'

8.s修飾符:dotAll模式

正則表達式中,點(.)是查找除了換行和行結束符的單個字符。
但有時希望匹配的是任意單個字符,ES2018引入s修飾符,使得.可以匹配任意單個字符。

非s模式
/foo.bar/.test('foo\nbar')
// false
希望匹配任意單個字符的變通做法
/foo[^]bar/.test('foo\nbar')
// true
ES2018的s模式
/foo.bar/s.test('foo\nbar') // true

s模式也成爲dotAll模式,即點(dot)代表一切字符。

  • 對應的也有判斷是否處於dotall模式的屬性
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不衝突,同時使用時,.匹配所有字符,而^和$匹配每一行的行首和行尾

9.後行斷言

ES2018以前只支持先行斷言、先行否定斷言。
ES2018開始引入支持後行斷言、後行否定斷言。

  • 先行斷言
    x只在y前面才匹配。正則表達式正文格式/x(?=y)/
/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
  • 先行否定斷言
    x只不在y前面才匹配,正則表達式正文格式/x(?!y)/
/\d+(?!%)/.exec('that’s all 44 of them')                 // ["44"]
  • 後行斷言
    x只在y後面才匹配。正則表達式正文/(?<=y)x/
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]
  • 後行否定斷言
    x只不在y後面才匹配,正則表達式正文格式/(?<!y)x/
/(?<!\$)\d+/.exec('it’s is worth about €90')                // ["90"]

後行斷言的組匹配順序是相反的(從右到左)

組匹配,第一個括號是貪婪,第二個火狐捕獲一個字符
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]
組匹配,後行斷言從右到左,則第二個括號是貪婪,第一個括號只捕獲1個字符
/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]

JS貪婪與非貪婪:https://www.jianshu.com/p/7fa165acd51e
分組匹配:https://www.cnblogs.com/embrace-ly/p/10660266.html

後行斷言的反斜槓引用順序是相反的(從右到左)

/(?<=(o)d\1)r/.exec('hodor')  // null
先匹配到r,在匹配到od,匹配結果反斜槓引用取第一個匹配=>od中的o。
/(?<=\1d(o))r/.exec('hodor')  // ["r", "o"]

反斜槓引用(\1\2):https://blog.csdn.net/weixin_43639512/article/details/84785585

10.Unicode屬性類

ES2018引入的心的類寫法\p{…{}和\P{…},運行正則表達式匹配符合Unicode某種屬性的所有字符。(u模式下)

  • 例子
在u模式下使用\p{Script=Greek}匹配一個希臘字母
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

11.具名組匹配

ES2018以前的組匹配

正則表達式使用圓括號進行組匹配

  • 例子
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
  • 缺點
    每一組的匹配含義不容易看出來,而且只能用數字序號(比如matchObj[1])引用,要是組的順序變了,引用的時候就必須修改序號

ES2018的具名組匹配

允許爲每個組匹配指定一個名字,既便於閱讀代碼,又便於引用

  • 使用
    “具名組匹配”在圓括號內部,模式的頭部添加“問號 + 尖括號 + 組名”(?)。
    然後就可以在exec方法返回結果的groups屬性上引用該組名。同時,數字序號(matchObj[1])依然有效。
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
  • 沒有匹配到的屬性值爲undefined,但鍵名始終存在
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'
  • replace方法的第二個參數也可以是函數。
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
'2015-01-02'.replace(re, (
   matched, // 整個匹配結果 2015-01-02
   capture1, // 第一個組匹配 2015
   capture2, // 第二個組匹配 01
   capture3, // 第三個組匹配 02
   position, // 匹配開始的位置 0
   S, // 原字符串 2015-01-02
   groups // 具名組構成的一個對象 {year, month, day}
 ) => {
 let {day, month, year} = groups;
 return `${day}/${month}/${year}`;
});

具名組匹配在原來的基礎上,新增了最後一個函數參數:具名組構成的一個對象。函數內部可以直接對這個對象進行解構賦值。

引用

在正則表達式內部引用某個“具名組匹配”,可以使用\k<組名>的寫法

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

數字引用(\1)依然有效(\1等同於上面的k

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

可同時引用\1和內部引用

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

12.String.prototype.matchAll

  • 不使用matchAll取出多個匹配。返回的是數組
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"]
// ]
  • 使用matchAll取出多個匹配。返回的是遍歷器(Iterator),可以使用for…of循環取出。好處是遍歷器比較節省資源,當匹配結果是個很大的數組時。
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"]
  • 遍歷器轉爲數組
// 轉爲數組方法一
[...string.matchAll(regex)]

// 轉爲數組方法二
Array.from(string.matchAll(regex));
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章