正則表達(dá)式

初衷:看了很多視頻、文章东涡,最后卻通通忘記了冯吓,別人的知識(shí)依舊是別人的,自己卻什么都沒獲得疮跑。此系列文章旨在加深自己的印象组贺,因此此系列文章大都是將別人的文章連復(fù)制帶寫而來(lái),若有侵權(quán)祖娘,請(qǐng)及時(shí)通知失尖,必定立即刪除。

正則表達(dá)式

首先我們要了解正則表達(dá)式是什么渐苏,它是一種匹配模式掀潮,不僅能匹配匹配字符,還能匹配位置琼富,不少人忽略了匹配字符這個(gè)作用仪吧,往往碰到這種問題就手足無(wú)措。

正則的模糊匹配

如果正則只有精確匹配是沒有多大意義的鞠眉,比如:

var reg = /hello/
console.log(reg.test('hello'))//true

正則表達(dá)式的強(qiáng)大之處在于它的模糊匹配薯鼠,分為橫向模糊和縱向模糊

橫向模糊:一個(gè)正則可匹配的字符串的長(zhǎng)度不是固定的,可以是多種情況的
其實(shí)現(xiàn)的方式是使用量詞:

{m,} 表示至少出現(xiàn)m次凡蚜。

{m} 等價(jià)于{m,m}人断,表示出現(xiàn)m次。

? 等價(jià)于{0,1}朝蜘,表示出現(xiàn)或者不出現(xiàn)恶迈。記憶方式:?jiǎn)柼?hào)的意思表示,有嗎谱醇?

+ 等價(jià)于{1,}暇仲,表示出現(xiàn)至少一次。記憶方式:加號(hào)是追加的意思副渴,得先有一個(gè)奈附,然后才考慮追加。

* 等價(jià)于{0,}煮剧,表示出現(xiàn)任意次斥滤,有可能不出現(xiàn)。記憶方式:看看天上的星星勉盅,可能一顆沒有佑颇,可能零散有幾顆,可能數(shù)也數(shù)不過來(lái)草娜。

比如:

var reg = /ab{2,5}c/g;
var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
console.log( string.match(reg) ); // ["abbc", "abbbc", "abbbbc", "abbbbbc"]

橫向模糊匹配到了多種情況挑胸,案例中用的正則是/ab{2,5}c/g,后面多了g宰闰,它是正則的一個(gè)修飾符茬贵。表示全局匹配簿透,即在目標(biāo)字符串中按順序找到滿足匹配模式的所有子串,強(qiáng)調(diào)的是“所有”解藻,而不只是“第一個(gè)”老充。

縱向模糊:一個(gè)正則匹配的字符串,具體到某一位字符時(shí)舆逃,它可以不是某個(gè)確定的字符蚂维,可以有多種可能
其實(shí)現(xiàn)的方式是使用范圍類

[a-z]//a-z之間的字母

-表示連字符,在此處作特殊用處路狮,但是如果我要匹配'a-z'這三個(gè)字符呢?可以這么寫:

[-az]或[a\-z]或[az-]

這樣引擎就不會(huì)認(rèn)為它們是一個(gè)氛圍了蔚约,符號(hào)在范圍類中起取反的作用奄妨,a表示除了a的所有字符。

系統(tǒng)根據(jù)范圍類又預(yù)定義了一些類方便我們使用:

\d 就是[0-9]苹祟。表示是一位數(shù)字砸抛。記憶方式:其英文是digit(數(shù)字)。

\D 就是[^0-9]树枫。表示除數(shù)字外的任意字符直焙。

\w 就是[0-9a-zA-Z_]。表示數(shù)字砂轻、大小寫字母和下劃線奔誓。記憶方式:w是word的簡(jiǎn)寫,也稱單詞字符搔涝。

\W 就是[^0-9a-zA-Z_]厨喂。非單詞字符。

\s 就是[ \t\v\n\r\f]庄呈。表示空白符蜕煌,包括空格、水平制表符诬留、垂直制表符斜纪、換行符、回車符文兑、換頁(yè)符盒刚。記憶方式:s是space character的首字母。

\S 就是[^ \t\v\n\r\f]彩届。 非空白符伪冰。

. 就是[^\n\r\u2028\u2029]。通配符樟蠕,表示幾乎任意字符贮聂。換行符靠柑、回車符、行分隔符和段分隔符除外吓懈。記憶方式:想想省略號(hào)...中的每個(gè)點(diǎn)歼冰,都可以理解成占位符,表示任何類似的東西耻警。

正則的貪婪匹配和惰性匹配

var reg = /\d{2,5}/g;//此時(shí)加了g就是貪婪匹配
var string = "123 1234 12345 123456";
console.log( string.match(reg) ); // ["123", "1234", "12345", "12345"]
var reg = /\d{2,5}/;//此時(shí)沒有g(shù)就是惰性匹配
var string = "123 1234 12345 123456";
console.log( string.match(reg) ); // ["123", index: 0, input: "123 1234 12345 123456"]
//input是正則構(gòu)造函數(shù)的屬性隔嫡,表示最近一次要匹配的字符串,即是輸入的文本

不加g就是惰性匹配甘穿,我匹配完一個(gè)就不敢了腮恩,懶得再干其他事兒了,加了g就是貪婪模式了,我現(xiàn)在精力無(wú)限温兼,會(huì)盡可能的干事兒秸滴,但是我還有些理智,不會(huì)干超出能力之外的事兒募判,比如你給我的范圍是{2,5}荡含,我會(huì)盡可能做5件事兒,但是不會(huì)超過5件事,反正只要在能力范圍內(nèi)届垫,越多越好

此時(shí)我既想盡可能的匹配又想讓它不那么貪婪有沒有辦法呢释液?辦法是有的,貪婪模式一般作用在量詞這里,限制在量詞這里就好了装处,可以在量詞這里加一個(gè)误债?即可搞定。

var reg = /\d{2,5}?/g;
var string = "123 1234 12345 123456";
console.log( string.match(reg) ); // ["12", "12", "34", "12", "34", "12", "34", "56"]

其中/\d{2,5}?/表示符衔,雖然2到5次都行找前,當(dāng)2個(gè)就夠的時(shí)候,就不在往下嘗試了判族。
此時(shí)就達(dá)到了我們的要求,不過這里完全是為了講解貪婪模式和惰性模式躺盛,并不推薦這么做,我完全可以將{2,5}改成{2}形帮,一樣的效果

var reg = /\d{2}/g;
var string = "123 1234 12345 123456";
console.log( string.match(reg) ); // ["12", "12", "34", "12", "34", "12", "34", "56"]

知道了惰性模式的原理槽惫,我們完全可以鼓搗出其他的各式各樣的情形:

{m,n}? 
{m,}?
??
+?
*?

多選分支

一個(gè)模式可以實(shí)現(xiàn)橫向和縱向模糊匹配。而多選分支可以支持多個(gè)子模式任選其一
具體形式如下:(p1|p2|p3)辩撑,其中p1界斜、p2和p3是子模式,用“|”(管道符)分隔合冀,表示其中任何之一

var reg = /good|nice/g;
var string = "good idea, nice try.";
console.log( string.match(reg) ); // ["good", "nice"]

但有個(gè)事實(shí)我們應(yīng)該注意各薇,比如我用/good|goodbye/,去匹配"goodbye"字符串時(shí),結(jié)果是"good"

var reg = /good|goodbye/g;
var string = "goodbye";
console.log( string.match(reg) ); // ["good"]

而把正則改成/goodbye|good/峭判,結(jié)果是

var reg = /goodbye|good/g;
var string = "goodbye";
console.log( string.match(reg) ); // ["goodbye"]

也就是說(shuō)开缎,分支結(jié)構(gòu)也是惰性的,即當(dāng)前面的分支匹配上了林螃,后面的就不再嘗試了

并且奕删,使用分支的時(shí)候注意使用括號(hào),

var reg = /a1|2|3b/ //x
var reg = /a(1|2|3)b/ //y

案例分析

匹配字符疗认,無(wú)非就是范圍類完残、量詞和分支結(jié)構(gòu)的組合使用罷了

  • 匹配16進(jìn)制顏色值
#ffbbad
#Fc01DF
#FFF
#ffE

分析:

表示一個(gè)16進(jìn)制字符,可以用范圍類[0-9a-fA-F]
其中字符可以出現(xiàn)3或6次横漏,需要是用量詞和分支結(jié)構(gòu)
使用分支結(jié)構(gòu)時(shí)谨设,需要注意順序(惰性)

var reg = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g
console.log(reg.test('#ffbbad'))//true
var str = '#ffbbad #Fc01DF #FFF #ffE'
console.log( str.match(reg) )
//["#ffbbad", "#Fc01DF", "#FFF", "#ffE"]
  • 匹配時(shí)間
    以24小時(shí)制為例:
23:59
02:07

分析:
對(duì)每個(gè)地方的數(shù)字進(jìn)行分析:
共4位數(shù)字,第一位數(shù)字可以為[0-2]绊茧。
當(dāng)?shù)?位為2時(shí)铝宵,第2位可以為[0-3],其他情況時(shí)华畏,第2位為[0-9]。
第3位數(shù)字為[0-5]尊蚁,第4位為[0-9]

var reg = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/
console.log( reg.test("23:59") ); // true
console.log( reg.test("02:07") ); // true

如果也要求匹配7:9亡笑,也就是說(shuō)時(shí)分前面的0可以省略:

var reg = /^(0?[0-9]|1[0-9]|[2][0-3]):0?[0-9]|[1-5][0-9]$/
console.log( reg.test("23:59") ); // true
console.log( reg.test("02:07") ); // true
console.log( reg.test("7:9") ); // true
  • 匹配日期
    比如yyyy-mm-dd格式為例
2017-06-10

分析:
年,四位數(shù)字即可横朋,可用[0-9]{4}仑乌。

月,共12個(gè)月琴锭,分兩種情況01晰甚、02、……决帖、09和10厕九、11、12地回,可用(0[1-9]|1[0-2])扁远。

日,最大31天刻像,可用(0[1-9]|[12][0-9]|3[01])

var reg = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
console.log( reg.test("2017-06-10") ); // true
  • window操作系統(tǒng)文件路徑
F:\study\javascript\reg\regular expression.pdf
F:\study\javascript\reg\
F:\study\javascript
F:\

分析:
整體模式是: 盤符:\文件夾\文件夾\文件夾
其中匹配F:\畅买,需要使用[a-zA-Z]:\,其中盤符不區(qū)分大小寫细睡,注意\字符需要轉(zhuǎn)義谷羞。

文件名或者文件夾名,不能包含一些特殊字符溜徙,此時(shí)我們需要排除范圍類[\:*<>|"?\r\n/]來(lái)表示合法字符湃缎。另外不能為空名犀填,至少有一個(gè)字符,也就是要使用量詞+雁歌。因此匹配“文件夾\”宏浩,可用[\:*<>|"?\r\n/]+\。

