關於正則位置匹配(斷言)的技巧

木魚木心發表於2018-07-25

正則位置匹配

先了解下以下幾個概念

  • 零寬:只匹配位置,在匹配過程中,不佔用字元,所以被稱為零寬

  • 先行:正則引擎在掃描字元的時候,從左往右掃描,匹配掃描指標未掃描過的字元,先於指標,故稱先行

  • 後行:匹配指標已掃描過的字元,後於指標到達該字元,故稱後行,即產生回溯

  • 正向:即匹配括號中的表示式

  • 負向:不匹配括號中的表示式

es5 就支援了先行斷言

es2018 才支援後行斷言

零寬正向先行斷言,又稱正向向前查詢(positive lookhead)

注意: .在正則裡面代表匹配除換行符,回車符等少數空白字元之外的任何字元,匹配其時需要轉義

(?=pattern):某位置後面緊接著的字元序列要匹配 pattern

例:

`sinM.`.match(/sin(?=M\.)/g); // ["sin"]
`M.sin`.match(/sin(?=M\.)/g); // null
複製程式碼

第一個 sin 會匹配,因為他後面有 pattern

零寬負向先行斷言,又稱負向向前查詢(negative lookhead)

(?!pattern):某位置後面緊接著的字元序列不能匹配 pattern

例:

`M.sin`.match(/sin(?!M\.)/g); // ["sin"]
`sinM.`.match(/sin(?!M\.)/g); // null
複製程式碼

第一個 sin 會匹配,因為他後面沒有 pattern

零寬正向後行斷言,又稱正向向後查詢(positive lookbehind)

(?<=pattern):某位置前面緊接著的字元序列要匹配 pattern

例:

'sinM.'.match(/(?<=M\.)sin/g); // null
'M.sin'.match(/(?<=M\.)sin/g); // ["sin"]
複製程式碼

第二個 sin 會匹配,因為它前面有 pattern

零寬負向後行斷言,又稱負向向後查詢(negative lookbehind)

(?<!pattern):某位置前面緊接著的字元序列不能匹配 pattern

例:

'sinM.'.match(/(?<!M\.)sin/g); // ["sin"]
'M.sin'.match(/(?<!M\.)sin/g); // null
複製程式碼

第一個 sin 會匹配,因為它前面沒有 pattern


來看個實際的例子,把4+6*sqrt(5)*Math.sqrt(5)轉換成可以通過eval或者new Function()獲得實際結果的字串

這個可以使用負向後行斷言,即替換前面不緊接 Math.的 sqrt 字串序列

let s = `4+6*sqrt(5)*Math.sqrt(5)`.replace(/(?<!Math\.)sqrt/g, func => `Math.${func}`);
eval(s); // 34
複製程式碼

第二個例子: 匹配 url 後面的路徑

'https://www.google.com/v3/api/getUser?user=panghu'.match(/(?<=\.\w*(?=\/)).*/);
複製程式碼

第三個例子:替換字串中 img 標籤的 width 為 100%

'<img id = "23" style="width:999x;"/><img id = "23" style="width:999x;"/>'.replace(
  /(?<=(<img[\s\S]*width:\s*))[^("\/);]*/gm,
  '100%'
);
複製程式碼

匹配 sin

'M.sin'.match(/(?<=M\.)sin/g); // ["sin"]
`M.sin`.match(/sin(?!M\.)/g); // ["sin"]
複製程式碼

這兩種方法都可以實現同樣的效果,但我個人更喜歡使用第一種方法,它的寫法更符合人的直接思維習慣

在全域性匹配修飾符 g 作用下正則 test 方法出現的“怪異”結果

先看下面兩行程式碼的執行結果

let reg = /js/g;
reg.test('js'); //before: lastIndex:0, after: lastIndex:2
reg.test('js'); //before: lastIndex:2, after: lastIndex:0
reg.test('js'); //before: lastIndex:0, after: lastIndex:2
複製程式碼

如果你的答案是三個 true 的話,那就錯了 答案其實是 true、false、true,這就是所謂的怪異現象

為什麼?答: RegExp 物件有個 lastIndex 屬性,它的初始值是 0, 當不使用 g 修飾符修飾時,每次執行 test 方法之後它都會自動置 0 而使用 g 修飾符時,每次執行 test 方法的時候,它都是從索引值為 lastIndex 的位置開始匹配,lastIndex 為匹配到的字元序列下一個索引值。只有當匹配失敗以後才會將 lastIndex 置為 0

例:上述例子中的第一個 test 方法執行之前,lastIndex 值為 0,執行之後 lastIndex 值為 2,於是當第二次執行 test 方法時,從字串索引值為 2 處開始匹配,顯然會匹配失敗,所以第三次匹配時又會匹配成功

匹配含 class 為 root 的標籤(不考慮特殊情況), 如<div class="root">

這裡可以涉及到的知識點有:貪婪/非貪婪匹配模式匹配回溯及其消除分組反向引用

基礎版:只匹配雙引號包裹的 class

`<div class="root"><span class="root"></span><i class='root'></i></div>`.match(/<[^>]*class="root".*?>/g);
// ["<div class="root">", "<span class="root">"]
複製程式碼

模式匹配[^>]表示匹配除[^]裡面的所有字元,這裡就是匹配除>外的所有字元 注意前後都需要非貪婪匹配符號?否則只有前面的,它會貪婪的吃掉 div;只有後面的,它會貪婪的吃掉 span

完整版:單雙引號包裹的 class 都可以匹配

`<div class="root"><span class="root"></span><i class='root'></i></div>`.match(/<[^>]*class=("root"|'root').*?>/g);
// ["<div class="root">", "<span class="root">", "<i class='root'>"]
複製程式碼

這裡如果不使用[^>]而使用.*就會出現下面這種匹配結果,不是我們想要的

["<div class="root">", "<span class="root">", "</span><i class='root'>"]

進階版:使用分組引用消除難看的("root"|'root'),再消除.*?回溯

`<div class="root"><span class="root"></span><i class='root'></i></div>`.match(/<[^>]*class=("|')root\1[^>]*>/g);
// ["<div class="root">", "<span class="root">", "<i class='root'>"]
複製程式碼

\1表示引用前面的第一個分組結果,即("|')的匹配結果,這樣就能保證單引號配對單引號,雙引號匹配雙引號

[^>]*代替.*?可以消除使用*?引發的回溯,因為*是儘可能多的匹配,而?是儘可能少的匹配

回顧開頭,我所說的特殊情況就是標籤的屬性值不能含有>,因為為了消除回溯使用的[^>]含有字元>,這部分其實可以使用其他正則代替,讓它在消除回溯的情況下可以匹配特殊情況

如果大家對匹配含 class 為 root 的標籤這部分涉及的知識點感興趣,可以在底下評論,我到時候再仔細講

如果你喜歡這篇文章的話,麻煩點個⭐原文地址資瓷下

參考:

JavaScript 權威指南(第 6 版)

Javascript 正規表示式迷你書

以上如有錯誤,歡迎指正

相關文章