JavaScript 正則入門到掌握

taokexia發表於2019-02-23

最近學習了 AST 抽象語法樹,在程式碼解析的過程中廣泛使用了正規表示式,由此認識到自己在正則基礎方面的薄弱,雖然清楚每個符號所表示的含義,但是當一大串正則符號出現在自己面前時,還是會懵逼一會,無法融匯貫通的掌握正則。

下面是自己整理的正則基礎知識和收集到的一些實戰訓練。希望通過訓練和筆記來加深自己對正規表示式的掌握!

基礎

正規表示式(Regular Expression,在程式碼中常簡寫為regex、regexp或RE)使用單個字串來描述、匹配一系列符合某個句法規則的字串搜尋模式。

搜尋模式可用於文字搜尋和文字替換。

下面是正則的基礎語法,只要熟悉就行,重要還是結合實際多運用正則,才能真正的掌握正則。

建立正規表示式

方式1:

// 語法格式為: /正規表示式主體/修飾符(可選)
// 案例
var patt = /var/i
// var 是正規表示式主體
// i 是修飾符
複製程式碼

方式2:

// 建立 RegExp 物件
// new RegExp(正規表示式主體[, 修飾符])
// RegExp(正規表示式主體[, 修飾符])
var patt = new RegExp('var', 'i')
複製程式碼

注意: /var/i 創鍵的正規表示式物件和 new RegExp(var, i) 建立的物件並不相等。雖然它們匹配規則一樣。

指定匹配模式的修飾符有:

修飾符 描述
i 執行對大小寫不敏感的匹配。
g 執行全域性匹配(查詢所有匹配而非在找到第一個匹配後停止)。
m 執行多行匹配。
s 允許 . 去匹配多行
u Unicode; 將模式視為Unicode序列點的序列Unicode;
y 粘性匹配; 僅匹配目標字串中此正規表示式的lastIndex屬性指示的索引(並且不嘗試從任何後續的索引匹配)。

字元類別(Character Classes)

元字元是正規表示式規定的一個特殊程式碼

程式碼 說明
. 匹配除換行符以外的任意字元。注意:/[.]/中匹配的是符號點.
\w 匹配字母或數字或下劃線或漢字 [A-Za-z0-9_]
\s 匹配任意的空白符 [\f\n\r\t\v​\u00a0]
\d 匹配數字 [0-9]
\ 用於轉義

邊界(Boundaries)

程式碼 說明
\b 匹配單詞的開始或結束 例如:/\bno/ 匹配 "at noon" 中的 "no"
^ 匹配字串的開始
$ 匹配字串的結束

分組(Grouping)與反向引用(back references)

字元 含義
(x) 捕獲括號。匹配 x 並且捕獲匹配項。 /(foo)/ 匹配且捕獲 "foo bar." 中的 "foo"。被匹配的子字串可以通過 元素[n] 中找到,或 RegExp 物件的屬性 $n 中找到。會消耗效能,如果需要捕獲的陣列,可以使用非捕獲括號
\n 反向引用(back reference),指向正規表示式中第 n 個括號(從左開始數)中匹配的子字串。/apple(,)\sorange\1/ 匹配 "apple, orange, cherry, peach." 中的 "apple,orange,"(最後的,號)
(?:x) 非捕獲括號(non-capturing parentheses)。匹配 x 不會捕獲匹配項。匹配項不能夠從結果再次訪問。(推薦)

重複

程式碼/語法 說明
* 重複零次或更多次
+ 重複一次或更多次
? 重複零次或一次
x*?
x+?
最小可能匹配,非貪婪模式。/".*?"/ 匹配 "foo" "bar" 中的 "foo"
{n} 重複n次
{n,m} 重複n到m次

斷言(Assertions)

程式碼/語法 說明
x(?=y) 只有當 x 後面緊跟著 y 時,才匹配 x。/Jack(?=Sprat)/ 只有在 'Jack' 後面緊跟著 'Sprat' 時才匹配
x(?!y) 只有當 x 後面不是緊跟著 y 時,才匹配 x。

例項屬性

  • RegExp.prototype.global 是否開啟全域性匹配
  • RegExp.prototype.ignoreCase 是否要忽略字元的大小寫。
  • RegExp.prototype.lastIndex 下次匹配開始的字串索引位置。
  • RegExp.prototype.multiline 是否開啟多行模式匹配(影響 ^ 和 $ 的行為)。
  • RegExp.prototype.source 正則物件的源模式文字。
  • RegExp.prototype.sticky 是否開啟粘滯匹配。