另外“文件夾\”靠瞎,可以出現(xiàn)任意次比庄。也就是([^\:<>|"?\r\n/]+\)。其中括號(hào)提供子表達(dá)式乏盐。

路徑的最后一部分可以是“文件夾”佳窑,沒有\(zhòng),因此需要添加([^\:*<>|"?\r\n/]+)?父能。

最后拼接成了一個(gè)看起來(lái)比較復(fù)雜的正則:

var reg = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/;
console.log( reg.test("F:\\study\\javascript\\reg\\regular expression.pdf") ); // true
console.log( reg.test("F:\\study\\javascript\\reg\\") ); // true
console.log( reg.test("F:\\study\\javascript") ); // true
console.log( reg.test("F:\\") ); // true
  • 匹配id
從<div id="container" class="main"></div>提取出 id="container"
var reg = /id=".*?"/
var str = '<div id="container" class="main"></div>';
console.log(str.match(reg)[0]); // id="container"

用到了惰性匹配神凑,防止把class也提取出來(lái)

優(yōu)化:

var reg = /id="[^"]*"/
var str = '<div id="container" class="main"></div>';
console.log(str.match(reg)[0]); // id="container"

正則中被忽略的位置

  • 位置是相鄰字符之間的位置


    位置
  • 如何匹配位置

^ $ \b \B (?=p) (?!p)

^(脫字符)匹配開頭,在多行匹配中匹配行開頭何吝。

$(美元符號(hào))匹配結(jié)尾溉委,在多行匹配中匹配行結(jié)尾

比如我們把字符串的開頭和結(jié)尾用"#"替換(位置可以替換成字符的!):

var result = 'hello'.replace(/^|$/g,'#')
console.log(result)
//#hello#

多行匹配模式時(shí)爱榕,二者是行的概念瓣喊,這個(gè)需要我們的注意

var result = "I\nlove\njs".replace(/^|$/gm, '#');
console.log(result);
//
#I#
#love#
#js#

\b是單詞邊界,具體就是\w和\W之間的位置黔酥,也包括\w和^之間的位置藻三,也包括\w和$之間的位置

var result = '[js] lesson_01.mp4'.replace(/\b/g,'#')
console.log(result)
//[#js#]#lesson_01#.#mp4#

首先,我們知道跪者,\w是范圍類[0-9a-zA-Z_]的簡(jiǎn)寫形式棵帽,即\w是字母數(shù)字或者下劃線的中任何一個(gè)字符。而\W是排除范圍類[^0-9a-zA-Z_]的簡(jiǎn)寫形式渣玲,即\W是\w以外的任何一個(gè)字符

此時(shí)我們可以看看"[#JS#] #Lesson_01#.#mp4#"中的每一個(gè)"#"逗概,是怎么來(lái)的。

第一個(gè)"#"柜蜈,兩邊是"["與"J"仗谆,是\W和\w之間的位置。

第二個(gè)"#"淑履,兩邊是"S"與"]"隶垮,也就是\w和\W之間的位置。

第三個(gè)"#"秘噪,兩邊是空格與"L"狸吞,也就是\W和\w之間的位置。

第四個(gè)"#",兩邊是"1"與"."蹋偏,也就是\w和\W之間的位置便斥。

第五個(gè)"#",兩邊是"."與"m"威始,也就是\W和\w之間的位置枢纠。

第六個(gè)"#",其對(duì)應(yīng)的位置是結(jié)尾黎棠,但其前面的字符"4"是\w晋渺,即\w和$之間的位置。

知道了\b的概念后脓斩,那么\B也就相對(duì)好理解了木西。

\B就是\b的反面的意思,非單詞邊界随静。例如在字符串中所有位置中八千,扣掉\b,剩下的都是\B的燎猛。

具體說(shuō)來(lái)就是\w與\w恋捆、\W與\W、^與\W重绷,\W與$之間的位置

var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');

console.log(result); // "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
// #[J#S]# L#e#s#s#o#n#_#0#1.m#p#4
exp1(?=exp2)    匹配后面是exp2的exp1
exp1(?!exp2)    匹配后面不是exp2的exp1
(?=p)鸠信,其中p是一個(gè)子模式,即字符p前面的位置
(?!p)论寨,其中p是一個(gè)子模式,即不是字符p前面的位置爽茴,要求接下來(lái)的字符與p匹配葬凳,但不能包括p的那些字符

exp1(?=exp2) 表達(dá)式會(huì)匹配exp1表達(dá)式,但只有其后面內(nèi)容是exp2的時(shí)候才會(huì)匹配

exp1(?=exp2) 表達(dá)式會(huì)匹配exp1表達(dá)式室奏,但只有其后面內(nèi)容不是exp2的時(shí)候才會(huì)匹配

(/good(?=Byron)/).exec('goodByron123'); //['good']
(/good(?=Byron)/).exec('goodCasper123'); //null

(/good(?!Byron)/).exec('goodByron123'); //null
(/good(?!Byron)/).exec('goodCasper123'); //['good']

(?=p)火焰,其中p是一個(gè)子模式,即p前面的位置
比如(?=l)胧沫,表示'l'字符前面的位置昌简,例如:

var result = 'hello'.replace(/(?=l)/g,'#')
console.log(result)
//he#l#lo

而(?!p)就是(?=p)的反面意思

var result = "hello".replace(/(?!l)/g, '#');

console.log(result); // "#h#ell#o#"

對(duì)于位置的理解,我們可以理解成空字符""

"hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";
"hello" == "" + "" + "hello"

因此绒怨,把/hello$/寫成/^hello$$$/纯赎,是沒有任何問題的:

var result = /^^hello$$$/.test("hello");
console.log(result); // true
var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello");
//這些亂七八糟的都是匹配的位置,可以當(dāng)成''

console.log(result); // true

也就是說(shuō)字符之間的位置南蹂,可以寫成多個(gè)犬金。

把位置理解空字符,是對(duì)位置非常有效的理解方式

  • 相關(guān)案例
  1. 不匹配任何東西的正則
/.^/

此正則要求只有一個(gè)字符,但該字符后面是開頭晚顷。

  1. 數(shù)字的千位分隔符表示法

比如把"12345678"峰伙,變成"12,345,678"。

可見是需要把相應(yīng)的位置替換成","

使用(?=\d{3}$)就可以做到:

var result = "12345678".replace(/(?=\d{3}$)/g, ',')

console.log(result); // "12345,678"

因?yàn)槎禾?hào)出現(xiàn)的位置该默,要求后面3個(gè)數(shù)字一組瞳氓,也就是\d{3}至少出現(xiàn)一次。

此時(shí)可以使用量詞+:

var result = "12345678".replace(/(?=(\d{3})+$)/g, ',')

console.log(result); // "12,345,678"

此時(shí)會(huì)出現(xiàn)問題:

var result = "123456789".replace(/(?=(\d{3})+$)/g, ',')

console.log(result); // ",123,456,789"

上面的正則,僅僅表示把從結(jié)尾向前數(shù)傲绣,一但是3的倍數(shù)葫督,就把其前面的位置替換成逗號(hào)

怎么解決呢?我們要求匹配的到這個(gè)位置不能是開頭恋沃。

我們知道匹配開頭可以使用^,但要求這個(gè)位置不是開頭怎么辦必指?

easy囊咏,(?!^)

var str1 = "12345678",

var str2 = "123456789";

reg = /(?!^)(?=(\d{3})+$)/g;

var result1 = str1.replace(reg, ',')

console.log(result1); // "12,345,678"

var result2 = str2.replace(reg, ',');

console.log(result2); // "123,456,789"

如果要把"12345678 123456789"替換成"12,345,678 123,456,789"。

此時(shí)我們需要修改正則塔橡,把里面的開頭^和結(jié)尾$梅割,替換成\b

var str = "12345678 123456789",

reg = /(?!\b)(?=(\d{3})+\b)/g;

var result = str.replace(reg, ',')

console.log(result); // "12,345,678 123,456,789"

其中(?!\b)怎么理解呢?

要求當(dāng)前是一個(gè)位置葛家,但不是\b前面的位置户辞,其實(shí)(?!\b)說(shuō)的就是\B。

因此最終正則變成了:/\B(?=(\d{3})+\b)/g

  • 密碼長(zhǎng)度6-12位癞谒,由數(shù)字底燎、小寫字符和大寫字母組成,但必須至少包括2種字符弹砚。

此題双仍,如果寫成多個(gè)正則來(lái)判斷,比較容易桌吃。但要寫成一個(gè)正則就比較困難朱沃。

那么,我們就來(lái)挑戰(zhàn)一下茅诱《何铮看看我們對(duì)位置的理解是否深刻

var reg = /^[0-9A-Za-z]{6,12}$/;
判斷是否包含有某一種字符:(?=.*[0-9]):數(shù)字前面的位置,所以必須包含數(shù)字
var reg = /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/;
同時(shí)包含數(shù)字和小寫字母瑟俭,可以用(?=.*[0-9])(?=.*[a-z]) 數(shù)字和字母前面的位置翎卓,數(shù)字、字母前面的位置尔当,所以必須包含數(shù)字莲祸、字母
var reg = /(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/;

我們可以把原題變成下列幾種情況之一:

1.同時(shí)包含數(shù)字和小寫字母

2.同時(shí)包含數(shù)字和大寫字母

3.同時(shí)包含小寫字母和大寫字母

4.同時(shí)包含數(shù)字蹂安、小寫字母和大寫字母

var reg = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9A-Za-z]{6,12}$/;

console.log( reg.test("1234567") ); // false 全是數(shù)字

console.log( reg.test("abcdef") ); // false 全是小寫字母

console.log( reg.test("ABCDEFGH") ); // false 全是大寫字母

console.log( reg.test("ab23C") ); // false 不足6位

console.log( reg.test("ABCDEF234") ); // true 大寫字母和數(shù)字

console.log( reg.test("abcdEF234") ); // true 三者都有

上面的正則看起來(lái)比較復(fù)雜,只要理解了第二步锐帜,其余就全部理解了田盈。

/(?=.*[0-9])^[0-9A-Za-z]{6,12}$/

對(duì)于這個(gè)正則,我們只需要弄明白(?=.*[0-9])^即可缴阎。

分開來(lái)看就是(?=.*[0-9])和^允瞧。

表示開頭前面還有個(gè)位置(當(dāng)然也是開頭,即同一個(gè)位置蛮拔,想想之前的空字符類比)述暂。

(?=.[0-9])表示該位置后面的字符匹配.[0-9],即建炫,有任何多個(gè)任意字符畦韭,后面再跟個(gè)數(shù)字。

另一種解法:
“至少包含兩種字符”的意思就是說(shuō)肛跌,不能全部都是數(shù)字艺配,也不能全部都是小寫字母,也不能全部都是大寫字母衍慎。

那么要求“不能全部都是數(shù)字”转唉,怎么做呢?(?!p)出馬稳捆!

var reg = /(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/;

三種'都不能'呢赠法?

var reg = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;

console.log( reg.test("1234567") ); // false 全是數(shù)字

console.log( reg.test("abcdef") ); // false 全是小寫字母

console.log( reg.test("ABCDEFGH") ); // false 全是大寫字母

console.log( reg.test("ab23C") ); // false 不足6位

console.log( reg.test("ABCDEF234") ); // true 大寫字母和數(shù)字

console.log( reg.test("abcdEF234") ); // true 三者都有

正則表達(dá)式括號(hào)的作用

1.分組和分支結(jié)構(gòu)

var reg = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(reg) ); // ["abab", "ab", "ababab"]

括號(hào)是提供分組功能,使量詞'+'作用于z和這個(gè)整體

var reg = /^I love (JavaScript|Regular Expression)$/;
console.log( reg.test("I love JavaScript") ); // true
console.log( reg.test("I love Regular Expression") ); // true

而在多選分支結(jié)構(gòu)(p1|p2)中乔夯,此處括號(hào)的作用也是不言而喻的砖织,提供了子表達(dá)式的所有可能

  1. 引用分組
    這是括號(hào)一個(gè)重要的作用,有了它末荐,我們就可以進(jìn)行數(shù)據(jù)提取镶苞,以及更強(qiáng)大的替換操作。

而要使用它帶來(lái)的好處鞠评,必須配合使用實(shí)現(xiàn)環(huán)境的API

以日期為例。假設(shè)格式是yyyy-mm-dd的

var reg = /(\d{4})-(\d{2})-(\d{2})/

提取數(shù)據(jù):

var reg = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( string.match(reg) ); 
//["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

match返回的一個(gè)數(shù)組壕鹉,第一個(gè)元素是整體匹配結(jié)果剃幌,然后是各個(gè)分組(括號(hào)里)匹配的內(nèi)容,然后是匹配下標(biāo)晾浴,最后是輸入的文本

可以使用正則對(duì)象的exec方法:

var reg = /(\d{4})-(\d{2})-(\d{2})/;
var str = "2017-06-12";
console.log( reg.exec(str) ); 
//["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

也可以使用構(gòu)造函數(shù)的全局屬性$1至$9來(lái)獲雀合纭:

var reg = /(\d{4})-(\d{2})-(\d{2})/;
var str = "2017-06-12";

reg.test(str); // 正則操作即可,例如
//reg.exec(str);
//str.match(reg);

console.log(regp.$1); // "2017"
console.log(regp.$2); // "06"
console.log(regp.$3); // "12"

替換:

想把yyyy-mm-dd格式脊凰,替換成mm/dd/yyyy怎么做抖棘?

var reg = /(\d{4})-(\d{2})-(\d{2})/;
var str = "2017-06-12";
var result = str.replace(reg, "$2/$3/$1");
console.log(result); // "06/12/2017"
var reg = /(\d{4})-(\d{2})-(\d{2})/;
var str = "2017-06-12";
var result = str.replace(reg, function() {
    return regp.$2 + "/" + regp.$3 + "/" + regp.$1;
});
console.log(result); // "06/12/2017"
var reg = /(\d{4})-(\d{2})-(\d{2})/;
var str = "2017-06-12";
var result = str.replace(reg, function(match, year, month, day) {
    return month + "/" + day + "/" + year;
});
console.log(result); // "06/12/2017"

反向引用:

比如要寫一個(gè)正則支持匹配如下三種格式:

2016-06-12

2016/06/12

2016.06.12
var reg = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var str1 = "2017-06-12";
var str2 = "2017/06/12";
var str3 = "2017.06.12";
var str4 = "2016-06/12";
console.log( reg.test(str1) ); // true
console.log( reg.test(str2) ); // true
console.log( reg.test(str3) ); // true
console.log( reg.test(str4) ); // false

注意里面的\1茂腥,表示的引用之前的那個(gè)分組(-|/|.)。不管它匹配到什么(比如-)切省,\1都匹配那個(gè)同樣的具體某個(gè)字符

括號(hào)嵌套怎么辦最岗?

以左括號(hào)(開括號(hào))為準(zhǔn)

var reg = /^((\d)(\d(\d)))\1\2\3\4$/;
var str = "1231231233";
console.log( reg.test(str) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3

\10是表示第10個(gè)分組,還是\1和0呢朝捆?答案是前者般渡,雖然一個(gè)正則里出現(xiàn)\10比較罕見

引用不存在的分組會(huì)怎樣?

因?yàn)榉聪蛞密脚蹋且们懊娴姆纸M驯用,但我們?cè)谡齽t里引用了不存在的分組時(shí),此時(shí)正則不會(huì)報(bào)錯(cuò)儒老,只是匹配反向引用的字符本身蝴乔。例如\2,就匹配"\2"驮樊。注意"\2"表示對(duì)2進(jìn)行了轉(zhuǎn)意

