面試官!讓我們聊聊正則

ngaiwe發表於2018-08-02

簡述

正規表示式是處理字串的利器,並提高工作效率,一個好的正則能夠幫我們省去幾十甚至上百行程式碼。在工作中,也許你會見到在程式碼中出現很多正則處理字串,也可能見到程式碼中毫無正則,原因在於會正則的人往往處理字串首先想到用正則去處理,不會的那必然用很多API處理。並且在面試的時候很多同學往往掛在正則這裡,所以對於前端正則是一個必備技能。這篇文章不光能教會你如何使用它,還能讓你懂得其中的原理,在以後使用和麵試中將做到毫無懼怕。

一.基礎

要想玩轉正則首先要明白三大元素:修飾符 元字元 量詞
例:var reg = /^\d$/g 這是一個簡單點正則,下面我們來對三大元素一探究竟

1.修飾符

  • g - global 全域性匹配(只要有匹配成功的結果一直匹配直到沒有為止)
  • i - ignoreCase 忽略大小寫
  • m - multiline 多行匹配

2.元字元

元字元其中分為特殊元字元和普通元字元
普通元字元就是列入陣列,字母等元素
常用特殊元字元如下:

  • \ 轉義字元(把一個普通字元轉變為有特殊意義的字元,或者把一個有意義的字元轉變為普通字元)
  • . 除了\n(換行符)以外的任意字元
  • \d 匹配一個0~9之間的數字
  • \D 匹配一個非0~9之間的數字(大寫與小寫字母組合正好是相反含義
  • \w 匹配一個0~9或字母或_之間的一個字元
  • \s 匹配一個任意空白字元
  • \b 匹配一個邊界符
  • x|y 匹配x或者y中的一個
  • [a-z] 匹配a-z中的任意一個字元
  • [^a-z] 匹配非a-z中的任意一個字元
  • [xyz] 匹配x或y或z中的一個字元
  • [^xyz] 和上方相反
  • () 整的小分組,匹配一個小分組(可以理解為大正則中的一個小正則)
  • ^ 以某一個元字元開始
  • $ 以某一個元字元結束
  • ?: 只匹配不捕獲
  • ?= 正向肯定預查
  • ?! 正向否定預查

3.量詞

量詞主要的作用是用來描述元字元出現的次數 如下:

  • + 讓前面的元字元出現一到多次
  • ? 出現零到一次
  • * 出現零到多次
  • {n} 出現n次
  • {n,} 出現n到多次
  • {n,m} 出現n到m次

看到這裡大家一定有些暈,這些稀奇古怪的符號,到底如何運用,下面將帶大家去運用這些字元,在運用之前同學們要做到牢記 牢記 牢記這些字元,想要玩轉正則必須要的就是牢記他們,當你牢記這些字元的同時說明你已經入門了。

二.元字元

  • 1.開始元字元 ^

    匹配啟示位置的元字元,例:var reg = /^2/; 表示開頭必須是2,如果開始元字元放在[]裡面 例:[^]表示非中括號種情況 相反含義

  • 2.結束元字元 $

    匹配結束位置的元字元,例: var reg = /2$/; 表示結尾必須是2,如果是var reg = /^2$/; 表示只能為2,因為2只代表一個元字元。

  • 3.轉義字元 \

    將特殊元字元轉換為普通字元,例:var reg = /^2.3$/ 正常理解為匹配啟示為2 結束為3中間是.的正則,但是在這裡面.屬於特殊元字元,意義是除了\n(換行符)以外的任意字元,所以不論是2.3/2+3/2s.3等等,只要是不·\n都匹配成功,所以為了這種需求就用到了轉義字元\ 如下: var reg = /^2\.3$/ 將特殊元字元.轉義為真正的.元素,再次匹配只有2.3才能匹配成功 如下思考:

    var reg1 = /^\d$/
    var reg2 = /^\\d$/
    var reg3 = /^\\\d$/
    var reg4 = /^\\\\d$/
    複製程式碼

    首先reg1 表示0-9之間的數字 所以0-9都能匹配成功
    reg2出現兩個\ 不論是0-9 d \d都匹配失敗,只有\\d才匹配成功,原因在於第一個轉義字元把第二個\轉義為普通\,此時第一個\也表示普通字元,所以只有匹配到\\d才生效
    reg3出現三個\,此時大家一定會認為\\\d才是正確答案,非也,此時的正確答案是\\[0-9]意思是\和0-9中任意一個數字,原因在於第一個\將第二個轉義,所以變為\\ \d分為了兩部分,\\為普通字元,而\d表示一個0~9之間的數字,所以正確答案是\\[0-9]
    reg4出現四個\ 很多同學會認為正確答案一定是\\\[0-9], 很可惜,正確的是\\\\d,原因是當第一個把第二個轉義為普通字元時,第三個又把第四個轉義為普通字元,所以最後匹配為\\\\d
    看到這裡相信有一部分小夥伴已經暈了,雖然在工作我們不會玩這種操作,但是要深刻理解轉義字元,在正則中如果你直接寫 var reg = /^$/會報錯,因為正則中不能單獨一個\出現,因為他是一個特殊元字元,需要寫至少兩個\, 例如: var reg = /^\\$/ 正確的匹配結果為'\\',所以牢記兩個\代表真正的\,這裡搞明白的同學,相信轉義字元已經完全掌握。

  • 4.或者元字元 x|y

    這個元字元很好理解就是匹配x或者匹配y,沒有什麼難度,舉個小例子:

    var reg = /2|3/
    // 匹配數字為2或者為3
    複製程式碼
  • 5.小分組 ()

    在上面同學們會發現用了()這樣的元字元,他主要的特點就是將括號內的正則規則看成一個整體,相當於一個小的獨立的正則,主要處理如下例子中的問題

    var reg = /^18|19$/
    
    // 這個例子很多同學能夠了解是18或者19開頭 但是結尾呢?真的只匹配19麼? 其實並不是
    // 正確的匹配除了18 19 還有181、189、819 這時候小分組就起到了作用如下
    
    var reg = /^(18|19)$/
    
    // 這裡將18或19用()包裹起來,起到小分組的作用
    // 這樣這個正則只匹配18開頭結尾或者19而不是181和189
    複製程式碼
  • 6.分組引用 \n

    分組引用的概念就是可以引用大正則中指定的小正則規則,例如:

    var reg = /^([a-z])([a-z])\2([a-z])$/
    // 符合的字串:book week http ...​
    複製程式碼

    具體上面的例子理解為\2代表著第二個小正則規則的完全引用,就是和第二個小正則([a-z])一樣,可以減少正則的複雜度和處理多次重複規則

  • 7.匹配字元 []

    [xyz][^xyz][a-z][^a-z]下面舉個小例子,便於同學們理解

    var reg = /^[a-zA_Z0-9_]$/
    // 這個正則和等價於\w 就是匹配一個0~9或字母或_之間的一個字元
    // 而正則[xyz]中的xyz分別代表a-z、A_Z、0-9,xyz只是一個代表標識,可以有xyzhw各種組合
    // 就像這個例子中有下劃線_一樣四個的匹配
    // 有一個重點補充,在[]中的特殊元字元一般都代表本身含義,如下
    var reg = /^[.?+&]$/
    // 代表著匹配. .? ?+ ...等等
    複製程式碼
  • 8.邊界符 \b

    匹配一個單詞邊界,也就是指單詞和空格間的位置(邊界主要是單詞的左右兩邊) 例如:

    var reg = /er\b/
    // 可以匹配never中的er,但是不能匹配verb中的er
    var reg = /\b\w+\b/g
    // 能匹配字母數字和下劃線與單詞邊界 'my blog is www.ngaiwe.com'
    // 能匹配 'my'、'blog'、'is'、'www'、'ngaiwe'、'com'
    複製程式碼
  • 9.只匹配不捕獲 ?:

    下面舉一個例子,但是涉及到捕獲內容,如果同學們不太明白,可以先跳過此處,看完下面捕獲,再返回來看

    var reg = /^(\d{6})(\d{4})(\d{2})(\d{2})\d{2}(\d)(\d|X)$/g
    var str = '110105199001220613'
    console.log(reg.exec(str))
    // 列印結果為 "110105199001220613", "110105", "1990", "01", "22", "1", "3"
    
    var reg = /^(\d{6})(?:\d{4})(\d{2})(\d{2})\d{2}(\d)(\d|X)$/g
    // 列印結果為 "110105199001220613", "110105", "01", "22", "1", "3"
    // 會將第二個小分組只匹配,不捕獲
    複製程式碼
  • 10.正向肯定預查 ?=

    這個概念比較難理解,用於就是為了匹配一個元素後面是的元素是否符合相應規則,但是並不消耗這個規則,例子1:

    var reg = /windows(?=95|98|NT|2000)/
    var str1 = 'windows2000'
    var str2 = 'windowsxp'
    
    console.log(reg.test(str1))
    console.log(reg.test(str2))
    // str1 為true str2 為false
    
    console.log(reg.exec(str1))
    console.log(reg.exec(str2))
    // 能捕獲到str1 並且捕獲結果時windows 並沒有將2000也同時捕獲
    // 說明正向預查只負責匹配相應規則
    複製程式碼

    例子2:

    var reg1 = /win(?=d)dows/
    var reg2 = /win(d)dows/
    var str = 'windows'
    console.log(reg1.test(str))
    console.log(reg2.test(str))
    // reg1 返回true reg2返回 false
    // 原因是正向預查只負責匹配,不消耗字元,也就是並不會匹配為裡面規則的字元
    // reg1 相當於匹配windows並且符合win後面第一個出現的是d
    // reg2 相當於匹配winddows
    複製程式碼
  • 11.正想否定預查 ?!

    和正向肯定預查相反,匹配不符合規則的正則

三.正則的兩種建立方式

正則的建立方式分為兩種,一種是字面量建立,另一種是建構函式建立

  • 1.字面量建立

var reg = /\d+/img

  • 2.建構函式建立

var reg = new RegExp('\\d+', 'img') 第一個引數是元字元並且\d這種特殊字元在這裡面是普通字元,所以需要用\轉義為特殊字元,第二個引數是修飾符

這兩種的用途有區別,一般需要動態建立正則元字元采用建構函式建立,因為裡面元字元是字串拼接,常規固定寫死正則採用字面量建立,例子如下:

var stringMode = 'string'
var reg = new RegExp(`^\\[object ${stringMode}\\]$`)
console.log(reg.toString())
複製程式碼

四.正則原型方法

首相上一張圖讓同學們看一下正則一共有哪些方法,如下:

正則原型物件方法

可以看到正則原型物件上一共就三個方法,exec testtoString

  • 1.exec該方法主要應用於捕獲組而設計,實參是要匹配的字串。如圖所示:

    正則原型物件方法

    捕獲原理

    • 1.在捕獲的時候先驗證當前字串和正則是否匹配,不匹配返回null(沒有捕獲到任何內容)
    • 2.如果匹配從字串最左邊開始,向右查詢到匹配內容,並把匹配的內容返回

    捕獲結果

    • 1.結果是一個陣列
    • 2.第一項0 是當前本次大正則中匹配的結果
    • 3.index是匹配到的結果在字串中的索引位置
    • 4.input當前正則操作的原始字串
    • 5.如果大正則中有分組(),獲取的陣列中從第二項開始都是每個小分組的捕獲結果
    • 下面舉一個身份證正則的例子 便於參考,具體裡面匹配規則下面會單獨介紹,這裡只學習欄位意義

    正則exec方法

  • 2.懶惰性

    正則捕獲存在懶惰性,在上面exec中,執行一次exec只捕獲到第一個符合規則的內容,第二次執行exec也是捕獲到第一個內容,後面的內容不論執行多少次都無法捕獲到

    解決方法

    在正則的末尾加修飾符g(全域性匹配)

    原理

    如圖所示,正則本身有一個屬性 lastIndex 下一次正則在字串中匹配查詢的開始索引
    預設值是0,從字串第一個位置開始查詢,由此可見,當執行完exec後lastIndex並沒有變,並且就算手動修改lastIndex也不會起作用,瀏覽器不識別

    正則exec方法
    正則exec方法

    加上了修飾符g就解決了這個問題,因為每一次exec之後,瀏覽器預設會修改lastIndex,下一次從上一次結束的位置開始查詢,所以可以得到後面內容

    正則exec方法
    正則exec方法

    我們來手動建立一個函式來實現匹配到全部內容並且都捕獲到,如下:

    var reg = /^(\d{6})(\d{4})(\d{2})(\d{2})\d{2}(\d)(\d|X)$/g
    var str = '110105199001220613'
    
    RegExp.prototype.myExec = function myExec() {
    	var str = arguments[0] || ''
    	var result = []
    	// 首先this指向的是RegExp,所以判斷this是否加了全域性修飾符g
    	// 如果沒有,防止執行死迴圈,我們只執行一次exec並將其返回即可
    	
    	if(!this.global) {
    		return this.exec(str)
    	}
    	
    	var arrs = this.exec(str)
    	while(arrs) {
    		result.push(arrs[0])
    		// 此時lastIndex的值已經變為上一次的結尾
    		arrs = this.exec(str)
    	}
    	return result
    }
    複製程式碼

    這個方法當正則reg加了修飾符g 則返回大正則匹配到結果,如果沒加g則返回exec捕獲結果

  • 3.test 該方法主要應用於正則匹配,當然也可以用在捕獲上面

    從字串左側開始匹配,匹配到符合正則規則的字元,返回true,否則返回false,同樣的test在修飾符g下也會修改lastIndex的值

    運用test實現捕獲

    var reg = /\{([a-z]+)\}/g
    var str = 'my name is {weiran}. I am from {china}'
    var result = []
    while (reg.test(str)) {
    	result.push(RegExp.$1)
    }
    console.log(result)
    // ['weiran', 'china']
    複製程式碼

    當test匹配到結尾或者匹配不到時,返回false,成功則向陣列新增當前小分組匹配第一個元素內容 在RegExp的constructor中存在$1-$9,他們的具體指的是當前本次匹配小分組第一到第九捕獲的內容

  • 4.toString

    就是將正規表示式轉化為字串

五.字串正則方法

  • 1.match

    同樣是捕獲的方法,如圖所示:

    正則match方法

    當加了修飾符g,返回的是大正則匹配結果組成的陣列,不加修飾符g則返回大正則和每個小分組返回結果組成的陣列,跟上面我們手寫的myExec一樣,其實原理就是我們上面寫的方法,如果想在加了修飾符g的時候返回結果和沒加一樣,所以在直接用match方法就能將匹配內容全部捕獲到。 但是他也有侷限性,就想上面說的在加了修飾符g的時候,會忽略小分組捕獲內容,只捕獲大正則捕獲內容,解決辦法就向上面myExec一樣,將arrs[0]改為arrs,在每次匹配到結果時,將每個小分組也儲存下來。

  • 2.replace

    主要運用在替換,其中兩個引數,第一個為要替換的字串或者正則,第二個是替換內容或一個返回函式,具體操作如下:

    var str = 'my name is {weiran}, my blog is {ngaiwe}'
    var reg = /\{([a-z]+)\}/
    str = str.replace(reg, '123')
    console.log(str)
    // 列印出 my name is 123, my blog is {ngaiwe}
    // 同學們會發現和exec的懶惰性很相似,不加修飾符g 只匹配第一個lastIndex沒有改變
    
    var reg = /\{([a-z]+)\}/g
    // 列印出 my name is 123, my blog is 123
    複製程式碼

    並且replace不會修改原始字串

    正則replace方法

    var str = 'my name is {weiran}, my blog is {ngaiwe}'
    var reg = /\{([a-z]+)\}/g
    str = str.replace(reg, function () {
    	console.log(arguments)
    })
    // 列印出當前匹配的小分組,如果函式中沒有return出替換值,則返回undefined
    複製程式碼
  • 3.split

    按照正則方式可以拆分成陣列,具體例子如下:

    var str = 'weiRanNgaiWe'
    var reg = /[A-Z]/
    console.log(str.split(reg))
    // ["wei", "an", "gai", "e"]按照大寫拆分成陣列
    複製程式碼
  • 4.search

    類似於indexOf,返回匹配元素的起始位置,如果沒有返回-1,不支援修飾符g

    var str = 'ngaiwe@126.com'
    var reg = /\d+/
    console.log(str.search(reg))
    // 返回 7
    複製程式碼

六.實戰案例剖析

  • 1.身份證號碼

    舉個身份證的例子: 110105199109214237
    由此分析前6位是數字地區區號組成,然後四位是年,兩位月,兩位日和四位隨機,倒數第二位單數男性,雙數女性,最後一位可能是大寫X,所以根據這個規則的正則是

    var str = '110105199109214237'
    var reg = /^(\d{6})(\d{4})(\d{2})(\d{2})\d{2}(\d)(\d|X)$/
    console.log(reg.exec(str))
    // ["110105199109214237", "110105", "1991", "09", "21", "3", "7", index: 0, input: "110105199109214237", groups: undefined]
    複製程式碼
  • 2.郵箱

    舉個郵箱的例子:weiran@vipkid.com.cn
    由此分析:
    1.@前面可能是數字、字母、下劃線、-、. 2.-和.不能相連在一起
    /^\w+((-|\w+)|(.\w+))*/ 開頭一定是數字,字母或下劃線組成,後面的內容可能是-與數字字母下劃線 或者.和數字字母下劃線組成的0到多個字元
    3.@後面部分
    首先一定是數字字母組成的多位字元
    然後可能存在是.組成的郵箱字尾或者連結前方字元的.和-
    最後肯定是.組成的郵箱字尾

    var reg = /^\w+((-|\w+)|(\.\w+))*@[a-zA-Z0-9]+((\.|-)[a-zA-Z0-9]+)*\.[a-zA-Z0-9]+$/
    var str = 'weiran@vipkid.com.cn'
    console.log(reg.test(str))
    // true
    複製程式碼
  • 3.URL擷取

    舉個擷取url引數的例子:
    www.ngaiwe.com/page/index.…
    我們想要的是引數轉化鍵值對和雜湊值{name: 'weiran',age: 27, sex: 0, HASH: 'develpoment'}
    由此分析:
    需要分為兩部分捕獲,首先第一次捕獲?後面的引數,第二次捕獲#後面的hash值
    首先匹配第一個,他的規則是匹配等號兩邊所以是/()=()/,並且匹配的是非?&=#的特殊字元,將他們儲存在obj物件中
    其次匹配hash,方法和第一個類似只是匹配#後面的部分

    String.prototype.myQueryURLParameter = function myQueryURLParameter () {
    	var obj = {}
    	this.replace(/([^?&=#]+)=([^?&=#]+)/g, function () {
    		obj[arguments[1]] = arguments[2]
    	})
    	this.replace(/#([^?&=#]+)/g, function () {
    		obj['HASH'] = arguments[1]
    	})
    	return obj
    }
    複製程式碼

七.總結

具體案例就就少到這裡,網上有各種正則的案例,同學們以後看到案例具體分析他們是如何寫的,不要一味地使用,這樣才能在工作中不斷地思考,學習。

八.部落格

魏燃技術部落格

有任何問題可留言或者傳送本人郵箱ngaiwe@126.com

九.分享

正則元字元表
正則線上測試工具

相關文章