例項方法

  • RegExp.prototype.exec() 在目標字串中執行一次正則匹配操作。
  • RegExp.prototype.test() 測試當前正則是否能匹配目標字串。
  • String.prototype.match(pattern) 根據pattern對字串進行正則匹配,返回匹配結果陣列,如匹配不到返回null
  • String.prototype.replace(pattern, replacement) 根據pattern進行正則匹配,把匹配結果替換為replacement, 返回一個新的字串

實戰

改變文字結構

var re = /(\w+)\s(\w+)/
// \w 匹配 [A-Za-z0-9_]
// + 重複一次或更多次
// \s 匹配 空格符
// () 捕獲括號
var str = "John Smith"
var newstr = str.replace(re, "$2, $1")
// $1: 捕獲的值為 John 
// $2: 捕獲的值為 Smith
console.log(newstr) //  轉換後為 "Smith, John"
複製程式碼

訓練1: 請通過正規表示式,把 () => 1+1 修改為 function () { return 1+1 }

多行匹配

var s = "Please yes\nmake my day!";
s.match(/yes.*day/);
// . 符號預設不匹配 \n
// 可以改為 /yes.*day/s ,使用 s 識別符號來匹配多行
// 返回 null

s.match(/yes[^]*day/);
// ^ 匹配字串的開始
// [^]* 表示匹配任何字元,包括換行符
// 返回 yes\nmake my day
複製程式碼

訓練2: 請通過正規表示式,獲取到 ab a\nb ab 中的 a\nb 字串

使用帶有 ”sticky“ 標誌的正規表示式

var text = "First line\nsecond line";
var regex = /(\S+) line\n?/y;
// () 捕獲括號
// \S+ 匹配一次或更多次非空白符
// \n? 匹配零到一次換行符
var match = regex.exec(text);
console.log(match[1]);  // 匹配到 First
// 輸入 match[0] 返回的是 First line\n 所要進行匹配的文字
console.log(regex.lastIndex); // 匹配到位置 11
var match2 = regex.exec(text);
console.log(match2[1]); // 匹配到 Second
console.log(regex.lastIndex); // 匹配到位置 22

var match3 = regex.exec(text);
console.log(match3 === null); // 返回 true, 沒有匹配到結果
複製程式碼

訓練3: 有文字"price: 10¥\nprice: 20¥\nprice: 30¥", 請獲取到 price 的值,儲存到陣列中,結果輸出為 ['10', '20', '30']

匹配連結

提取連結傳遞引數的值

// 提取 str 的 name 和 age
var str = 'https://www.baidu.com?name=jawil&age=23'
// 獲取name的值
var regName = /\?name=([^&]*)/
// \? 表示匹配 ? 字元,從該字元開始匹配
// name= 表示匹配 name= 的字串
// [^&]* 表示匹配字元直到遇到 & 符號為止
// ([^&]*) 表示捕獲匹配到的結果
// match 的值為 ["?name=jawil", "jawil"]
var match = regName.exec(str)
console.log(match[1]) // 返回 jawil
複製程式碼

訓練4: 請修改正規表示式,獲得到 age 的屬性值, 該正規表示式是怎麼樣的?

數字格式化

把 1234567890 格式化為 1,234,567,890

var str = '1234567890'
var reg = /\B(?=(\d{3})+(?!\d))/g
// \b 匹配單詞的開始或結束
// \B 匹配單詞的非開始和非結束的位置
//     \Bon 匹配 ' on' 為 null,即開頭不能為 on
//     \Bon 匹配 ' onon' 為 on,匹配後一個 on
// (\d{3}+) 捕獲1個或多個的3個連續數字
// (?!\d) 斷言: 表示3個數字不允許後面跟著數字
// (\d{3}+)+(?!\d) 表示匹配的後邊界跟著 3*n(n>=1)的數字.
// (?=) 斷言: 表示後邊必須跟著的內容
//     \B(?=(\d{3}+)+(?!\d)) 匹配非邊界後跟著的3個數字

// 執行過程
// 第一次匹配到為 ["", "890"] 沒有匹配到文字,但捕獲到3個數字
// 替換後為 1234567,890
// 第二次匹配到為 ["", "567"]
// 替換後為 1234,567,890
// 第三次匹配到為 ["", "234"]
// 替換後為 1,234,567,890
// 最後一次執行結果為 null 替換結束
var format = str.replace(reg, ',')
console.log(format) // 1,234,567,890
複製程式碼