var reg = /\1\2\3\4\5\6\7\8\9/;
console.log( reg.test("\1\2\3\4\5\6\7\8\9") ); 
console.log( "\1\2\3\4\5\6\7\8\9".split("") );
// ["?", "?", "?", "?", "?", "?", "?", "8", "9"]

非捕獲分組:

之前文中出現(xiàn)的分組薇正,都會(huì)捕獲它們匹配到的數(shù)據(jù),以便后續(xù)引用巩剖,因此也稱他們是捕獲型分組铝穷。

如果只想要括號(hào)最原始的功能,但不會(huì)引用它佳魔,即曙聂,既不在API里引用,也不在正則里反向引用鞠鲜。此時(shí)可以使用非捕獲分組(?:p)

var reg = /(?:ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) ); // ["abab", "ab", "ababab"]
  • 相關(guān)案例:
  1. 字符串trim方法模擬
    第一種宁脊,匹配到開頭和結(jié)尾的空白符,然后替換成空字符
function trim(str) {
    return str.replace(/^\s+|\s+$/g, '');
}
console.log( trim("  hello   ") ); 
//"hello"

第二種贤姆,匹配整個(gè)字符串榆苞,然后用引用來(lái)提取出相應(yīng)的數(shù)據(jù)

function trim(str) {
    return str.replace(/^\s*(.*?)\s*$/g,"$1");
}
console.log( trim("  hello   ") ); 
// "hello"
  1. 將每個(gè)單詞的首字母轉(zhuǎn)換為大寫
function titleize(str) {
    return str.toLowerCase().replace(/(?:^|\s)\w/g, function(c) {
        return c.toUpperCase();
    });
}
console.log( titleize('my name is epeli') );// "My Name Is Epeli"

思路是找到每個(gè)單詞的首字母,當(dāng)然這里不使用非捕獲匹配也是可以的

  1. 駝峰化
function camelize(str) {
    return str.replace(/[-_\s]+(.)?/g, function(match, c) {
        return c ? c.toUpperCase() : '';
    });
}
console.log( camelize('-moz-transform') ); // MozTransform

首字母不會(huì)轉(zhuǎn)化為大寫的霞捡。其中分組(.)表示首字母坐漏,單詞的界定,前面的字符可以是多個(gè)連字符碧信、下劃線以及空白符赊琳。正則后面的?的目的,是為了應(yīng)對(duì)str尾部的字符可能不是單詞字符砰碴,比如str是'-moz-transform '

  1. 中劃線化
function dasherize(str) {
    return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
}
console.log( dasherize('MozTransform') ); // -moz-transform
  1. html轉(zhuǎn)義和反轉(zhuǎn)義
// 將HTML特殊字符轉(zhuǎn)換成等值的實(shí)體
function escapeHTML(str) {
    var escapeChars = {
      '¢' : 'cent',
      '£' : 'pound',
      '¥' : 'yen',
      '€': 'euro',
      '?' :'copy',
      '?' : 'reg',
      '<' : 'lt',
      '>' : 'gt',
      '"' : 'quot',
      '&' : 'amp',
      '\'' : '#39'
    };
    return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'), function(match) {
        return '&' + escapeChars[match] + ';';
    });
}
console.log( escapeHTML('<div>Blah blah blah</div>') );
// <div>Blah blah blah</div>
// 實(shí)體字符轉(zhuǎn)換為等值的HTML躏筏。
function unescapeHTML(str) {
    var htmlEntities = {
      nbsp: ' ',
      cent: '¢',
      pound: '£',
      yen: '¥',
      euro: '€',
      copy: '?',
      reg: '?',
      lt: '<',
      gt: '>',
      quot: '"',
      amp: '&',
      apos: '\''
    };
    return str.replace(/\&([^;]+);/g, function(match, key) {
        if (key in htmlEntities) {
            return htmlEntities[key];
        }
        return match;
    });
}
console.log( unescapeHTML('<div>Blah blah blah</div>') );
// => <div>Blah blah blah</div>

通過key獲取相應(yīng)的分組引用,然后作為對(duì)象的鍵

  1. 匹配成對(duì)標(biāo)簽
<title>regular expression</title>

<p>bye bye</p>

匹配一個(gè)開標(biāo)簽呈枉,可以使用正則<[^>]+>趁尼,

匹配一個(gè)閉標(biāo)簽埃碱,可以使用</[^>]+>,

但是要求匹配成對(duì)標(biāo)簽酥泞,那就需要使用反向引用

var reg = /<([^>]+)>[\d\D]*<\/\1>/;
var str1 = "<title>regular expression</title>";
var str2 = "<p>laoyao bye bye</p>";
var str3 = "<title>wrong!</p>";
console.log( reg.test(str1) ); // true
console.log( reg.test(str2) ); // true
console.log( reg.test(str3) ); // false

其中開標(biāo)簽<[>]+>改成<([>]+)>砚殿,使用括號(hào)的目的是為了后面使用反向引用,而提供分組婶博。閉標(biāo)簽使用了反向引用瓮具,</\1>。

另外[\d\D]的意思是凡人,這個(gè)字符是數(shù)字或者不是數(shù)字,因此挠轴,也就是匹配任意字符的意思

正則表達(dá)式的原理-回溯

  1. 沒有回溯的匹配
    假設(shè)我們的正則是/ab{1,3}c/传睹,其可視化形式是:


而當(dāng)目標(biāo)字符串是"abbbc"時(shí),就沒有所謂的“回溯”岸晦。其匹配過程是:



其中子表達(dá)式b{1,3}表示“b”字符連續(xù)出現(xiàn)1到3次欧啤。

  1. 有回溯的匹配
    如果目標(biāo)字符串是"abbc",中間就有回溯启上。


圖中第5步有紅顏色邢隧,表示匹配不成功。此時(shí)b{1,3}已經(jīng)匹配到了2個(gè)字符“b”冈在,準(zhǔn)備嘗試第三個(gè)時(shí)倒慧,結(jié)果發(fā)現(xiàn)接下來(lái)的字符是“c”。那么就認(rèn)為b{1,3}就已經(jīng)匹配完畢包券。然后狀態(tài)又回到之前的狀態(tài)(即第6步纫谅,與第4步一樣),最后再用子表達(dá)式c溅固,去匹配字符“c”付秕。當(dāng)然,此時(shí)整個(gè)表達(dá)式匹配成功了侍郭。

圖中的第6步询吴,就是“回溯”。

你可能對(duì)此沒有感覺亮元,這里我們?cè)倥e一個(gè)例子汰寓。正則是:


目標(biāo)字符串是"abbbc",匹配過程是:


其中第7步和第10步是回溯苹粟。第7步與第4步一樣,此時(shí)b{1,3}匹配了兩個(gè)"b"跃闹,而第10步與第3步一樣嵌削,此時(shí)b{1,3}只匹配了一個(gè)"b"毛好,這也是b{1,3}的最終匹配結(jié)果。

