Es6學習之正則表達式

1.y 修飾符 § 

除了u修飾符,ES6 還爲正則表達式添加了y修飾符,叫做“粘連”(sticky)修飾符。

y修飾符的作用與g修飾符類似,也是全局匹配,後一次匹配都從上一次匹配成功的下一個位置開始。不同之處在於,g修飾符只要剩餘位置中存在匹配就可,而y修飾符確保匹配必須從剩餘的第一個位置開始,這也就是“粘連”的涵義。

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

上面代碼有兩個正則表達式,一個使用g修飾符,另一個使用y修飾符。這兩個正則表達式各執行了兩次,第一次執行的時候,兩者行爲相同,剩餘字符串都是_aa_a。由於g修飾沒有位置要求,所以第二次執行會返回結果,而y修飾符要求匹配必須從頭部開始,所以返回null

如果改一下正則表達式,保證每次都能頭部匹配,y修飾符就會返回結果了。

var s = 'aaa_aa_a';
var r = /a+_/y;

r.exec(s) // ["aaa_"]
r.exec(s) // ["aa_"]

實際上,y修飾符號隱含了頭部匹配的標誌^

/b/y.exec('aba')
// null

上面代碼由於不能保證頭部匹配,所以返回nully修飾符的設計本意,就是讓頭部匹配的標誌^在全局匹配中都有效。

下面是字符串對象的replace方法的例子。

const REGEX = /a/gy;
'aaxa'.replace(REGEX, '-') // '--xa'

上面代碼中,最後一個a因爲不是出現在下一次匹配的頭部,所以不會被替換。

單單一個y修飾符對match方法,只能返回第一個匹配,必須與g修飾符聯用,才能返回所有匹配。

'a1a2a3'.match(/a\d/y) // ["a1"]
'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]

y修飾符的一個應用,是從字符串提取 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;
}

上面代碼中,如果字符串裏面沒有非法字符,y修飾符與g修飾符的提取結果是一樣的。但是,一旦出現非法字符,兩者的行爲就不一樣了。

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

上面代碼中,g修飾符會忽略非法字符,而y修飾符不會,這樣就很容易發現錯誤。

2.s 修飾符:dotAll 模式

/foo.bar/.test('foo\nbar')
// false

上面代碼中,因爲.不匹配\n,所以正則表達式返回false

但是,很多時候我們希望匹配的是任意單個字符,這時有一種變通的寫法。

/foo[^]bar/.test('foo\nbar')
// true

這種解決方案畢竟不太符合直覺,ES2018 引入s修飾符,使得.可以匹配任意單個字符。

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

3.具名組匹配

使用exec方法,就可以將這三組匹配結果提取出來。

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 引入了具名組匹配(Named Capture Groups),允許爲每一個組匹配指定一個名字,既便於閱讀代碼,又便於引用。

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

4.解構賦值和替換

有了具名組匹配以後,可以使用解構賦值直接從匹配結果上爲變量賦值。

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方法的第二個參數是一個字符串,而不是正則表達式。

replace方法的第二個參數也可以是函數,該函數的參數序列如下。

'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}`;
});

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

5.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"]
// ]

上面代碼中,while循環取出每一輪的正則匹配,一共三輪。

目前有一個提案,增加了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"]

上面代碼中,由於string.matchAll(regex)返回的是遍歷器,所以可以用for...of循環取出。相對於返回數組,返回遍歷器的好處在於,如果匹配結果是一個很大的數組,那麼遍歷器比較節省資源。

遍歷器轉爲數組是非常簡單的,使用...運算符和Array.from方法就可以了。

// 轉爲數組方法一
[...string.matchAll(regex)]

// 轉爲數組方法二
Array.from(string.matchAll(regex));

實踐:

全局匹配:如果不加標誌g,會出現死循環

​
let reg = new RegExp(this.state.regex, "g");
while ((result = reg.exec(prevState.regexString))) {
        results = [...results, result + "\n"];
      }

​

學習:阮一峯 正則表達式 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章