訓練5: 通過正規表示式把字串'255255255255' 格式化為 ip地址格式 '255.255.255.255'

訓練6: 編寫正規表示式測試傳入的ip地址是否正確,例如傳入 1.1.1.1 返回 true, 傳入 1.1.1.257 返回 false

零寬斷言

如果不想匹配符號,只匹配一個位置,就要用到"零寬斷言"。用於幫我們匹配文字,而它自身不會被匹配到內容中去。

var str = 'ttabttdef'
var reg = /tt(?=ab)/
reg.exec(str)
// 匹配到的是 tt, 只有後面跟的是 ab 的才匹配
var reg1 = /tt(?=ab)ab/
reg1.exec(str)
// 匹配到的是 ttab, 斷言只匹配位置, 所以斷言匹配的這些字元會接下去進行匹配。
// 而不會因為被斷言匹配過了就跳過這些字元。
複製程式碼

訓練7: 使用斷言匹配出字串 <div>red blue green</div> 中的字串 red blue green

訓練8: 使用斷言,匹配出<xxx>任意字串</xxx>中的 任意字串, 其中 xxx 表示標籤名, 比如匹配出<div>adsasdfa<br>asdfsd</div>中的 adsasdfa<br>asdfsd

虛擬 DOM 標籤解析

在Vue原始碼中構建虛擬DOM, 首先把 template 編譯成AST語法樹, 再轉換為 render 函式 最終返回一個VNode(VNode就是Vue的虛擬DOM節點)

而要編譯 template, 首先就是要對 template 進行解析。解析的過程中使用到了正規表示式, 比如解析下面程式碼

var dom = `<div :class="c" class="demo" v-if="isShow">{{item}}</div>`

// 匹配變數名
const variable = /[a-zA-Z_][\w\-\.]*/
// 匹配標籤名
const tagName = /^<([a-zA-Z_][\w\-\.]*)/
console.log(tagName.exec(dom)[1])// 返回 div
// 匹配屬性
const attr = /\s*([^\s'"<>/=]+)(?:=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+))/g
// \s*([^\s'"<>/=]+) 匹配屬性名
// (?:=) 匹配賦值號 = 
// (?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)) 匹配屬性值,包括 ""、 '' 和其它形式
var res = attr.exec(dom)
console.log(res[1]) // 返回 :class
console.log(res[2]) // 返回 "c"
// 匹配 {{}} 裡的內容
const val = /{{(.*)}}/ 
console.log(val.exec(dom))// 返回 item
複製程式碼

答案

訓練1:

var str = '() => 1+1'
var reg = /(\(\))\s(=>)\s([^]*)/
var str1 = str.replace(reg, 'function $1 { return $3 }')
複製程式碼

訓練2:

var s = "ab a\nb ab"
s.match(/a\nb/)
複製程式碼

訓練3:

var str = 'price: 10¥\nprice: 20¥\nprice: 30¥'
var reg = /\D*(\d+)/y
var a = []
while((match = reg.exec(str)) !== null) {
  a.push(match[1])
}
console.log(a)
複製程式碼

訓練4:

var str = 'https://www.baidu.com?name=jawil&age=23'
var regAge = /\&age=([^&]*)/
var match = regAge.exec(str)
複製程式碼

訓練5:

var pattern = /\B(?=(\d{3})+(?!\d))/g
var str = '255255255255'
str.replace(pattern, '.')
複製程式碼

訓練6:

  • 首先要匹配的是 0 ~ 255
  • 匹配 50 ~ 55: 5[0-5]{1}
  • 匹配 0 ~ 49: [0-4]\d{1}
  • 匹配 200 ~ 255: 2(5[0-5]{1}|[0-4]\d{1})
  • 匹配 0 ~ 199: [0-1]?\d{1,2}
  • 完整匹配 0 ~ 255:
    • (2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})

組合成檢測的ip 的正規表示式

var reg1 = /(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})/
// 把後邊3段檢測程式碼進行簡化後
var reg2 = /(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})(\.(2(5[0-5]{1}|[0-4]\d{1})|[0-1]?\d{1,2})){3}/
複製程式碼

訓練7:

var str="<div>red blue green</div>"
var reg = /<div>(.*)(?=<)/
reg.exec(str)[1] // red blue green
複製程式碼

訓練8:

var str = '<div>asdas<br>dfadfsdasdf</div>'
var reg = /^<(\w+)>(.*)(?=<\/\1>)/
reg3.exec(str)[2] // asdas<br>dfadfsdasdf
複製程式碼

參考資料

相關文章