這里再看一個(gè)清晰的回溯苛秕,正則是:


目標(biāo)字符串是:"acd"ef肌访,匹配過程是:


圖中省略了嘗試匹配雙引號(hào)失敗的過程⊥Ы伲可以看出“.*”是非常影響效率的吼驶。

為了減少一些不必要的回溯,可以把正則修改為/"[^"]*"/店煞。

  1. 常見的回溯形式

正則表達(dá)式匹配字符串的這種方式蟹演,有個(gè)學(xué)名,叫回溯法顷蟀。

回溯法也稱試探法酒请,它的基本思想是:從問題的某一種狀態(tài)(初始狀態(tài))出發(fā),搜索從這種狀態(tài)出發(fā)所能達(dá)到的所有“狀態(tài)”鸣个,當(dāng)一條路走到“盡頭”的時(shí)候(不能再前進(jìn))羞反,再后退一步或若干步,從另一種可能“狀態(tài)”出發(fā)囤萤,繼續(xù)搜索昼窗,直到所有的“路徑”(狀態(tài))都試探過。這種不斷“前進(jìn)”涛舍、不斷“回溯”尋找解的方法澄惊,就稱作“回溯法”

本質(zhì)上就是深度優(yōu)先搜索算法做盅。其中退到之前的某一步這一過程缤削,我們稱為“回溯”。從上面的描述過程中吹榴,可以看出亭敢,路走不通時(shí),就會(huì)發(fā)生“回溯”图筹。即帅刀,嘗試匹配失敗時(shí),接下來(lái)的一步通常就是回溯

道理远剩,我們是懂了扣溺。那么JS中正則表達(dá)式會(huì)產(chǎn)生回溯的地方都有哪些呢?

3.1 貪婪量詞

之前的例子都是貪婪量詞相關(guān)的瓜晤。比如b{1,3}锥余,因?yàn)槠涫秦澙返模瑖L試可能的順序是從多往少的方向去嘗試痢掠。首先會(huì)嘗試"bbb"驱犹,然后再看整個(gè)正則是否能匹配嘲恍。不能匹配時(shí),吐出一個(gè)"b"雄驹,即在"bb"的基礎(chǔ)上佃牛,再繼續(xù)嘗試医舆。如果還不行爷速,再吐出一個(gè),再試里烦。如果還不行呢凿蒜?只能說(shuō)明匹配失敗了。

雖然局部匹配是貪婪的胁黑,但也要滿足整體能正確匹配废封。否則,皮之不存丧蘸,毛將焉附漂洋?

此時(shí)我們不禁會(huì)問,如果當(dāng)多個(gè)貪婪量詞挨著存在力喷,并相互有沖突時(shí)刽漂,此時(shí)會(huì)是怎樣弟孟?

答案是拂募,先下手為強(qiáng)庭猩!因?yàn)樯疃葍?yōu)先搜索。測(cè)試如下:

var str = "12345";

var reg = /(\d{1,3})(\d{1,3})/;
console.log( str.match(regex) );
//["12345", "123", "45", index: 0, input: "12345"]

其中陈症,前面的\d{1,3}匹配的是"123"录肯,后面的\d{1,3}匹配的是"45"。

3.2 惰性量詞
惰性量詞就是在貪婪量詞后面加個(gè)問號(hào)优炬。表示盡可能少的匹配疏叨,比如:

var string = "12345";
var regex = /(\d{1,3}?)(\d{1,3})/;
console.log( string.match(regex) );
// => ["1234", "1", "234", index: 0, input: "12345"]

其中\(zhòng)d{1,3}?只匹配到一個(gè)字符"1",而后面的\d{1,3}匹配了"234"穿剖。

雖然惰性量詞不貪,但也會(huì)有回溯的現(xiàn)象卦溢。比如正則是:


目標(biāo)字符串是"12345"糊余,匹配過程是:


知道你不貪、很知足单寂,但是為了整體匹配成贬芥,沒辦法,也只能給你多塞點(diǎn)了宣决。因此最后\d{1,3}?匹配的字符是"12"蘸劈,是兩個(gè)數(shù)字,而不是一個(gè)尊沸。

3.3 分支結(jié)構(gòu)

我們知道分支也是惰性的威沫,比如/can|candy/,去匹配字符串"candy"洼专,得到的結(jié)果是"can"棒掠,因?yàn)榉种?huì)一個(gè)一個(gè)嘗試,如果前面的滿足了屁商,后面就不會(huì)再試驗(yàn)了烟很。

分支結(jié)構(gòu),可能前面的子模式會(huì)形成了局部匹配蜡镶,如果接下來(lái)表達(dá)式整體不匹配時(shí)雾袱,仍會(huì)繼續(xù)嘗試剩下的分支。這種嘗試也可以看成一種回溯官还。

比如正則:


目標(biāo)字符串是"candy"芹橡,匹配過程:


上面第5步,雖然沒有回到之前的狀態(tài)妻枕,但仍然回到了分支結(jié)構(gòu)僻族,嘗試下一種可能。所以屡谐,可以認(rèn)為它是一種回溯的

簡(jiǎn)單總結(jié)就是述么,正因?yàn)橛卸喾N可能,所以要一個(gè)一個(gè)試愕掏。直到度秘,要么到某一步時(shí),整體匹配成功了;要么最后都試完后剑梳,發(fā)現(xiàn)整體匹配不成功唆貌。

  1. 貪婪量詞“試”的策略是:買衣服砍價(jià)。價(jià)錢太高了垢乙,便宜點(diǎn)锨咙,不行,再便宜點(diǎn)追逮。

  2. 惰性量詞“試”的策略是:賣東西加價(jià)酪刀。給少了,再多給點(diǎn)行不钮孵,還有點(diǎn)少啊骂倘,再給點(diǎn)。

  3. 分支結(jié)構(gòu)“試”的策略是:貨比三家巴席。這家不行历涝,換一家吧,還不行漾唉,再換荧库。

既然有回溯的過程,那么匹配效率肯定低一些毡证。相對(duì)誰(shuí)呢电爹?相對(duì)那些DFA引擎。

而JS的正則引擎是NFA料睛,NFA是“非確定型有限自動(dòng)機(jī)”的簡(jiǎn)寫丐箩。

大部分語(yǔ)言中的正則都是NFA,為啥它這么流行呢恤煞?

答:你別看我匹配慢屎勘,但是我編譯快啊,而且我還有趣哦居扒。

正則表達(dá)式的拆分(讀)

  1. 結(jié)構(gòu)和操作符
    編程語(yǔ)言一般都有操作符概漱。只要有操作符,就會(huì)出現(xiàn)一個(gè)問題喜喂。當(dāng)一大堆操作在一起時(shí)瓤摧,先操作誰(shuí)玉吁,又后操作誰(shuí)呢这揣?為了不產(chǎn)生歧義给赞,就需要語(yǔ)言本身定義好操作順序,即所謂的優(yōu)先級(jí)片迅。

而在正則表達(dá)式中残邀,操作符都體現(xiàn)在結(jié)構(gòu)中,即由特殊字符和普通字符所代表的一個(gè)個(gè)特殊整體。

JS正則表達(dá)式中,都有哪些結(jié)構(gòu)呢遗嗽?

字符字面量粘我、范圍類痹换、量詞征字、錨字符、分組娇豫、選擇分支匙姜、反向引用。
字面量,匹配一個(gè)具體字符,包括不用轉(zhuǎn)義的和需要轉(zhuǎn)義的订咸。比如a匹配字符"a"抹恳,又比如\n匹配換行符椎组,又比如\.匹配小數(shù)點(diǎn)。

范圍類,匹配一個(gè)字符哮洽,可以是多種可能之一填渠,比如[0-9]枪眉,表示匹配一個(gè)數(shù)字捺檬。也有\(zhòng)d的簡(jiǎn)寫形式。另外還有反義范圍類贸铜,表示可以是除了特定字符之外任何一個(gè)字符堡纬,比如[^0-9],表示一個(gè)非數(shù)字字符蒿秦,也有\(zhòng)D的簡(jiǎn)寫形式烤镐。

量詞,表示一個(gè)字符連續(xù)出現(xiàn)棍鳖,比如a{1,3}表示“a”字符連續(xù)出現(xiàn)3次炮叶。另外還有常見的簡(jiǎn)寫形式,比如a+表示“a”字符連續(xù)出現(xiàn)至少一次渡处。

錨點(diǎn)镜悉,匹配一個(gè)位置,而不是字符医瘫。比如^匹配字符串的開頭侣肄,又比如\b匹配單詞邊界,又比如(?=\d)表示數(shù)字前面的位置醇份。

分組稼锅,用括號(hào)表示一個(gè)整體,比如(ab)+僚纷,表示"ab"兩個(gè)字符連續(xù)出現(xiàn)多次缰贝,也可以使用非捕獲分組(?:ab)+。

分支畔濒,多個(gè)子表達(dá)式多選一剩晴,比如abc|bcd,表達(dá)式匹配"abc"或者"bcd"字符子串侵状。

反向引用赞弥,比如\2,表示引用第2個(gè)分組

其中涉及到的操作符有:

1.轉(zhuǎn)義符 \
2.括號(hào)和方括號(hào) (...)趣兄、(?:...)绽左、(?=...)、(?!...)艇潭、[...]
3.量詞限定符 {m}拼窥、{m,n}戏蔑、{m,}、?鲁纠、*总棵、+
4.位置和序列 ^ 、$改含、 \元字符情龄、 一般字符
5. 管道符(豎杠) |

上面操作符的優(yōu)先級(jí)從上至下,由高到低捍壤。

這里骤视,我們來(lái)分析一個(gè)正則:

/ab?(c|de*)+|fg/
  • 由于括號(hào)的存在,所以鹃觉,(c|de*)是一個(gè)整體結(jié)構(gòu)专酗。
  • 在(c|de)中,注意其中的量詞盗扇,因此e*是一個(gè)整體結(jié)構(gòu)笼裳。
  • 又因?yàn)榉种ЫY(jié)構(gòu)“|”優(yōu)先級(jí)最低,因此c是一個(gè)整體粱玲、而de*是另一個(gè)整體。
  • 同理拜轨,整個(gè)正則分成了 a抽减、b?、(...)+橄碾、f卵沉、g。而由于分支的原因法牲,又可以分成ab?(c|de*)+和fg這兩部分史汗。


  1. 注意要點(diǎn)
    2.1 匹配字符串整體問題

因?yàn)槭且ヅ湔麄€(gè)字符串,我們經(jīng)常會(huì)在正則前后中加上錨字符^和$拒垃。

比如要匹配目標(biāo)字符串"abc"或者"bcd"時(shí)停撞,如果一不小心,就會(huì)寫成/^abc|bcd$/悼瓮。

而位置字符和字符序列優(yōu)先級(jí)要比豎杠高戈毒,故其匹配的結(jié)構(gòu)是:


應(yīng)該修改成:


2.2 量詞連綴問題

假設(shè),要匹配這樣的字符串:

  1. 每個(gè)字符為a横堡、b埋市、c任選其一

  2. 字符串的長(zhǎng)度是3的倍數(shù)

