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

正則位置匹配

先了解下如下幾個概念

  • 零寬:只匹配位置,在匹配過程當中,不佔用字符,因此被稱爲零寬git

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

  • 後行:匹配指針已掃描過的字符,後於指針到達該字符,故稱後行,即產生回溯正則表達式

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

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

es5 就支持了先行斷言google

es2018 才支持後行斷言url

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

注意: .在正則裏面表明匹配除換行符,回車符等少數空白字符以外的任何字符,匹配其時須要轉義

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

例:spa

`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 正則表達式迷你書

以上若有錯誤,歡迎指正

相關文章
相關標籤/搜索