初衷:看了很多視頻、文章东涡,最后卻通通忘記了冯吓,別人的知識(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)案例
- 不匹配任何東西的正則
/.^/
此正則要求只有一個(gè)字符,但該字符后面是開頭晚顷。
- 數(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á)式的所有可能
- 引用分組
這是括號(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)案例:
- 字符串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"
- 將每個(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)然這里不使用非捕獲匹配也是可以的
- 駝峰化
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 '
- 中劃線化
function dasherize(str) {
return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
}
console.log( dasherize('MozTransform') ); // -moz-transform
- 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ì)象的鍵
- 匹配成對(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á)式的原理-回溯
-
沒有回溯的匹配
假設(shè)我們的正則是/ab{1,3}c/传睹,其可視化形式是:
而當(dāng)目標(biāo)字符串是"abbbc"時(shí),就沒有所謂的“回溯”岸晦。其匹配過程是:
其中子表達(dá)式b{1,3}表示“b”字符連續(xù)出現(xiàn)1到3次欧啤。
-
有回溯的匹配
如果目標(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)失敗的過程⊥Ы伲可以看出“.*”是非常影響效率的吼驶。
為了減少一些不必要的回溯,可以把正則修改為/"[^"]*"/店煞。
- 常見的回溯形式
正則表達(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)整體匹配不成功唆貌。
貪婪量詞“試”的策略是:買衣服砍價(jià)。價(jià)錢太高了垢乙,便宜點(diǎn)锨咙,不行,再便宜點(diǎn)追逮。
惰性量詞“試”的策略是:賣東西加價(jià)酪刀。給少了,再多給點(diǎn)行不钮孵,還有點(diǎn)少啊骂倘,再給點(diǎn)。
分支結(jié)構(gòu)“試”的策略是:貨比三家巴席。這家不行历涝,換一家吧,還不行漾唉,再換荧库。
既然有回溯的過程,那么匹配效率肯定低一些毡证。相對(duì)誰(shuí)呢电爹?相對(duì)那些DFA引擎。
而JS的正則引擎是NFA料睛,NFA是“非確定型有限自動(dòng)機(jī)”的簡(jiǎn)寫丐箩。
大部分語(yǔ)言中的正則都是NFA,為啥它這么流行呢恤煞?
答:你別看我匹配慢屎勘,但是我編譯快啊,而且我還有趣哦居扒。
正則表達(dá)式的拆分(讀)
- 結(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這兩部分史汗。
- 注意要點(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è),要匹配這樣的字符串:
每個(gè)字符為a横堡、b埋市、c任選其一
字符串的長(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)義的琉历。
- 案例分析
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. 效率
平衡法則
構(gòu)建正則有一點(diǎn)非常重要蚤霞,需要做到下面幾點(diǎn)的平衡:匹配預(yù)期的字符串
不匹配非預(yù)期的字符串
可讀性和可維護(hù)性
效率
構(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;
}
- 準(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ù)性了。
- 效率
保證了準(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)容包括:
正則表達(dá)式的四種操作
相關(guān)API注意要點(diǎn)
真實(shí)案例
正則表達(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)掌握的嫉你。
- 相關(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"
- 真實(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á)式火拼系列 - 老姚