此時(shí)正則不能想當(dāng)然地寫成/^[abc]{3}+$/,這樣會(huì)報(bào)錯(cuò)命贴,說(shuō)“+”前面沒什么可重復(fù)的:
此時(shí)要修改成:


2.3 元字符轉(zhuǎn)義問題
所謂元字符道宅,就是正則中有特殊含義的字符食听。

所有結(jié)構(gòu)里,用到的元字符總結(jié)如下:

^ $ . * + ? | \ / ( ) [ ] { } = ! : - ,

當(dāng)匹配上面的字符本身時(shí)污茵,可以一律轉(zhuǎn)義:

var str = "^$.*+?|\\/[]{}=!:-,";
// \字符也要轉(zhuǎn)義
var reg = /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/;
console.log( reg.test(str) ); 
// => true

在string中樱报,也可以把每個(gè)字符轉(zhuǎn)義,當(dāng)然省咨,轉(zhuǎn)義后的結(jié)果仍是本身:

var str1 = "^$.*+?|\\/[]{}=!:-,";
var str2 = "\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,";
console.log( str1 == str2 ); 
// => true

現(xiàn)在的問題是肃弟,是不是每個(gè)字符都需要轉(zhuǎn)義呢?否零蓉,看情況笤受。

2.3.1 范圍類中的元字符

跟范圍類相關(guān)的元字符有[]、^敌蜂、-箩兽。因此在會(huì)引起歧義的地方進(jìn)行轉(zhuǎn)義。例如開頭的^必須轉(zhuǎn)義章喉,不然會(huì)把整個(gè)范圍類汗贫,看成反義范圍類。

var str = "^$.*+?|\\/[]{}=!:-,";
var reg = /[\^$.*+?|\\/\[\]{}=!:\-,]/g;
console.log( str.match(reg) );
// => ["^", "$", ".", "*", "+", "?", "|", "\", "/", "[", "]", "{", "}", "=", "!", ":", "-", ","]

2.3.2 匹配“[abc]”和“{3,5}”

我們知道[abc]秸脱,是個(gè)字符組落包。如果要匹配字符串"[abc]"時(shí),該怎么辦摊唇?

可以寫成/[abc]/咐蝇,也可以寫成/[abc]/,測(cè)試如下:

var str = "[abc]";
var reg = /\[abc]/g;
console.log( str.match(reg)[0] ); 
// => "[abc]"

只需要在第一個(gè)方括號(hào)轉(zhuǎn)義即可巷查,因?yàn)楹竺娴姆嚼ㄌ?hào)構(gòu)不成字符組有序,正則不會(huì)引發(fā)歧義,自然不需要轉(zhuǎn)義岛请。

同理旭寿,要匹配字符串"{3,5}",只需要把正則寫成/{3,5}/即可崇败。

另外盅称,我們知道量詞有簡(jiǎn)寫形式{m,},卻沒有{,n}的情況后室。雖然后者不構(gòu)成量詞的形式微渠,但此時(shí)并不會(huì)報(bào)錯(cuò)。當(dāng)然咧擂,匹配的字符串也是"{,n}"逞盆,測(cè)試如下:

var str = "{,3}";
var reg = /{,3}/g;
console.log( str.match(reg)[0] );
// => "{,3}"

2.3.3 其余情況

比如= ! : - ,等符號(hào),只要不在特殊結(jié)構(gòu)中松申,也不需要轉(zhuǎn)義云芦。

但是俯逾,括號(hào)需要前后都轉(zhuǎn)義的,如/(123)/舅逸。

至于剩下的^ $ . * + ? | \ /等字符桌肴,只要不在字符組內(nèi),都需要轉(zhuǎn)義的琉历。

  1. 案例分析
    3.1 身份證
    正則表達(dá)式是:
/^(\d{15}|\d{17}[\dxX])$/

因?yàn)樨Q杠“|”,的優(yōu)先級(jí)最低坠七,所以正則分成了兩部分\d{15}和\d{17}[\dxX]。

\d{15}表示15位連續(xù)數(shù)字旗笔。
\d{17}[\dxX]表示17位連續(xù)數(shù)字彪置,最后一位可以是數(shù)字可以大小寫字母"x"。
可視化如下:


3.2 IPV4地址

正則表達(dá)式是:

/^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/

這個(gè)正則蝇恶,看起來(lái)非常嚇人拳魁。但是熟悉優(yōu)先級(jí)后,會(huì)立馬得出如下的結(jié)構(gòu):

((...)\.){3}(...)

上面的兩個(gè)(...)是一樣的結(jié)構(gòu)撮弧。表示匹配的是3位數(shù)字潘懊。因此整個(gè)結(jié)構(gòu)是

3位數(shù).3位數(shù).3位數(shù).3位數(shù)
然后再來(lái)分析(...):

(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])

它是一個(gè)多選結(jié)構(gòu),分成5個(gè)部分:

0{0,2}\d贿衍,匹配一位數(shù)授舟,包括0補(bǔ)齊的。比如贸辈,9释树、09、009裙椭;
0?\d{2},匹配兩位數(shù)署浩,包括0補(bǔ)齊的揉燃,也包括一位數(shù);
1\d{2}筋栋,匹配100到199;
2[0-4]\d炊汤,匹配200-249;
25[0-5]弊攘,匹配250-255抢腐。
最后來(lái)看一下其可視化形式:

掌握正則表達(dá)式中的優(yōu)先級(jí)后,再看任何正則應(yīng)該都有信心分析下去了襟交。

至于例子迈倍,不一而足,沒有寫太多捣域。

這里稍微總結(jié)一下啼染,豎杠的優(yōu)先級(jí)最低宴合,即最后運(yùn)算。

只要知道這一點(diǎn)迹鹅,就能讀懂大部分正則卦洽。

另外關(guān)于元字符轉(zhuǎn)義問題,當(dāng)自己不確定與否時(shí)斜棚,盡管去轉(zhuǎn)義阀蒂,總之是不會(huì)錯(cuò)的。

正則表達(dá)式的創(chuàng)建(寫)

本文就解決該問題弟蚀,內(nèi)容包括:

1. 平衡法則

2. 構(gòu)建正則前提

3. 準(zhǔn)確性

4. 效率
  1. 平衡法則
    構(gòu)建正則有一點(diǎn)非常重要蚤霞,需要做到下面幾點(diǎn)的平衡:

  2. 匹配預(yù)期的字符串

  3. 不匹配非預(yù)期的字符串

  4. 可讀性和可維護(hù)性

  5. 效率

  6. 構(gòu)建正則前提

2.1 是否能使用正則

正則太強(qiáng)大了,以至于我們隨便遇到一個(gè)操作字符串問題時(shí)粗梭,都會(huì)下意識(shí)地去想争便,用正則該怎么做。但我們始終要提醒自己断医,正則雖然強(qiáng)大滞乙,但不是萬(wàn)能的,很多看似很簡(jiǎn)單的事情鉴嗤,還是做不到的斩启。

比如匹配這樣的字符串:1010010001....

雖然很有規(guī)律,但是只靠正則就是無(wú)能為力醉锅。

2.2 是否有必要使用正則

要認(rèn)識(shí)到正則的局限兔簇,不要去研究根本無(wú)法完成的任務(wù)。同時(shí)硬耍,也不能走入另一個(gè)極端:無(wú)所不用正則垄琐。能用字符串API解決的簡(jiǎn)單問題,就不該正則出馬经柴。

比如狸窘,從日期中提取出年月日,雖然可以使用正則:

var str = "2017-07-01";
var reg = /^(\d{4})-(\d{2})-(\d{2})/;
console.log( str.match(reg) );
// => ["2017-07-01", "2017", "07", "01", index: 0, input: "2017-07-01"]

其實(shí)坯认,可以使用字符串的split方法來(lái)做翻擒,即可:

var str = "2017-07-01";
var result = str.split("-");
console.log( result );
// => ["2017", "07", "01"]

比如,判斷是否有問號(hào)牛哺,雖然可以使用:

var string = "?id=xx&act=search";
console.log( string.search(/\?/) );
// => 0

其實(shí)陋气,可以使用字符串的indexOf方法:

var string = "?id=xx&act=search";
console.log( string.indexOf("?") );
// => 0

比如獲取子串,雖然可以使用正則:

var str = "JavaScript";
console.log( str.match(/.{4}(.+)/)[1] );
// => Script

其實(shí)引润,可以直接使用字符串的substring或substr方法來(lái)做:

var str = "JavaScript";
console.log( str.substring(4) );
// => Script

2.3 是否有必要構(gòu)建一個(gè)復(fù)雜的正則

比如密碼匹配問題巩趁,要求密碼長(zhǎng)度6-12位,由數(shù)字淳附、小寫字符和大寫字母組成晶渠,但必須至少包括2種字符凰荚。

在匹配位置那篇文章里,我們寫出了正則是:

/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/

其實(shí)可以使用多個(gè)小正則來(lái)做:

var reg1 = /^[0-9A-Za-z]{6,12}$/;
var reg2 = /^[0-9]{6,12}$/;
var reg3 = /^[A-Z]{6,12}$/;
var reg4 = /^[a-z]{6,12}$/;
function checkPassword(str) {
    if (!reg1.test(str)) return false;
    if (reg2.test(str)) return false;
    if (reg3.test(str)) return false;
    if (reg4.test(str)) return false;
    return true;
}
  1. 準(zhǔn)確性

所謂準(zhǔn)確性褒脯,就是能匹配預(yù)期的目標(biāo)便瑟,并且不匹配非預(yù)期的目標(biāo)。

這里提到了“預(yù)期”二字番川,那么我們就需要知道目標(biāo)的組成規(guī)則到涂。

不然沒法界定什么樣的目標(biāo)字符串是符合預(yù)期的,什么樣的又不是符合預(yù)期的颁督。

下面將舉例說(shuō)明践啄,當(dāng)目標(biāo)字符串構(gòu)成比較復(fù)雜時(shí),該如何構(gòu)建正則沉御,并考慮到哪些平衡屿讽。

3.1 匹配固定電話

比如要匹配如下格式的固定電話號(hào)碼:

055188888888 0551-88888888 (0551)88888888
第一步,了解各部分的模式規(guī)則吠裆。

上面的電話伐谈,總體上分為區(qū)號(hào)和號(hào)碼兩部分(不考慮分機(jī)號(hào)和+86的情形)。

區(qū)號(hào)是0開頭的3到4位數(shù)字试疙,對(duì)應(yīng)的正則是:0\d{2,3}

號(hào)碼是非0開頭的7到8位數(shù)字诵棵,對(duì)應(yīng)的正則是:[1-9]\d{6,7}

因此,匹配055188888888的正則是:/^0\d{2,3}[1-9]\d{6,7}$/

匹配0551-88888888的正則是:/^0\d{2,3}-[1-9]\d{6,7}$/

匹配(0551)88888888的正則是:/^(0\d{2,3})[1-9]\d{6,7}$/

第二步祝旷,明確形式關(guān)系履澳。

這三者情形是或的關(guān)系,可以構(gòu)建分支:

/^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/

提取公共部分:

/^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/

進(jìn)一步簡(jiǎn)寫:

/^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/

上面的正則構(gòu)建過程略顯羅嗦怀跛,但是這樣做距贷,能保證正則是準(zhǔn)確的。

上述三種情形是或的關(guān)系吻谋,這一點(diǎn)很重要忠蝗,不然很容易按字符是否出現(xiàn)的情形把正則寫成:

/^(?0\d{2,3})?-?[1-9]\d{6,7}$/
雖然也能匹配上述目標(biāo)字符串,但也會(huì)匹配(0551-88888888這樣的字符串滨溉。當(dāng)然什湘,這不是我們想要的长赞。

其實(shí)這個(gè)正則也不是完美的晦攒,因?yàn)楝F(xiàn)實(shí)中,并不是每個(gè)3位數(shù)和4位數(shù)都是一個(gè)真實(shí)的區(qū)號(hào)得哆。

這就是一個(gè)平衡取舍問題脯颜,一般夠用就行。

3.2 匹配浮點(diǎn)數(shù)

要求匹配如下的格式:

1.23贩据、+1.23栋操、-1.23
10、+10葱绒、-10
.2、+.2作箍、-.2

可以看出正則分為三部分。

符號(hào)部分:[+-]

整數(shù)部分:\d+

小數(shù)部分:.\d+

上述三個(gè)部分牧愁,并不是全部都出現(xiàn)磨确。如果此時(shí)很容易寫出如下的正則:

/^[+-]?(\d+)?(\.\d+)?$/

此正則看似沒問題恨诱,但這個(gè)正則也會(huì)匹配空字符""厕鹃。

因?yàn)槟繕?biāo)字符串的形式關(guān)系不是要求每部分都是可選的秸弛。

要匹配1.23绞铃、+1.23、-1.23,可以用:/^[+-]?\d+.\d+$/

要匹配10奴璃、+10、-10岭辣,可以用/^[+-]?\d+$/

要匹配.2偷遗、+.2、-.2鲜锚,可以用/^[+-]?.\d+$/

因此整個(gè)正則是這三者的或的關(guān)系,提取公眾部分后是:

/^[+-]?(\d+\.\d+|\d+|\.\d+)$/

其可視化形式是:


如果要求不匹配+.2和-.2速妖,此時(shí)正則變成:

當(dāng)然旅择,/^[+-]?(\d+.\d+|\d+|.\d+)$/也不是完美的,我們也是做了些取舍胶果,比如:

  • 它也會(huì)匹配012這樣以0開頭的整數(shù)蕊连。如果要求不匹配的話囚聚,需要修改整數(shù)部分的正則鬼譬。
  • 一般進(jìn)行驗(yàn)證操作之前汗捡,都要經(jīng)過trim和判空淑际。那樣的話,也許那個(gè)錯(cuò)誤正則也就夠用了扇住。
  • 也可以進(jìn)一步改寫成:/^[+-]?(\d+)?(.)?\d+$/春缕,這樣我們就需要考慮可讀性和可維護(hù)性了。
  1. 效率

保證了準(zhǔn)確性后艘蹋,才需要是否要考慮要優(yōu)化锄贼。大多數(shù)情形是不需要優(yōu)化的,除非運(yùn)行的非常慢女阀。什么情形正則表達(dá)式運(yùn)行才慢呢宅荤?我們需要考察正則表達(dá)式的運(yùn)行過程(原理)。

正則表達(dá)式的運(yùn)行分為如下的階段:

編譯
設(shè)定起始位置
嘗試匹配
匹配失敗的話浸策,從下一位開始繼續(xù)第3步
最終結(jié)果:匹配成功或失敗

下面以代碼為例冯键,來(lái)看看這幾個(gè)階段都做了什么:

var regex = /\d+/g;
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
// => 0 ["123", index: 0, input: "123abc34def"]
// => 3 ["34", index: 6, input: "123abc34def"]
// => 8 null
// => 0 ["123", index: 0, input: "123abc34def"]

具體分析如下:

var regex = /\d+/g;
當(dāng)生成一個(gè)正則時(shí),引擎會(huì)對(duì)其進(jìn)行編譯庸汗。報(bào)錯(cuò)與否出現(xiàn)這這個(gè)階段惫确。

regex.exec("123abc34def")
當(dāng)嘗試匹配時(shí),需要確定從哪一位置開始匹配蚯舱。一般情形都是字符串的開頭雕薪,即第0位。

但當(dāng)使用test和exec方法晓淀,且正則有g(shù)時(shí)所袁,起始位置是從正則對(duì)象的lastIndex屬性開始

因此第一次exec是從第0位開始凶掰,而第二次是從3開始的燥爷。

設(shè)定好起始位置后蜈亩,就開始嘗試匹配了。

比如第一次exec前翎,從0開始稚配,去嘗試匹配,并且成功地匹配到3個(gè)數(shù)字港华。此時(shí)結(jié)束時(shí)的下標(biāo)是2道川,因此下一次的起始位置是3。

而第二次立宜,起始下標(biāo)是3冒萄,但第3個(gè)字符是“a”,并不是數(shù)字橙数。但此時(shí)并不會(huì)直接報(bào)匹配失敗尊流,而是移動(dòng)到下一位置,即從第4位開始繼續(xù)嘗試匹配灯帮,但該字符是b崖技,也不是數(shù)字。再移動(dòng)到下一位钟哥,是c仍不行迎献,再移動(dòng)一位是數(shù)字3,此時(shí)匹配到了兩位數(shù)字34腻贰。此時(shí)忿晕,下一次匹配的位置是d的位置,即第8位银受。

第三次践盼,是從第8位開始匹配,直到試到最后一位宾巍,也沒發(fā)現(xiàn)匹配的咕幻,因此匹配失敗,返回null顶霞。同時(shí)設(shè)置lastIndex為0肄程,即,如要再嘗試匹配的話选浑,需從頭開始蓝厌。

從上面可以看出,匹配會(huì)出現(xiàn)效率問題古徒,主要出現(xiàn)在上面的第3階段和第4階段拓提。

因此,主要優(yōu)化手法也是針對(duì)這兩階段的隧膘。

4.1 使用具體型字符組來(lái)代替通配符代态,來(lái)消除回溯

而在第三階段寺惫,最大的問題就是回溯(定義辛润,請(qǐng)參考《回溯法原理》)挠铲。

例如,匹配雙引用號(hào)之間的字符代虾。如歉摧,匹配字符串123"abc"456中的"abc"艇肴。

如果正則用的是:/"."/,叁温,會(huì)在第3階段產(chǎn)生4次回溯(粉色表示.匹配的內(nèi)容):

如果正則用的是:/".?"/再悼,會(huì)產(chǎn)生2次回溯(粉色表示.?匹配的內(nèi)容):

因?yàn)榛厮莸拇嬖冢枰姹4娑喾N可能中未嘗試過的狀態(tài)券盅,以便后續(xù)回溯時(shí)使用帮哈。注定要占用一定的內(nèi)存膛檀。

此時(shí)要使用具體化的字符組锰镀,來(lái)代替通配符.,以便消除不必要的字符咖刃,此時(shí)使用正則/"[^"]*"/泳炉,即可。

4.2 使用非捕獲型分組

因?yàn)槔ㄌ?hào)的作用之一是嚎杨,可以捕獲分組和分支里的數(shù)據(jù)花鹅。那么就需要內(nèi)存來(lái)保存它們。

當(dāng)我們不需要使用分組引用和反向引用時(shí)枫浙,此時(shí)可以使用非捕獲分組刨肃。例如:

/^[+-]?(\d+.\d+|\d+|.\d+)$/
可以修改成:

/^[+-]?(?:\d+.\d+|\d+|.\d+)$/
4.3 獨(dú)立出確定字符

例如/a+/,可以修改成/aa*/箩帚。

因?yàn)楹笳吣鼙惹罢叨啻_定了字符a真友。這樣會(huì)在第四步中,加快判斷是否匹配失敗紧帕,進(jìn)而加快移位的速度盔然。

4.4 提取分支公共部分

比如/abc|def/,修改成/^(?:abc|def)/是嗜。

又比如/this|that/愈案,修改成/th(?:is|at)/。

這樣做鹅搪,可以減少匹配過程中可消除的重復(fù)站绪。

4.5 減少分支的數(shù)量,縮小它們的范圍

/red|read/丽柿,可以修改成/rea?d/崇众。此時(shí)分支和量詞產(chǎn)生的回溯的成本是不一樣的掂僵。但這樣優(yōu)化后,可讀性會(huì)降低的

一般情況下顷歌,針對(duì)某問題能寫出一個(gè)滿足需求的正則锰蓬,基本上就可以了。

至于準(zhǔn)確性和效率方面的追求眯漩,純屬看個(gè)人要求了芹扭。我覺得夠用就行了。

關(guān)于準(zhǔn)確性赦抖,本文關(guān)心的是最常用的解決思路:

針對(duì)每種情形舱卡,分別寫出正則,然用分支把他們合并在一起队萤,再提取分支公共部分轮锥,就能得到準(zhǔn)確的正則。

至于優(yōu)化要尔,本文沒有為了湊數(shù)舍杜,去寫一大堆。了解了匹配原理赵辕,常見的優(yōu)化手法也就這么幾種既绩。

正則表達(dá)式編程

如何使用正則表達(dá)式呢?有哪些關(guān)鍵的點(diǎn)呢还惠?本文就解決這個(gè)問題饲握。

內(nèi)容包括:

  1. 正則表達(dá)式的四種操作

  2. 相關(guān)API注意要點(diǎn)

  3. 真實(shí)案例

  4. 正則表達(dá)式的四種操作

正則表達(dá)式是匹配模式,不管如何使用正則表達(dá)式蚕键,萬(wàn)變不離其宗救欧,都需要先“匹配”。

有了匹配這一基本操作后锣光,才有其他的操作:驗(yàn)證笆怠、切分、提取嫉晶、替換骑疆。

進(jìn)行任何相關(guān)操作,也需要宿主引擎相關(guān)API的配合使用替废。當(dāng)然箍铭,在JS中,相關(guān)API也不多椎镣。

1.1 驗(yàn)證

驗(yàn)證是正則表達(dá)式最直接的應(yīng)用诈火,比如表單驗(yàn)證。

在說(shuō)驗(yàn)證之前状答,先要說(shuō)清楚匹配是什么概念冷守。

所謂匹配刀崖,就是看目標(biāo)字符串里是否有滿足匹配的子串。因此拍摇,“匹配”的本質(zhì)就是“查找”亮钦。

有沒有匹配,是不是匹配上充活,判斷是否的操作蜂莉,即稱為“驗(yàn)證”。

這里舉一個(gè)例子混卵,來(lái)看看如何使用相關(guān)API進(jìn)行驗(yàn)證操作的映穗。

比如,判斷一個(gè)字符串中是否有數(shù)字幕随。

使用search

var reg = /\d/;
var str = "abc123";
console.log( !!~str.search(reg) );
// => true

使用test

var reg = /\d/;
var str = "abc123";
console.log( reg.test(str) );
// => true

使用match

var reg = /\d/;
var str = "abc123";
console.log( !!str.match(reg) );
// => true

使用exec

var reg = /\d/;
var str = "abc123";
console.log( !!reg.exec(str) );
// => true

1.2 切分

匹配上了蚁滋,我們就可以進(jìn)行一些操作,比如切分赘淮。

所謂“切分”辕录,就是把目標(biāo)字符串,切成一段一段的拥知。在JS中使用的是split踏拜。

比如碎赢,目標(biāo)字符串是"html,css,javascript"低剔,按逗號(hào)來(lái)切分:

var reg = /,/;
var str = "html,css,javascript";
console.log( str.split(reg) );
// => ["html", "css", "javascript"]

又比如,如下的日期格式:

2017/06/26
2017.06.26
2017-06-26

可以使用split“切出”年月日:

var reg = /\D/;
console.log( "2017/06/26".split(reg) );
console.log( "2017.06.26".split(reg) );
console.log( "2017-06-26".split(reg) );
// => ["2017", "06", "26"]
// => ["2017", "06", "26"]
// => ["2017", "06", "26"]

1.3 提取

雖然整體匹配上了肮塞,但有時(shí)需要提取部分匹配的數(shù)據(jù)襟齿。

此時(shí)正則通常要使用分組引用(分組捕獲)功能,還需要配合使用相關(guān)API枕赵。

這里猜欺,還是以日期為例,提取出年月日拷窜。注意下面正則中的括號(hào):

match

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
console.log( string.match(regex) );
// =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]

exec
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
console.log( regex.exec(string) );
// =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
test

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
regex.test(string);
console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
// => "2017" "06" "26"

search

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
string.search(regex);
console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
// => "2017" "06" "26"

replace

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
var date = [];
string.replace(regex, function(match, year, month, day) {
    date.push(year, month, day);
});
console.log(date);
// => ["2017", "06", "26"]

1.4 替換

找开皿,往往不是目的,通常下一步是為了替換篮昧。在JS中赋荆,使用replace進(jìn)行替換。

比如把日期格式懊昨,從yyyy-mm-dd替換成yyyy/mm/dd:

var string = "2017-06-26";
var today = new Date( string.replace(/-/g, "/") );
console.log( today );
// => Mon Jun 26 2017 00:00:00 GMT+0800 (中國(guó)標(biāo)準(zhǔn)時(shí)間)

這里只是簡(jiǎn)單地應(yīng)用了一下replace窄潭。但,replace方法是強(qiáng)大的酵颁,是需要重點(diǎn)掌握的嫉你。

  1. 相關(guān)API注意要點(diǎn)

從上面可以看出用于正則操作的方法月帝,共有6個(gè),字符串實(shí)例4個(gè)幽污,正則實(shí)例2個(gè):

String#search
String#split
String#match
String#replace
RegExp#test
RegExp#exec

2.1 search和match的參數(shù)問題

我們知道字符串實(shí)例的那4個(gè)方法參數(shù)都支持正則和字符串嚷辅。

但search和match,會(huì)把字符串轉(zhuǎn)換為正則的距误。

var string = "2017.06.27";

console.log( string.search(".") );
// => 0
//需要修改成下列形式之一
console.log( string.search("\\.") );
console.log( string.search(/\./) );
// => 4
// => 4

console.log( string.match(".") );
// => ["2", index: 0, input: "2017.06.27"]
//需要修改成下列形式之一
console.log( string.match("\\.") );
console.log( string.match(/\./) );
// => [".", index: 4, input: "2017.06.27"]
// => [".", index: 4, input: "2017.06.27"]

console.log( string.split(".") );
// => ["2017", "06", "27"]

console.log( string.replace(".", "/") );
// => "2017/06.27"

2.2 match返回結(jié)果的格式問題

match返回結(jié)果的格式潦蝇,與正則對(duì)象是否有修飾符g有關(guān)。

var string = "2017.06.27";
var regex1 = /\b(\d+)\b/;
var regex2 = /\b(\d+)\b/g;
console.log( string.match(regex1) );
console.log( string.match(regex2) );
// => ["2017", "2017", index: 0, input: "2017.06.27"]
// => ["2017", "06", "27"]

沒有g(shù)深寥,返回的是標(biāo)準(zhǔn)匹配格式攘乒,即,數(shù)組的第一個(gè)元素是整體匹配的內(nèi)容惋鹅,接下來(lái)是分組捕獲的內(nèi)容则酝,然后是整體匹配的第一個(gè)下標(biāo),最后是輸入的目標(biāo)字符串闰集。

有g(shù)沽讹,返回的是所有匹配的內(nèi)容。

當(dāng)沒有匹配時(shí)武鲁,不管有無(wú)g爽雄,都返回null。
2.3 exec比match更強(qiáng)大

當(dāng)正則沒有g(shù)時(shí)沐鼠,使用match返回的信息比較多挚瘟。但是有g(shù)后,就沒有關(guān)鍵的信息index了饲梭。

而exec方法就能解決這個(gè)問題乘盖,它能接著上一次匹配后繼續(xù)匹配:

var string = "2017.06.27";
var regex2 = /\b(\d+)\b/g;
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
// => ["2017", "2017", index: 0, input: "2017.06.27"]
// => 4
// => ["06", "06", index: 5, input: "2017.06.27"]
// => 7
// => ["27", "27", index: 8, input: "2017.06.27"]
// => 10
// => null
// => 0

其中正則實(shí)例lastIndex屬性,表示下一次匹配開始的位置憔涉。

比如第一次匹配了“2017”订框,開始下標(biāo)是0,共4個(gè)字符兜叨,因此這次匹配結(jié)束的位置是3穿扳,下一次開始匹配的位置是4。

從上述代碼看出国旷,在使用exec時(shí)矛物,經(jīng)常需要配合使用while循環(huán):

var string = "2017.06.27";
var regex2 = /\b(\d+)\b/g;
var result;
while ( result = regex2.exec(string) ) {
    console.log( result, regex2.lastIndex );
}
// => ["2017", "2017", index: 0, input: "2017.06.27"] 4
// => ["06", "06", index: 5, input: "2017.06.27"] 7
// => ["27", "27", index: 8, input: "2017.06.27"] 10

2.4 修飾符g,對(duì)exex和test的影響

上面提到了正則實(shí)例的lastIndex屬性议街,表示嘗試匹配時(shí)泽谨,從字符串的lastIndex位開始去匹配。

字符串的四個(gè)方法,每次匹配時(shí)吧雹,都是從0開始的骨杂,即lastIndex屬性始終不變。

而正則實(shí)例的兩個(gè)方法exec雄卷、test搓蚪,當(dāng)正則是全局匹配時(shí),每一次匹配完成后丁鹉,都會(huì)修改lastIndex妒潭。下面讓我們以test為例,看看你是否會(huì)迷糊:

var regex = /a/g;
console.log( regex.test("a"), regex.lastIndex );
console.log( regex.test("aba"), regex.lastIndex );
console.log( regex.test("ababc"), regex.lastIndex );
// => true 1
// => true 3
// => false 0

注意上面代碼中的第三次調(diào)用test揣钦,因?yàn)檫@一次嘗試匹配雳灾,開始從下標(biāo)lastIndex即3位置處開始查找,自然就找不到了冯凹。

如果沒有g(shù)谎亩,自然都是從字符串第0個(gè)字符處開始嘗試匹配:

var regex = /a/;
console.log( regex.test("a"), regex.lastIndex );
console.log( regex.test("aba"), regex.lastIndex );
console.log( regex.test("ababc"), regex.lastIndex );
// => true 0
// => true 0
// => true 0

2.5 test整體匹配時(shí)需要使用^和$

這個(gè)相對(duì)容易理解,因?yàn)閠est是看目標(biāo)字符串中是否有子串匹配正則宇姚,即有部分匹配即可匈庭。

如果,要整體匹配浑劳,正則前后需要添加開頭和結(jié)尾:

console.log( /123/.test("a123b") );
// => true
console.log( /^123$/.test("a123b") );
// => false
console.log( /^123$/.test("123") );
// => true

2.6 split相關(guān)注意事項(xiàng)

split方法看起來(lái)不起眼阱持,但要注意的地方有兩個(gè)的。

第一魔熏,它可以有第二個(gè)參數(shù)衷咽,表示結(jié)果數(shù)組的最大長(zhǎng)度:

var string = "html,css,javascript";
console.log( string.split(/,/, 2) );
// =>["html", "css"]

第二,正則使用分組時(shí)道逗,結(jié)果數(shù)組中是包含分隔符的:

var string = "html,css,javascript";
console.log( string.split(/(,)/) );
// =>["html", ",", "css", ",", "javascript"]

2.7 replace是很強(qiáng)大的

《JavaScript權(quán)威指南》認(rèn)為exec是這6個(gè)API中最強(qiáng)大的兵罢,而我始終認(rèn)為replace才是最強(qiáng)大的献烦。因?yàn)樗材苣玫皆撃玫降男畔⒆仪希缓罂梢约俳杼鎿Q之名,做些其他事情巩那。

總體來(lái)說(shuō)replace有兩種使用形式吏夯,這是因?yàn)樗牡诙€(gè)參數(shù),可以是字符串即横,也可以是函數(shù)噪生。

當(dāng)?shù)诙€(gè)參數(shù)是字符串時(shí),如下的字符有特殊的含義:

$1,$2,...,$99 匹配第1~99個(gè)分組里捕獲的文本
$& 匹配到的子串
$` 匹配到的子串的左邊文本
$' 匹配到的子串的右邊文本
$$ 美元符號(hào)

例如东囚,把"2,3,5"跺嗽,變成"5=2+3":

var result = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2");
console.log(result);
// => "5=2+3"

又例如,把"2,3,5",變成"222,333,555":

var result = "2,3,5".replace(/\d+/g, "$&$&$&");
console.log(result);
// => "222,333,555"

再例如桨嫁,把"2+3=5"植兰,變成"2+3=2+3=5=5":

var result = "2+3=5".replace(/=/, "$&$`$&$'$&");
console.log(result);
// => "2+3=2+3=5=5"

當(dāng)?shù)诙€(gè)參數(shù)是函數(shù)時(shí),我們需要注意該回調(diào)函數(shù)的參數(shù)具體是什么:

"1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function(match, $1, $2, index, input) {
    console.log([match, $1, $2, index, input]);
});
// => ["1234", "1", "4", 0, "1234 2345 3456"]
// => ["2345", "2", "5", 5, "1234 2345 3456"]
// => ["3456", "3", "6", 10, "1234 2345 3456"]

此時(shí)我們可以看到replace拿到的信息璃吧,并不比exec少

2.8 使用構(gòu)造函數(shù)需要注意的問題

一般不推薦使用構(gòu)造函數(shù)生成正則楣导,而應(yīng)該優(yōu)先使用字面量。因?yàn)橛脴?gòu)造函數(shù)會(huì)多寫很多“\”畜挨。

var string = "2017-06-27 2017.06.27 2017/06/27";
var regex = /\d{4}(-|\.|\/)\d{2}\1\d{2}/g;
console.log( string.match(regex) );
// => ["2017-06-27", "2017.06.27", "2017/06/27"]

regex = new RegExp("\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}", "g");
console.log( string.match(regex) );
// => ["2017-06-27", "2017.06.27", "2017/06/27"]

2.9 修飾符

ES5中修飾符筒繁,共3個(gè):

g 全局匹配,即找到所有匹配的巴元,單詞是global
i 忽略字母大小寫毡咏,單詞ingoreCase
m 多行匹配,只影響^和$逮刨,二者變成行的概念血当,即行開頭和行結(jié)尾。單詞是multiline

當(dāng)然正則對(duì)象也有相應(yīng)的只讀屬性:

var regex = /\w/img;
console.log( regex.global );
console.log( regex.ignoreCase );
console.log( regex.multiline );
// => true
// => true
// => true

2.10 source屬性

正則實(shí)例對(duì)象屬性禀忆,除了global臊旭、ingnoreCase、multiline箩退、lastIndex屬性之外离熏,還有一個(gè)source屬性。

它什么時(shí)候有用呢戴涝?

比如滋戳,在構(gòu)建動(dòng)態(tài)的正則表達(dá)式時(shí),可以通過查看該屬性啥刻,來(lái)確認(rèn)構(gòu)建出的正則到底是什么:

var className = "high";
var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
console.log( regex.source )
// => (^|\s)high(\s|$) 即字符串"(^|\\s)high(\\s|$)"

2.11 構(gòu)造函數(shù)屬性

構(gòu)造函數(shù)的靜態(tài)屬性基于所執(zhí)行的最近一次正則操作而變化奸鸯。除了是$1,...,$9之外,還有幾個(gè)不太常用的屬性(有兼容性問題):

RegExp.input 最近一次目標(biāo)字符串可帽,簡(jiǎn)寫成RegExp["$_"]
RegExp.lastMatch 最近一次匹配的文本娄涩,簡(jiǎn)寫成RegExp["$&"]
RegExp.lastParen 最近一次捕獲的文本,簡(jiǎn)寫成RegExp["$+"]
RegExp.leftContext 目標(biāo)字符串中l(wèi)astMatch之前的文本映跟,簡(jiǎn)寫成RegExp["$`"]
RegExp.rightContext 目標(biāo)字符串中l(wèi)astMatch之后的文本蓄拣,簡(jiǎn)寫成RegExp["$'"]

測(cè)試代碼如下:

var regex = /([abc])(\d)/g;
var string = "a1b2c3d4e5";
string.match(regex);

console.log( RegExp.input );
console.log( RegExp["$_"]);
// => "a1b2c3d4e5"

console.log( RegExp.lastMatch );
console.log( RegExp["$&"] );
// => "c3"

console.log( RegExp.lastParen );
console.log( RegExp["$+"] );
// => "3"

console.log( RegExp.leftContext );
console.log( RegExp["$`"] );
// => "a1b2"

console.log( RegExp.rightContext );
console.log( RegExp["$'"] );
// => "d4e5"
  1. 真實(shí)案例

3.1 使用構(gòu)造函數(shù)生成正則表達(dá)式

我們知道要優(yōu)先使用字面量來(lái)創(chuàng)建正則,但有時(shí)正則表達(dá)式的主體是不確定的努隙,此時(shí)可以使用構(gòu)造函數(shù)來(lái)創(chuàng)建球恤。模擬getElementsByClassName方法,就是很能說(shuō)明該問題的一個(gè)例子荸镊。

這里getElementsByClassName函數(shù)的實(shí)現(xiàn)思路是:

比如要獲取className為"high"的dom元素咽斧;
首先生成一個(gè)正則:/(^|\s)high(\s|$)/堪置;
然后再用其逐一驗(yàn)證頁(yè)面上的所有dom元素的類名,拿到滿足匹配的元素即可张惹。

<p class="high">1111</p>
<p class="high">2222</p>
<p>3333</p>
<script>
function getElementsByClassName(className) {
    var elements = document.getElementsByTagName("*");
    var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
    var result = [];
    for (var i = 0; i < elements.length; i++) {
        var element = elements[i];
        if (regex.test(element.className)) {
            result.push(element)
        }
    }
    return result;
}
var highs = getElementsByClassName('high');
highs.forEach(function(item) {
    item.style.color = 'red';
});
</script>

3.2 使用字符串保存數(shù)據(jù)

一般情況下晋柱,我們都愿意使用數(shù)組來(lái)保存數(shù)據(jù)。但我看到有的框架中诵叁,使用的卻是字符串雁竞。

使用時(shí),仍需要把字符串切分成數(shù)組拧额。雖然不一定用到正則碑诉,但總感覺酷酷的,這里分享如下:

var utils = {};
"Boolean|Number|String|Function|Array|Date|RegExp|Object|Error".split("|").forEach(function(item) {
    utils["is" + item] = function(obj) {
        return {}.toString.call(obj) == "[object " + item + "]";
    };
});
console.log( utils.isArray([1, 2, 3]) );
// => true

3.3 if語(yǔ)句中使用正則替代&&

比如侥锦,模擬ready函數(shù)进栽,即加載完畢后再執(zhí)行回調(diào)(不兼容ie的):

var readyRE = /complete|loaded|interactive/;

function ready(callback) {
    if (readyRE.test(document.readyState) && document.body) {
        callback()
    } 
    else {
        document.addEventListener(
            'DOMContentLoaded', 
            function () {
                callback()
            },
            false
        );
    }
};
ready(function() {
    alert("加載完畢!")
});

3.4 使用強(qiáng)大的replace

因?yàn)閞eplace方法比較強(qiáng)大恭垦,有時(shí)用它根本不是為了替換快毛,只是拿其匹配到的信息來(lái)做文章。

這里以查詢字符串(querystring)壓縮技術(shù)為例番挺,注意下面replace方法中唠帝,回調(diào)函數(shù)根本沒有返回任何東西。

function compress(source) {
    var keys = {};
    source.replace(/([^=&]+)=([^&]*)/g, function(full, key, value) {
        keys[key] = (keys[key] ? keys[key] + ',' : '') + value;
    });
    var result = [];
    for (var key in keys) {
        result.push(key + '=' + keys[key]);
    }
    return result.join('&');
}

console.log( compress("a=1&b=2&a=3&b=4") );
// => "a=1,3&b=2,4"

最后這里再做個(gè)簡(jiǎn)單使用的正則測(cè)試器玄柏。

<section>
    <div id="err"></div>
    <input id="regex" placeholder="請(qǐng)輸入正則表達(dá)式">
    <input id="text" placeholder="請(qǐng)輸入測(cè)試文本">
    <button id="run">測(cè)試一下</button>
    <div id="result"></div>
</section>
<style>
section{
    display:flex;
    flex-direction:column;
    justify-content:space-around;
    height:300px;
    padding:0 200px;
}
section *{
    min-height:30px;
}
#err {
    color:red;
}
#result{
    line-height:30px;
}
.info {
    background:#00c5ff;
    padding:2px;
    margin:2px;
    display:inline-block;
}
</style>
<script>
(function() {
    // 獲取相應(yīng)dom元素
    var regexInput = document.getElementById("regex");
    var textInput = document.getElementById("text");
    var runBtn = document.getElementById("run");
    var errBox = document.getElementById("err");
    var resultBox = document.getElementById("result");
    
    // 綁定點(diǎn)擊事件
    runBtn.onclick = function() {
        // 清除錯(cuò)誤和結(jié)果
        errBox.innerHTML = "";
        resultBox.innerHTML = "";
        
        // 獲取正則和文本
        var text = textInput.value;
        var regex = regexInput.value;
        
        if (regex == "") {
            errBox.innerHTML = "請(qǐng)輸入正則表達(dá)式";
        } else if (text == "") {
            errBox.innerHTML = "請(qǐng)輸入測(cè)試文本";
        } else {
            regex = createRegex(regex);
            if (!regex) return;
            var result, results = [];
            
            // 沒有修飾符g的話襟衰,會(huì)死循環(huán)
            if (regex.global) {
                while(result = regex.exec(text)) {
                    results.push(result);
                }
            } else {
                results.push(regex.exec(text));
            }
                        
            if (results[0] == null) {
                resultBox.innerHTML = "匹配到0個(gè)結(jié)果";
                return;
            }
            
            // 倒序是有必要的
            for (var i = results.length - 1; i >= 0; i--) {
                var result = results[i];
                var match = result[0];
                var prefix = text.substr(0, result.index);
                var suffix = text.substr(result.index + match.length);
                text = prefix 
                    + '<span class="info">'
                    + match
                    + '</span>'
                    + suffix;
            }
            resultBox.innerHTML = "匹配到" + results.length + "個(gè)結(jié)果:<br>" + text;
        }
    };
    
    // 生成正則表達(dá)式,核心函數(shù)
    function createRegex(regex) {
        try {
            if (regex[0] == "/") {
                regex = regex.split("/");
                regex.shift();
                var flags = regex.pop();
                regex = regex.join("/");
                regex = new RegExp(regex, flags);
            } else {
                regex = new RegExp(regex, "g");
            }
            return regex;
        } catch(e) {
            errBox.innerHTML = "無(wú)效的正則表達(dá)式";
            return false;
        }
    }
})();
</script>

實(shí)現(xiàn)一個(gè)模板引擎:

var TemplateEngine = function(html, options) {
    var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', cursor = 0;
    var add = function(line, js) {
        js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
            (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
        return add;
    }
    while(match = re.exec(html)) {
        add(html.slice(cursor, match.index))(match[1], true);
        cursor = match.index + match[0].length;
    }
    add(html.substr(cursor, html.length - cursor));
    code += 'return r.join("");';
    return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);
}
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.age%> years old.</p>';
console.log(TemplateEngine(template, {
    name: "Krasimir",
    age: 29
}));

以下是參考鏈接:
js正則表達(dá)式火拼系列 - 老姚

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末粪摘,一起剝皮案震驚了整個(gè)濱河市瀑晒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌徘意,老刑警劉巖苔悦,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異椎咧,居然都是意外死亡玖详,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門邑退,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)竹宋,“玉大人,你說(shuō)我怎么就攤上這事地技。” “怎么了秒拔?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵莫矗,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng)作谚,這世上最難降的妖魔是什么三娩? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮妹懒,結(jié)果婚禮上雀监,老公的妹妹穿的比我還像新娘。我一直安慰自己眨唬,他們只是感情好会前,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著匾竿,像睡著了一般瓦宜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上岭妖,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天临庇,我揣著相機(jī)與錄音,去河邊找鬼昵慌。 笑死假夺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的斋攀。 我是一名探鬼主播侄泽,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蜻韭!你這毒婦竟也來(lái)了悼尾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤肖方,失蹤者是張志新(化名)和其女友劉穎闺魏,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俯画,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡析桥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了艰垂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泡仗。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖猜憎,靈堂內(nèi)的尸體忽然破棺而出娩怎,到底是詐尸還是另有隱情,我是刑警寧澤胰柑,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布截亦,位于F島的核電站爬泥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏崩瓤。R本人自食惡果不足惜袍啡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望却桶。 院中可真熱鬧境输,春花似錦、人聲如沸颖系。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)集晚。三九已至窗悯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間偷拔,已是汗流浹背蒋院。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留莲绰,地道東北人欺旧。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蛤签,于是被迫代替她去往敵國(guó)和親辞友。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容