九遭贸、正則表達(dá)式
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
一些人遇到問(wèn)題時(shí)會(huì)認(rèn)為吱肌,“我知道了纲缓,我會(huì)用正則表達(dá)式卷拘。”現(xiàn)在它們有兩個(gè)問(wèn)題了祝高。
Jamie Zawinski
Yuan-Ma said, 'When you cut against the grain of the wood, much strength is needed. When you program against the grain of the problem, much code is needed.'
Master Yuan-Ma栗弟,《The Book of Programming》
https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/9-0.jpg
程序設(shè)計(jì)工具技術(shù)的發(fā)展與傳播方式是在混亂中不斷進(jìn)化。在此過(guò)程中獲勝的往往不是優(yōu)雅或杰出的一方工闺,而是那些瞄準(zhǔn)主流市場(chǎng)乍赫,并能夠填補(bǔ)市場(chǎng)需求的,或者碰巧與另一種成功的技術(shù)集成在一起的工具技術(shù)陆蟆。
本章將會(huì)討論正則表達(dá)式(regular expression)這種工具雷厂。正則表達(dá)式是一種描述字符串?dāng)?shù)據(jù)模式的方法。它們形成了一種小而獨(dú)立的語(yǔ)言叠殷,也是 JavaScript 和許多其他語(yǔ)言和系統(tǒng)的一部分改鲫。
正則表達(dá)式雖然不易理解,但是功能非常強(qiáng)大林束。正則表達(dá)式的語(yǔ)法有點(diǎn)詭異像棘,JavaScript 提供的程序設(shè)計(jì)接口也不太易用。但正則表達(dá)式的確是檢查壶冒、處理字符串的強(qiáng)力工具缕题。如果讀者能夠正確理解正則表達(dá)式,將會(huì)成為更高效的程序員胖腾。
創(chuàng)建正則表達(dá)式
正則表達(dá)式是一種對(duì)象類(lèi)型烟零。我們可以使用兩種方法來(lái)構(gòu)造正則表達(dá)式:一是使用RegExp
構(gòu)造器構(gòu)造一個(gè)正則表達(dá)式對(duì)象;二是使用斜杠(/
)字符將模式包圍起來(lái)咸作,生成一個(gè)字面值锨阿。
let re1 = new RegExp("abc");
let re2 = /abc/;
這兩個(gè)正則表達(dá)式對(duì)象都表示相同的模式:字符a
后緊跟一個(gè)b
,接著緊跟一個(gè)c
记罚。
使用RegExp
構(gòu)造器時(shí)群井,需要將模式書(shū)寫(xiě)成普通的字符串,因此反斜杠的使用規(guī)則與往常相同毫胜。
第二種寫(xiě)法將模式寫(xiě)在斜杠之間书斜,處理反斜杠的方式與第一種方法略有差別。首先酵使,由于斜杠會(huì)結(jié)束整個(gè)模式荐吉,因此模式中包含斜杠時(shí),需在斜杠前加上反斜杠口渔。此外样屠,如果反斜杠不是特殊字符代碼(比如\n
)的一部分,則會(huì)保留反斜杠,不像字符串中會(huì)將其忽略痪欲,也不會(huì)改變模式的含義悦穿。一些字符,比如問(wèn)號(hào)业踢、加號(hào)在正則表達(dá)式中有特殊含義栗柒,如果你想要表示其字符本身,需要在字符前加上反斜杠知举。
let eighteenPlus = /eighteen\+/;
匹配測(cè)試
正則表達(dá)式對(duì)象有許多方法瞬沦。其中最簡(jiǎn)單的就是test
方法。test
方法接受用戶(hù)傳遞的字符串雇锡,并返回一個(gè)布爾值逛钻,表示字符串中是否包含能與表達(dá)式模式匹配的字符串。
console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false
不包含特殊字符的正則表達(dá)式簡(jiǎn)單地表示一個(gè)字符序列锰提。如果使用test
測(cè)試字符串時(shí)曙痘,字符串中某處出現(xiàn)abc
(不一定在開(kāi)頭),則返回true
立肘。
字符集
我們也可調(diào)用indexOf
來(lái)找出字符串中是否包含abc
屡江。正則表達(dá)式允許我們表達(dá)一些更復(fù)雜的模式。
假如我們想匹配任意數(shù)字赛不。在正則表達(dá)式中,我們可以將一組字符放在兩個(gè)方括號(hào)之間罢洲,該表達(dá)式可以匹配方括號(hào)中的任意字符踢故。
下面兩個(gè)表達(dá)式都可以匹配包含數(shù)字的字符串。
console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true
我們可以在方括號(hào)中的兩個(gè)字符間插入連字符(–
)惹苗,來(lái)指定一個(gè)字符范圍殿较,范圍內(nèi)的字符順序由字符 Unicode 代碼決定。在 Unicode 字符順序中桩蓉,0 到 9 是從左到右彼此相鄰的(代碼從48到57)淋纲,因此[0-9]
覆蓋了這一范圍內(nèi)的所有字符,也就是說(shuō)可以匹配任意數(shù)字院究。
許多常見(jiàn)字符組都有自己的內(nèi)置簡(jiǎn)寫(xiě)洽瞬。 數(shù)字就是其中之一:\ d
與[0-9]
表示相同的東西。
\d
任意數(shù)字符號(hào)\w
字母和數(shù)字符號(hào)(單詞符號(hào))\s
任意空白符號(hào)(空格业汰,制表符伙窃,換行符等類(lèi)似符號(hào))\D
非數(shù)字符號(hào)\W
非字母和數(shù)字符號(hào)\S
非空白符號(hào).
除了換行符以外的任意符號(hào)
因此你可以使用下面的表達(dá)式匹配類(lèi)似于30-01-2003 15:20
這樣的日期數(shù)字格式:
let dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;
console.log(dateTime.test("30-01-2003 15:20"));
// → true
console.log(dateTime.test("30-jan-2003 15:20"));
// → false
這個(gè)表達(dá)式看起來(lái)是不是非常糟糕?該表達(dá)式中一半都是反斜杠样漆,影響讀者的理解为障,使得讀者難以揣摩表達(dá)式實(shí)際想要表達(dá)的模式。稍后我們會(huì)看到一個(gè)稍加改進(jìn)的版本。
我們也可以將這些反斜杠代碼用在方括號(hào)中鳍怨。例如呻右,[\d.]
匹配任意數(shù)字或一個(gè)句號(hào)。但是方括號(hào)中的句號(hào)會(huì)失去其特殊含義鞋喇。其他特殊字符也是如此声滥,比如+
。
你可以在左方括號(hào)后添加脫字符(^
)來(lái)排除某個(gè)字符集确徙,即表示不匹配這組字符中的任何字符醒串。
let notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110"));
// → false
console.log(notBinary.test("1100100010200110"));
// → true
部分模式重復(fù)
現(xiàn)在我們已經(jīng)知道如何匹配一個(gè)數(shù)字。如果我們想匹配一個(gè)整數(shù)(一個(gè)或多個(gè)數(shù)字的序列)鄙皇,該如何處理呢芜赌?
在正則表達(dá)式某個(gè)元素后面添加一個(gè)加號(hào)(+
),表示該元素至少重復(fù)一次伴逸。因此/\d+/
可以匹配一個(gè)或多個(gè)數(shù)字字符缠沈。
console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("''"));
// → false
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("''"));
// → true
星號(hào)(*
)擁有類(lèi)似含義,但是可以匹配模式不存在的情況错蝴。在正則表達(dá)式的元素后添加星號(hào)并不會(huì)導(dǎo)致正則表達(dá)式停止匹配該元素后面的字符洲愤。只有正則表達(dá)式無(wú)法找到可以匹配的文本時(shí)才會(huì)考慮匹配該元素從未出現(xiàn)的情況。
元素后面跟一個(gè)問(wèn)號(hào)表示這部分模式“可選”顷锰,即模式可能出現(xiàn) 0 次或 1 次柬赐。下面的例子可以匹配neighbour
(u
出現(xiàn)1次),也可以匹配neighbor
(u
沒(méi)有出現(xiàn))官紫。
let neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true
我們可以使用花括號(hào)準(zhǔn)確指明某個(gè)模式的出現(xiàn)次數(shù)肛宋。例如,在某個(gè)元素后加上{4}
束世,則該模式需要出現(xiàn)且只能出現(xiàn) 4 次酝陈。也可以使用花括號(hào)指定一個(gè)范圍:比如{2,4}
表示該元素至少出現(xiàn) 2 次,至多出現(xiàn) 4 次毁涉。
這里給出另一個(gè)版本的正則表達(dá)式沉帮,可以匹配日期、月份贫堰、小時(shí)穆壕,每個(gè)數(shù)字都可以是一位或兩位數(shù)字。這種形式更易于解釋其屏。
let dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("30-1-2003 8:45"));
// → true
花括號(hào)中也可以省略逗號(hào)任意一側(cè)的數(shù)字粱檀,表示不限制這一側(cè)的數(shù)量。因此{,5}
表示 0 到 5 次漫玄,而{5,}
表示至少五次茄蚯。
子表達(dá)式分組
為了一次性對(duì)多個(gè)元素使用*
或者+
压彭,那么你必須使用圓括號(hào),創(chuàng)建一個(gè)分組渗常。對(duì)于后面的操作符來(lái)說(shuō)壮不,圓括號(hào)里的表達(dá)式算作單個(gè)元素。
let cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true
第一個(gè)和第二個(gè)+
字符分別作用于boo
與hoo
的o
字符皱碘,而第三個(gè)+
字符則作用于整個(gè)元組(hoo+
)询一,可以匹配hoo+
這種正則表達(dá)式出現(xiàn)一次及一次以上的情況。
示例中表達(dá)式末尾的i表示正則表達(dá)式不區(qū)分大小寫(xiě)癌椿,雖然模式中使用小寫(xiě)字母健蕊,但可以匹配輸入字符串中的大寫(xiě)字母B
。
匹配和分組
test
方法是匹配正則表達(dá)式最簡(jiǎn)單的方法踢俄。該方法只負(fù)責(zé)判斷字符串是否與某個(gè)模式匹配缩功。正則表達(dá)式還有一個(gè)exec
(執(zhí)行,execute)方法都办,如果無(wú)法匹配模式則返回null
嫡锌,否則返回一個(gè)表示匹配字符串信息的對(duì)象。
let match = /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8
exec
方法返回的對(duì)象包含index屬性琳钉,表示字符串成功匹配的起始位置势木。除此之外,該對(duì)象看起來(lái)像(而且實(shí)際上就是)一個(gè)字符串?dāng)?shù)組歌懒,其首元素是與模式匹配的字符串——在上面的例子中就是我們查找的數(shù)字序列啦桌。
字符串也有一個(gè)類(lèi)似的match方法。
console.log("one two 100".match(/\d+/));
// → ["100"]
若正則表達(dá)式包含使用圓括號(hào)包圍的子表達(dá)式分組及皂,與這些分組匹配的文本也會(huì)出現(xiàn)在數(shù)組中甫男。第一個(gè)元素是與整個(gè)模式匹配的字符串,其后是與第一個(gè)分組匹配的部分字符串(表達(dá)式中第一次出現(xiàn)左圓括號(hào)的那部分)躲庄,然后是第二個(gè)分組。
let quotedText = /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]
若分組最后沒(méi)有匹配任何字符串(例如在元組后加上一個(gè)問(wèn)號(hào))钾虐,結(jié)果數(shù)組中與該分組對(duì)應(yīng)的元素將是undefined
噪窘。類(lèi)似的,若分組匹配了多個(gè)元素效扫,則數(shù)組中只包含最后一個(gè)匹配項(xiàng)倔监。
console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]
分組是提取部分字符串的實(shí)用特性。如果我們不只是想驗(yàn)證字符串中是否包含日期菌仁,還想將字符串中的日期字符串提取出來(lái)浩习,并將其轉(zhuǎn)換成等價(jià)的日期對(duì)象,那么我們可以使用圓括號(hào)包圍那些匹配數(shù)字的模式字符串济丘,并直接將日期從exec
的結(jié)果中提取出來(lái)谱秽。
不過(guò)洽蛀,我們暫且先討論另一個(gè)話(huà)題——在 JavaScript 中存儲(chǔ)日期和時(shí)間的內(nèi)建方法。
日期類(lèi)
JavaScript 提供了用于表示日期的標(biāo)準(zhǔn)類(lèi)疟赊,我們甚至可以用其表示時(shí)間點(diǎn)郊供。該類(lèi)型名為Date
。如果使用new
創(chuàng)建一個(gè)Date
對(duì)象近哟,你會(huì)得到當(dāng)前的日期和時(shí)間驮审。
console.log(new Date());
// → Mon Nov 13 2017 16:19:11 GMT+0100 (CET)
你也可以創(chuàng)建表示特定時(shí)間的對(duì)象。
console.log(new Date(2009, 11, 9));
// → Wed Dec 09 2009 00:00:00 GMT+0100 (CET)
console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)
JavaScript 中約定是:使用從 0 開(kāi)始的數(shù)字表示月份(因此使用 11 表示 12 月)吉执,而使用從1開(kāi)始的數(shù)字表示日期疯淫。這非常容易令人混淆。要注意這個(gè)細(xì)節(jié)戳玫。
構(gòu)造器的后四個(gè)參數(shù)(小時(shí)熙掺、分鐘、秒量九、毫秒)是可選的适掰,如果用戶(hù)沒(méi)有指定這些參數(shù),則參數(shù)的值默認(rèn)為 0荠列。
時(shí)間戳存儲(chǔ)為 UTC 時(shí)區(qū)中 1970 年以來(lái)的毫秒數(shù)类浪。 這遵循一個(gè)由“Unix 時(shí)間”設(shè)定的約定,該約定是在那個(gè)時(shí)候發(fā)明的肌似。 你可以對(duì) 1970 年以前的時(shí)間使用負(fù)數(shù)费就。 日期對(duì)象上的getTime
方法返回這個(gè)數(shù)字。 你可以想象它會(huì)很大川队。
console.log(new Date(2013, 11, 19).getTime());
// → 1387407600000
console.log(new Date(1387407600000));
// → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)
如果你為Date
構(gòu)造器指定了一個(gè)參數(shù)力细,構(gòu)造器會(huì)將該參數(shù)看成毫秒數(shù)。你可以創(chuàng)建一個(gè)新的Date
對(duì)象固额,并調(diào)用getTime
方法眠蚂,或調(diào)用Date.now()
函數(shù)來(lái)獲取當(dāng)前時(shí)間對(duì)應(yīng)的毫秒數(shù)。
Date
對(duì)象提供了一些方法來(lái)提取時(shí)間中的某些數(shù)值斗躏,比如getFullYear
逝慧、getMonth
、getDate
啄糙、getHours
笛臣、getMinutes
、getSeconds
隧饼。除了getFullYear
之外該對(duì)象還有一個(gè)getYear
方法沈堡,會(huì)返回使用兩位數(shù)字表示的年份(比如 93 或 14),但很少用到燕雁。
通過(guò)在希望捕獲的那部分模式字符串兩邊加上圓括號(hào)诞丽,我們可以從字符串中創(chuàng)建對(duì)應(yīng)的Date
對(duì)象鲸拥。
function getDate(string) {
let [_, day, month, year] =
/(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
return new Date(year, month - 1, day);
}
console.log(getDate("30-1-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)
_
(下劃線(xiàn))綁定被忽略,并且只用于跳過(guò)由exec
返回的數(shù)組中的率拒,完整匹配元素崩泡。
單詞和字符串邊界
不幸的是,getDate
會(huì)從字符串"100-1-30000"
中提取出一個(gè)無(wú)意義的日期——00-1-3000
猬膨。正則表達(dá)式可以從字符串中的任何位置開(kāi)始匹配角撞,在我們的例子中,它從第二個(gè)字符開(kāi)始匹配勃痴,到倒數(shù)第二個(gè)字符為止谒所。
如果我們想要強(qiáng)制匹配整個(gè)字符串,可以使用^
標(biāo)記和$
標(biāo)記沛申。脫字符表示輸入字符串起始位置劣领,美元符號(hào)表示字符串結(jié)束位置。因此/^\d+$/
可以匹配整個(gè)由一個(gè)或多個(gè)數(shù)字組成的字符串铁材,/^!/
匹配任何以感嘆號(hào)開(kāi)頭的字符串尖淘,而/x^/
不匹配任何字符串(字符串起始位置之前不可能有字符x
)。
另一方面著觉,如果我們想要確保日期字符串起始結(jié)束位置在單詞邊界上村生,可以使用\b
標(biāo)記。所謂單詞邊界饼丘,指的是起始和結(jié)束位置都是單詞字符(也就是\w
代表的字符集合)趁桃,而起始位置的前一個(gè)字符以及結(jié)束位置的后一個(gè)字符不是單詞字符。
console.log(/cat/.test("concatenate"));
// → true
console.log(/\bcat\b/.test("concatenate"));
// → false
這里需要注意肄鸽,邊界標(biāo)記并不匹配實(shí)際的字符卫病,只在強(qiáng)制正則表達(dá)式滿(mǎn)足模式中的條件時(shí)才進(jìn)行匹配。
選項(xiàng)模式
假如我們不僅想知道文本中是否包含數(shù)字典徘,還想知道數(shù)字之后是否跟著一個(gè)單詞(pig
蟀苛、cow
或chicken
)或其復(fù)數(shù)形式。
那么我們可以編寫(xiě)三個(gè)正則表達(dá)式并輪流測(cè)試逮诲,但還有一種更好的方式帜平。管道符號(hào)(|
)表示從其左側(cè)的模式和右側(cè)的模式任意選擇一個(gè)進(jìn)行匹配。因此代碼如下所示汛骂。
let animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pigchickens"));
// → false
小括號(hào)可用于限制管道符號(hào)選擇的模式范圍罕模,而且你可以連續(xù)使用多個(gè)管道符號(hào)评腺,表示從多于兩個(gè)模式中選擇一個(gè)備選項(xiàng)進(jìn)行匹配帘瞭。
匹配原理
從概念上講,當(dāng)你使用exec
或test
時(shí)蒿讥,正則表達(dá)式引擎在你的字符串中尋找匹配蝶念,通過(guò)首先從字符串的開(kāi)頭匹配表達(dá)式抛腕,然后從第二個(gè)字符匹配表達(dá)式,直到它找到匹配或達(dá)到字符串的末尾媒殉。 它會(huì)返回找到的第一個(gè)匹配担敌,或者根本找不到任何匹配。
為了進(jìn)行實(shí)際的匹配廷蓉,引擎會(huì)像處理流程圖一樣處理正則表達(dá)式全封。 這是上例中用于家畜表達(dá)式的圖表:
https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/9-1.svg
如果我們可以找到一條從圖表左側(cè)通往圖表右側(cè)的路徑,則可以說(shuō)“表達(dá)式產(chǎn)生了匹配”桃犬。我們保存在字符串中的當(dāng)前位置刹悴,每移動(dòng)通過(guò)一個(gè)盒子,就驗(yàn)證當(dāng)前位置之后的部分字符串是否與該盒子匹配攒暇。
因此土匀,如果我們嘗試從位置 4 匹配"the 3 pigs"
,大致會(huì)以如下的過(guò)程通過(guò)流程圖:
在位置 4形用,有一個(gè)單詞邊界就轧,因此我們通過(guò)第一個(gè)盒子。
依然在位置 4田度,我們找到一個(gè)數(shù)字妒御,因此我們通過(guò)第二個(gè)盒子。
在位置 5每币,有一條路徑循環(huán)回到第二個(gè)盒子(數(shù)字)之前携丁,而另一條路徑則移動(dòng)到下一個(gè)盒子(單個(gè)空格字符)。由于這里是一個(gè)空格兰怠,而非數(shù)字梦鉴,因此我們必須選擇第二條路徑。
我們目前在位置 6(
pig
的起始位置)揭保,而表中有三路分支肥橙。這里看不到"cow"
或"chicken"
,但我們看到了"pig"
秸侣,因此選擇"pig"
這條分支存筏。在位置 9(三路分支之后),有一條路徑跳過(guò)了
s
這個(gè)盒子味榛,直接到達(dá)最后的單詞邊界椭坚,另一條路徑則匹配s
。這里有一個(gè)s
字符搏色,而非單詞邊界善茎,因此我們通過(guò)s
這個(gè)盒子。我們?cè)谖恢?10(字符串結(jié)尾)频轿,只能匹配單詞邊界垂涯。而字符串結(jié)尾可以看成一個(gè)單詞邊界烁焙,因此我們通過(guò)最后一個(gè)盒子,成功匹配字符串耕赘。
回溯
正則表達(dá)式/\b([01]+b|\d+|[\da-f]h)\b/
可以匹配三種字符串:以b
結(jié)尾的二進(jìn)制數(shù)字骄蝇,以h
結(jié)尾的十六進(jìn)制數(shù)字(即以 16 為進(jìn)制,字母a
到f
表示數(shù)字 10 到 15)操骡,或者沒(méi)有后綴字符的常規(guī)十進(jìn)制數(shù)字九火。這是對(duì)應(yīng)的圖表。
https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/9-2.svg
當(dāng)匹配該表達(dá)式時(shí)册招,常常會(huì)發(fā)生一種情況:輸入的字符串進(jìn)入上方(二進(jìn)制)分支的匹配過(guò)程吃既,但輸入中并不包含二進(jìn)制數(shù)字。我們以匹配字符串"103"
為例跨细,匹配過(guò)程只有遇到字符 3 時(shí)才知道進(jìn)入了錯(cuò)誤分支鹦倚。該字符串匹配我們給出的表達(dá)式,但沒(méi)有匹配目前應(yīng)當(dāng)處于的分支冀惭。
因此匹配器執(zhí)行“回溯”震叙。進(jìn)入一個(gè)分支時(shí),匹配器會(huì)記住當(dāng)前位置(在本例中散休,是在字符串起始媒楼,剛剛通過(guò)圖中第一個(gè)表示邊界的盒子),因此若當(dāng)前分支無(wú)法匹配戚丸,可以回退并嘗試另一條分支划址。對(duì)于字符串"103"
,遇到字符 3 之后限府,它會(huì)開(kāi)始嘗試匹配十六進(jìn)制數(shù)字的分支夺颤,它會(huì)再次失敗,因?yàn)閿?shù)字后面沒(méi)有h
胁勺。所以它嘗試匹配進(jìn)制數(shù)字的分支世澜,由于這條分支可以匹配,因此匹配器最后的會(huì)返回十進(jìn)制數(shù)的匹配信息署穗。
一旦字符串與模式完全匹配寥裂,匹配器就會(huì)停止。這意味著多個(gè)分支都可能匹配一個(gè)字符串案疲,但匹配器最后只會(huì)使用第一條分支(按照出現(xiàn)在正則表達(dá)式中的出現(xiàn)順序排序)封恰。
回溯也會(huì)發(fā)生在處理重復(fù)模式運(yùn)算符(比如+
和*
)時(shí)。如果使用"abcxe"
匹配/^.*x/
褐啡,.*
部分诺舔,首先嘗試匹配整個(gè)字符串,接著引擎發(fā)現(xiàn)匹配模式還需要一個(gè)字符x
。由于字符串結(jié)尾沒(méi)有x
混萝,因此*
運(yùn)算符嘗試少匹配一個(gè)字符。但匹配器依然無(wú)法在abcx
之后找到x
字符萍恕,因此它會(huì)再次回溯逸嘀,此時(shí)*
運(yùn)算符只匹配abc
。現(xiàn)在匹配器發(fā)現(xiàn)了所需的x
允粤,接著報(bào)告從位置 0 到位置 4 匹配成功崭倘。
我們有可能編寫(xiě)需要大量回溯的正則表達(dá)式。當(dāng)模式能夠以許多種不同方式匹配輸入的一部分時(shí)类垫,這種問(wèn)題就會(huì)出現(xiàn)司光。例如,若我們?cè)诰帉?xiě)匹配二進(jìn)制數(shù)字的正則表達(dá)式時(shí)悉患,一時(shí)糊涂残家,可能會(huì)寫(xiě)出諸如/([01]+)+b/
之類(lèi)的表達(dá)式。
https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/9-3.svg
若我們嘗試匹配一些只由 0 與 1 組成的長(zhǎng)序列售躁,匹配器首先會(huì)不斷執(zhí)行內(nèi)部循環(huán)坞淮,直到它發(fā)現(xiàn)沒(méi)有數(shù)字為止。接下來(lái)匹配器注意到陪捷,這里不存在b
回窘,因此向前回溯一個(gè)位置,開(kāi)始執(zhí)行外部循環(huán)市袖,接著再次放棄啡直,再次嘗試執(zhí)行一次內(nèi)部循環(huán)。該過(guò)程會(huì)嘗試這兩個(gè)循環(huán)的所有可能路徑苍碟。這意味著每多出一個(gè)字符酒觅,其工作量就會(huì)加倍。甚至只需較少的一堆字符微峰,就可使匹配實(shí)際上永不停息地執(zhí)行下去阐滩。
replace
方法
字符串有一個(gè)replace
方法,該方法可用于將字符串中的一部分替換為另一個(gè)字符串县忌。
console.log("papa".replace("p", "m"));
// → mapa
該方法第一個(gè)參數(shù)也可以是正則表達(dá)式掂榔,這種情況下會(huì)替換正則表達(dá)式首先匹配的部分字符串。若在正則表達(dá)式后追加g
選項(xiàng)(全局症杏,Global)装获,該方法會(huì)替換字符串中所有匹配項(xiàng),而不是只替換第一個(gè)厉颤。
console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar
如果 JavaScript 為replace
添加一個(gè)額外參數(shù)穴豫,或提供另一個(gè)不同的方法(replaceAll
),來(lái)區(qū)分替換一次匹配還是全部匹配,將會(huì)是較為明智的方案精肃。遺憾的是秤涩,因?yàn)槟承┰?JavaScript 依靠正則表達(dá)式的屬性來(lái)區(qū)分替換行為。
如果我們?cè)谔鎿Q字符串中使用元組司抱,就可以體現(xiàn)出replace
方法的真實(shí)威力筐眷。例如,假設(shè)我們有一個(gè)規(guī)模很大的字符串习柠,包含了人的名字匀谣,每個(gè)名字占據(jù)一行,名字格式為“姓资溃,名”武翎。若我們想要交換姓名,并移除中間的逗號(hào)(轉(zhuǎn)變成“名溶锭,姓”這種格式)宝恶,我們可以使用下面的代碼:
console.log(
"Liskov, Barbara\nMcCarthy, John\nWadler, Philip"
.replace(/(\w+), (\w+)/g, "$2 $1"));
// → Barbara Liskov
// John McCarthy
// Philip Wadler
替換字符串中的$1
和$2
引用了模式中使用圓括號(hào)包裹的元組。$1
會(huì)替換為第一個(gè)元組匹配的字符串趴捅,$2
會(huì)替換為第二個(gè)卑惜,依次類(lèi)推,直到$9
為止驻售。也可以使用$&
來(lái)引用整個(gè)匹配露久。
第二個(gè)參數(shù)不僅可以使用字符串,還可以使用一個(gè)函數(shù)欺栗。每次匹配時(shí)毫痕,都會(huì)調(diào)用函數(shù)并以匹配元組(也可以是匹配整體)作為參數(shù),該函數(shù)返回值為需要插入的新字符串迟几。
這里給出一個(gè)小示例:
let s = "the cia and fbi";
console.log(s.replace(/\b(fbi|cia)\b/g,
str => str.toUpperCase()));
// → the CIA and FBI
這里給出另一個(gè)值得討論的示例:
let stock = "1 lemon, 2 cabbages, and 101 eggs";
function minusOne(match, amount, unit) {
amount = Number(amount) - 1;
if (amount == 1) { // only one left, remove the 's'
unit = unit.slice(0, unit.length - 1);
} else if (amount == 0) {
amount = "no";
}
return amount + " " + unit;
}
console.log(stock.replace(/(\d+) (\w+)/g, minusOne));
// → no lemon, 1 cabbage, and 100 eggs
該程序接受一個(gè)字符串消请,找出所有滿(mǎn)足模式“一個(gè)數(shù)字緊跟著一個(gè)單詞(數(shù)字和字母)”的字符串,返回時(shí)將捕獲字符串中的數(shù)字減一类腮。
元組(\d+)
最后會(huì)變成函數(shù)中的amount
參數(shù)臊泰,而·(\w+)元組將會(huì)綁定
unit。該函數(shù)將
amount轉(zhuǎn)換成數(shù)字(由于該參數(shù)是
\d+`的匹配結(jié)果蚜枢,因此此過(guò)程總是執(zhí)行成功)缸逃,并根據(jù)剩下 0 還是 1,決定如何做出調(diào)整厂抽。
貪婪模式
使用replace
編寫(xiě)一個(gè)函數(shù)移除 JavaScript 代碼中的所有注釋也是可能的需频。這里我們嘗試一下:
function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
console.log(stripComments("1 + /* 2 */3"));
// → 1 + 3
console.log(stripComments("x = 10;// ten!"));
// → x = 10;
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 1
或運(yùn)算符之前的部分匹配兩個(gè)斜杠字符,后面跟著任意數(shù)量的非換行字符筷凤。多行注釋部分較為復(fù)雜昭殉,我們使用[^]
(任何非空字符集合)來(lái)匹配任意字符。我們這里無(wú)法使用句號(hào),因?yàn)閴K注釋可以跨行挪丢,句號(hào)無(wú)法匹配換行符蹂风。
但最后一行的輸出顯然有錯(cuò)。
為何乾蓬?
在回溯一節(jié)中已經(jīng)提到過(guò)惠啄,表達(dá)式中的[^]*
部分會(huì)首先匹配所有它能匹配的部分。如果其行為引起模式的下一部分匹配失敗巢块,匹配器才會(huì)回溯一個(gè)字符,并再次嘗試巧号。在本例中族奢,匹配器首先匹配整個(gè)剩余字符串,然后向前移動(dòng)丹鸿。匹配器回溯四個(gè)字符后越走,會(huì)找到*/,并完成匹配靠欢。這并非我們想要的結(jié)果廊敌。我們的意圖是匹配單個(gè)注釋?zhuān)堑竭_(dá)代碼末尾并找到最后一個(gè)塊注釋的結(jié)束部分。
因?yàn)檫@種行為,所以我們說(shuō)模式重復(fù)運(yùn)算符(+
、*
渤弛、?
和{}
)是“貪婪”的血公,指的是這些運(yùn)算符會(huì)盡量多地匹配它們可以匹配的字符,然后回溯谎替。若讀者在這些符號(hào)后加上一個(gè)問(wèn)號(hào)(+?
、*?
、??
护锤、{}?
),它們會(huì)變成非貪婪的酿傍,此時(shí)這些符號(hào)會(huì)盡量少地匹配字符烙懦,只有當(dāng)剩下的模式無(wú)法匹配時(shí)才會(huì)多進(jìn)行匹配。
而這便是我們想要的情況赤炒。通過(guò)讓星號(hào)盡量少地匹配字符氯析,我們可以匹配第一個(gè)*/
,進(jìn)而匹配一個(gè)塊注釋?zhuān)粫?huì)匹配過(guò)多內(nèi)容莺褒。
function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1
對(duì)于使用了正則表達(dá)式的程序而言魄鸦,其中出現(xiàn)的大量缺陷都可歸咎于一個(gè)問(wèn)題:在非貪婪模式效果更好時(shí),無(wú)意間錯(cuò)用了貪婪運(yùn)算符癣朗。若使用了模式重復(fù)運(yùn)算符拾因,請(qǐng)首先考慮一下是否可以使用非貪婪符號(hào)替代貪婪運(yùn)算符。
動(dòng)態(tài)創(chuàng)建RegExp
對(duì)象
有些情況下,你無(wú)法在編寫(xiě)代碼時(shí)準(zhǔn)確知道需要匹配的模式绢记。假設(shè)你想尋找文本片段中的用戶(hù)名扁达,并使用下劃線(xiàn)字符將其包裹起來(lái)使其更顯眼。由于你只有在程序運(yùn)行時(shí)才知道姓名蠢熄,因此你無(wú)法使用基于斜杠的記法跪解。
但你可以構(gòu)建一個(gè)字符串,并使用RegExp
構(gòu)造器根據(jù)該字符串構(gòu)造正則表達(dá)式對(duì)象签孔。
這里給出一個(gè)示例叉讥。
let name = "harry";
let text = "Harry is a suspicious character.";
let regexp = new RegExp("\\b(" + name + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → _Harry_ is a suspicious character.
由于我們創(chuàng)建正則表達(dá)式時(shí)使用的是普通字符串,而非使用斜杠包圍的正則表達(dá)式饥追,因此如果想創(chuàng)建\b
邊界图仓,我們不得不使用兩個(gè)反斜杠。RegExp
構(gòu)造器的第二個(gè)參數(shù)包含了正則表達(dá)式選項(xiàng)但绕。在本例中救崔,"gi"
表示全局和不區(qū)分大小寫(xiě)。
但由于我們的用戶(hù)是怪異的青少年捏顺,如果用戶(hù)將名字設(shè)定為"dea+hl[]rd"
六孵,將會(huì)發(fā)生什么?這將會(huì)導(dǎo)致正則表達(dá)式變得沒(méi)有意義幅骄,無(wú)法匹配用戶(hù)名劫窒。
為了能夠處理這種情況,我們可以在任何有特殊含義的字符前添加反斜杠拆座。
let name = "dea+hl[]rd";
let text = "This dea+hl[]rd guy is super annoying.";
let escaped = name.replace(/[^\w\s]/g, "\\$&");
let regexp = new RegExp("\\b(" + escaped + ")\\b", "gi");
console.log(text.replace(regexp, "_><_"));
// → This _dea+hl[]rd_ guy is super annoying.
search
方法
字符串的indexOf
方法不支持以正則表達(dá)式為參數(shù)烛亦。
但還有一個(gè)search
方法,調(diào)用該方法時(shí)需要傳遞一個(gè)正則表達(dá)式懂拾。類(lèi)似于indexOf
煤禽,該方法會(huì)返回首先匹配的表達(dá)式的索引,若沒(méi)有找到則返回 –1岖赋。
console.log(" word".search(/\S/));
// → 2
console.log(" ".search(/\S/));
// → -1
遺憾的是檬果,沒(méi)有任何方式可以指定匹配的起始偏移(就像indexOf
的第二個(gè)參數(shù)),而指定起始偏移這個(gè)功能是很實(shí)用的唐断。
lastIndex
屬性
exec
方法同樣沒(méi)提供方便的方法來(lái)指定字符串中的起始匹配位置选脊。但我們可以使用一種比較麻煩的方法來(lái)實(shí)現(xiàn)該功能。
正則表達(dá)式對(duì)象包含了一些屬性脸甘。其中一個(gè)屬性是source
恳啥,該屬性包含用于創(chuàng)建正則表達(dá)式的字符串。另一個(gè)屬性是lastIndex
丹诀,可以在極少數(shù)情況下控制下一次匹配的起始位置钝的。
所謂的極少數(shù)情況翁垂,指的是當(dāng)正則表達(dá)式啟用了全局(g
)或者粘性(y
),并且使用exec
匹配模式的時(shí)候硝桩。此外沿猜,另一個(gè)解決方案應(yīng)該是向exec
傳遞的額外參數(shù),但 JavaScript 的正則表達(dá)式接口能設(shè)計(jì)得如此合理才是怪事碗脊。
let pattern = /y/g;
pattern.lastIndex = 3;
let match = pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5
如果成功匹配模式啼肩,exec
調(diào)用會(huì)自動(dòng)更新lastIndex
屬性,來(lái)指向匹配字符串后的位置衙伶。如果無(wú)法匹配祈坠,會(huì)將lastIndex
清零(就像新構(gòu)建的正則表達(dá)式對(duì)象lastIndex
屬性為零一樣)。
全局和粘性選項(xiàng)之間的區(qū)別在于矢劲,啟用粘性時(shí)赦拘,僅當(dāng)匹配直接從lastIndex
開(kāi)始時(shí),搜索才會(huì)成功卧须,而全局搜索中另绩,它會(huì)搜索匹配可能起始的所有位置儒陨。
let global = /abc/g;
console.log(global.exec("xyz abc"));
// → ["abc"]
let sticky = /abc/y;
console.log(sticky.exec("xyz abc"));
// → null
對(duì)多個(gè)exec
調(diào)用使用共享的正則表達(dá)式值時(shí)花嘶,這些lastIndex
屬性的自動(dòng)更新可能會(huì)導(dǎo)致問(wèn)題。 你的正則表達(dá)式可能意外地在之前的調(diào)用留下的索引處開(kāi)始蹦漠。
let digit = /\d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null
全局選項(xiàng)還有一個(gè)值得深思的效果椭员,它會(huì)改變match
匹配字符串的工作方式。如果調(diào)用match
時(shí)使用了全局表達(dá)式笛园,不像exec
返回的數(shù)組隘击,match
會(huì)找出所有匹配模式的字符串,并返回一個(gè)包含所有匹配字符串的數(shù)組研铆。
console.log("Banana".match(/an/g));
// → ["an", "an"]
因此使用全局正則表達(dá)式時(shí)需要倍加小心埋同。只有以下幾種情況中,你確實(shí)需要全局表達(dá)式即調(diào)用replace
方法時(shí)棵红,或是需要顯示使用lastIndex
時(shí)凶赁。這也基本是全局表達(dá)式唯一的應(yīng)用場(chǎng)景了。
循環(huán)匹配
一個(gè)常見(jiàn)的事情是逆甜,找出字符串中所有模式的出現(xiàn)位置虱肄,這種情況下,我們可以在循環(huán)中使用lastIndex
和exec
訪(fǎng)問(wèn)匹配的對(duì)象交煞。
let input = "A string with 3 numbers in it... 42 and 88.";
let number = /\b(\d+)\b/g;
let match;
while (match = number.exec(input)) {
console.log("Found", match[0], "at", match.index);
}
// → Found 3 at 14
// Found 42 at 33
// Found 88 at 40
這里我們利用了賦值表達(dá)式的一個(gè)特性咏窿,該表達(dá)式的值就是被賦予的值。因此通過(guò)使用match=re.exec(input)
作為while
語(yǔ)句的條件素征,我們可以在每次迭代開(kāi)始時(shí)執(zhí)行匹配集嵌,將結(jié)果保存在變量中萝挤,當(dāng)無(wú)法找到更多匹配的字符串時(shí)停止循環(huán)。
解析INI
文件
為了總結(jié)一下本章介紹的內(nèi)容纸淮,我們來(lái)看一下如何調(diào)用正則表達(dá)式來(lái)解決問(wèn)題平斩。假設(shè)我們編寫(xiě)一個(gè)程序從因特網(wǎng)上獲取我們敵人的信息(這里我們實(shí)際上不會(huì)編寫(xiě)該程序,僅僅編寫(xiě)讀取配置文件的那部分代碼咽块,對(duì)不起)绘面。配置文件如下所示。
searchengine=https://duckduckgo.com/?q=$1
spitefulness=9.7
; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451
[davaeorn]
fullname=Davaeorn
type=evil wizard
outputdir=/home/marijn/enemies/davaeorn
該配置文件格式的語(yǔ)法規(guī)則如下所示(它是廣泛使用的格式侈沪,我們通常稱(chēng)之為INI
文件):
忽略空行和以分號(hào)起始的行揭璃。
使用
[]
包圍的行表示一個(gè)新的節(jié)(section)。如果行中是一個(gè)標(biāo)識(shí)符(包含字母和數(shù)字)亭罪,后面跟著一個(gè)=字符瘦馍,則表示向當(dāng)前節(jié)添加選項(xiàng)。
其他的格式都是無(wú)效的应役。
我們的任務(wù)是將這樣的字符串轉(zhuǎn)換為一個(gè)對(duì)象情组,該對(duì)象的屬性包含沒(méi)有節(jié)的設(shè)置的字符串,和節(jié)的子對(duì)象的字符串箩祥,節(jié)的子對(duì)象也包含節(jié)的設(shè)置院崇。
由于我們需要逐行處理這種格式的文件,因此預(yù)處理時(shí)最好將文件分割成一行行文本袍祖。我們使用第 6 章中的string.split("\n")
來(lái)分割文件內(nèi)容底瓣。但是一些操作系統(tǒng)并非使用換行符來(lái)分隔行,而是使用回車(chē)符加換行符("\r\n"
)蕉陋【杵荆考慮到這點(diǎn),我們也可以使用正則表達(dá)式作為split
方法的參數(shù)凳鬓,我們使用類(lèi)似于/\r?\n/
的正則表達(dá)式茁肠,這樣可以同時(shí)支持"\n"
和"\r\n"
兩種分隔符。
function parseINI(string) {
// Start with an object to hold the top-level fields
let currentSection = {name: null, fields: []};
let categories = [currentSection];
string.split(/\r?\n/).forEach(line => {
let match;
if (match = line.match(/^(\w+)=(.*)$/)) {
section[match[1]] = match[2];
section = result[match[1]] = {};
} else if (!/^\s*(;.*)?$/.test(line)) {
throw new Error("Line '" + line + "' is not valid.");
}
});
return result;
}
console.log(parseINI(`
name=Vasilis
[address]
city=Tessaloniki`));
// → {name: "Vasilis", address: {city: "Tessaloniki"}}
代碼遍歷文件的行并構(gòu)建一個(gè)對(duì)象缩举。 頂部的屬性直接存儲(chǔ)在該對(duì)象中垦梆,而在節(jié)中找到的屬性存儲(chǔ)在單獨(dú)的節(jié)對(duì)象中。 section
綁定指向當(dāng)前節(jié)的對(duì)象蚁孔。
有兩種重要的行 - 節(jié)標(biāo)題或?qū)傩孕小?當(dāng)一行是常規(guī)屬性時(shí)奶赔,它將存儲(chǔ)在當(dāng)前節(jié)中。 當(dāng)它是一個(gè)節(jié)標(biāo)題時(shí)杠氢,創(chuàng)建一個(gè)新的節(jié)對(duì)象站刑,并設(shè)置section
來(lái)指向它。
這里需要注意鼻百,我們反復(fù)使用^
和$
確保表達(dá)式匹配整行绞旅,而非一行中的一部分摆尝。如果不使用這兩個(gè)符號(hào),大多數(shù)情況下程序也可以正常工作因悲,但在處理特定輸入時(shí)堕汞,程序就會(huì)出現(xiàn)不合理的行為,我們一般很難發(fā)現(xiàn)這個(gè)缺陷的問(wèn)題所在晃琳。
if (match = string.match(...))
類(lèi)似于使用賦值作為while
的條件的技巧讯检。你通常不確定你對(duì)match
的調(diào)用是否成功,所以你只能在測(cè)試它的if
語(yǔ)句中訪(fǎng)問(wèn)結(jié)果對(duì)象卫旱。 為了不打破else if
形式的令人愉快的鏈條人灼,我們將匹配結(jié)果賦給一個(gè)綁定,并立即使用該賦值作為if
語(yǔ)句的測(cè)試顾翼。
國(guó)際化字符
由于 JavaScript 最初的實(shí)現(xiàn)非常簡(jiǎn)單投放,而且這種簡(jiǎn)單的處理方式后來(lái)也成了標(biāo)準(zhǔn),因此 JavaScript 正則表達(dá)式處理非英語(yǔ)字符時(shí)非常無(wú)力适贸。例如灸芳,就 JavaScript 的正則表達(dá)式而言,“單詞字符”只是 26 個(gè)拉丁字母(大寫(xiě)和小寫(xiě))和數(shù)字拜姿,而且由于某些原因還包括下劃線(xiàn)字符烙样。像α
或β
這種明顯的單詞字符,則無(wú)法匹配\w
(會(huì)匹配大寫(xiě)的\W
砾隅,因?yàn)樗鼈儗儆诜菃卧~字符)误阻。
由于奇怪的歷史性意外债蜜,\s
(空白字符)則沒(méi)有這種問(wèn)題晴埂,會(huì)匹配所有 Unicode 標(biāo)準(zhǔn)中規(guī)定的空白字符,包括不間斷空格和蒙古文元音分隔符寻定。
另一個(gè)問(wèn)題是儒洛,默認(rèn)情況下,正則表達(dá)式使用代碼單元狼速,而不是實(shí)際的字符琅锻,正如第 5 章中所討論的那樣。 這意味著由兩個(gè)代碼單元組成的字符表現(xiàn)很奇怪向胡。
console.log(/\ud83c\udf4e{3}/.test("\ud83c\udf4e\ud83c\udf4e\ud83c\udf4e"));
// → false
console.log(/<.>/.test("<\ud83c\udf39>"));
// → false
console.log(/<.>/u.test("<\ud83c\udf39>"));
// → true
問(wèn)題是第一行中的"\ud83c\udf4e"
(emoji 蘋(píng)果)被視為兩個(gè)代碼單元恼蓬,而{3}
部分僅適用于第二個(gè)。 與之類(lèi)似僵芹,點(diǎn)匹配單個(gè)代碼單元处硬,而不是組成玫瑰 emoji 符號(hào)的兩個(gè)代碼單元。
你必須在正則表達(dá)式中添加一個(gè)u
選項(xiàng)(表示 Unicode)拇派,才能正確處理這些字符荷辕。 不幸的是凿跳,錯(cuò)誤的行為仍然是默認(rèn)行為,因?yàn)楦淖兯赡軙?huì)導(dǎo)致依賴(lài)于它的現(xiàn)有代碼出現(xiàn)問(wèn)題疮方。
盡管這是剛剛標(biāo)準(zhǔn)化的控嗜,在撰寫(xiě)本文時(shí)尚未得到廣泛支持,但可以在正則表達(dá)式中使用\p
(必須啟用 Unicode 選項(xiàng))以匹配 Unicode 標(biāo)準(zhǔn)分配了給定屬性的所有字符骡显。
console.log(/\p{Script=Greek}/u.test("α"));
// → true
console.log(/\p{Script=Arabic}/u.test("α"));
// → false
console.log(/\p{Alphabetic}/u.test("α"));
// → true
console.log(/\p{Alphabetic}/u.test("!"));
// → false
Unicode 定義了許多有用的屬性疆栏,盡管找到你需要的屬性可能并不總是沒(méi)有意義。 你可以使用\p{Property=Value}
符號(hào)來(lái)匹配任何具有該屬性的給定值的字符惫谤。 如果屬性名稱(chēng)保持不變承边,如\p{Name}
中那樣,名稱(chēng)被假定為二元屬性石挂,如Alphabetic
博助,或者類(lèi)別,如Number
痹愚。
本章小結(jié)
正則表達(dá)式是表示字符串模式的對(duì)象富岳,使用自己的語(yǔ)言來(lái)表達(dá)這些模式:
/abc/
:字符序列/[abc]/
:字符集中的任何字符/[^abc]/
:不在字符集中的任何字符/[0-9]/
:字符范圍內(nèi)的任何字符/x+/
:出現(xiàn)一次或多次/x+?/
:出現(xiàn)一次或多次,非貪婪模式/x*/
:出現(xiàn)零次或多次/x??/
:出現(xiàn)零次或多次拯腮,非貪婪模式/x{2窖式,4}/
:出現(xiàn)兩次到四次/(abc)/
:元組/a|b|c/
:匹配任意一個(gè)模式/\d/
:數(shù)字字符/\w/
:字母和數(shù)字字符(單詞字符)/\s/
:任意空白字符/./
:任意字符(除換行符外)/\b/
:?jiǎn)卧~邊界/^/
:輸入起始位置/$/
:輸入結(jié)束位置
正則表達(dá)式有一個(gè)test
方法來(lái)測(cè)試給定的字符串是否匹配它。 它還有一個(gè)exec
方法动壤,當(dāng)找到匹配項(xiàng)時(shí)萝喘,返回一個(gè)包含所有匹配組的數(shù)組。 這樣的數(shù)組有一個(gè)index
屬性琼懊,用于表明匹配開(kāi)始的位置阁簸。
字符串有一個(gè)match
方法來(lái)對(duì)正確表達(dá)式匹配它們,以及search
方法來(lái)搜索字符串哼丈,只返回匹配的起始位置启妹。 他們的replace
方法可以用替換字符串或函數(shù)替換模式匹配。
正則表達(dá)式擁有選項(xiàng)醉旦,這些選項(xiàng)寫(xiě)在閉合斜線(xiàn)后面饶米。 i
選項(xiàng)使匹配不區(qū)分大小寫(xiě)。 g
選項(xiàng)使表達(dá)式成為全聚德车胡,除此之外檬输,它使replace
方法替換所有實(shí)例,而不是第一個(gè)匈棘。 y
選項(xiàng)使它變?yōu)檎承陨ゴ龋@意味著它在搜索匹配時(shí)不會(huì)向前搜索并跳過(guò)部分字符串。 u
選項(xiàng)開(kāi)啟 Unicode 模式羹饰,該模式解決了處理占用兩個(gè)代碼單元的字符時(shí)的一些問(wèn)題伊滋。
正則表達(dá)式是難以駕馭的強(qiáng)力工具碳却。它可以簡(jiǎn)化一些任務(wù),但用到一些復(fù)雜問(wèn)題上時(shí)也會(huì)難以控制管理笑旺。想要學(xué)會(huì)使用正則表達(dá)式的重要一點(diǎn)是:不要將其用到無(wú)法干凈地表達(dá)為正則表達(dá)式的問(wèn)題昼浦。
習(xí)題
在做本章習(xí)題時(shí),讀者不可避免地會(huì)對(duì)一些正則表達(dá)式的莫名其妙的行為感到困惑筒主,因而備受挫折关噪。讀者可以使用類(lèi)似于 http://debuggex.com/ 這樣的在線(xiàn)學(xué)習(xí)工具,將你想編寫(xiě)的正則表達(dá)式可視化乌妙,并試驗(yàn)其對(duì)不同輸入字符串的響應(yīng)使兔。
RegexpGolf
Code Golf 是一種游戲,嘗試盡量用最少的字符來(lái)描述特定程序藤韵。類(lèi)似的虐沥,Regexp Golf 這種活動(dòng)是編寫(xiě)盡量短小的正則表達(dá)式,來(lái)匹配給定模式(而且只能匹配給定模式)泽艘。
針對(duì)以下幾項(xiàng)欲险,編寫(xiě)正則表達(dá)式,測(cè)試給定的子串是否在字符串中出現(xiàn)匹涮。正則表達(dá)式匹配的字符串天试,應(yīng)該只包含以下描述的子串之一。除非明顯提到單詞邊界然低,否則千萬(wàn)不要擔(dān)心邊界問(wèn)題喜每。當(dāng)你的表達(dá)式有效時(shí),請(qǐng)檢查一下能否讓正則表達(dá)式更短小雳攘。
car
和cat
pop
和prop
ferret
带兜、ferry
和ferrari
以
ious
結(jié)尾的單詞句號(hào)、冒號(hào)来农、分號(hào)之前的空白字符
多于六個(gè)字母的單詞
不包含
e
(或者E
)的單詞
需要幫助時(shí)鞋真,請(qǐng)參考本章總結(jié)中的表格崇堰。使用少量測(cè)試字符串來(lái)測(cè)試每個(gè)解決方案沃于。
// Fill in the regular expressions
verify(/.../,
["my car", "bad cats"],
["camper", "high art"]);
verify(/.../,
["pop culture", "mad props"],
["plop", "prrrop"]]);
verify(/.../,
["ferret", "ferry", "ferrari"],
["ferrum", "transfer A"]);
verify(/.../,
["how delicious", "spacious room"],
["ruinous", "consciousness"]);
verify(/.../,
["bad punctuation ."],
["escape the period"]);
verify(/.../,
["hottentottententen"],
["no", "hotten totten tenten"]);
verify(/.../,
["red platypus", "wobbling nest"],
["earth bed", "learning ape", "BEET"]);
function verify(regexp, yes, no) {
// Ignore unfinished exercises
if (regexp.source == "...") return;
for (let str of yes) if (!regexp.test(str)) {
console.log(`Failure to match '${str}'`);
}
for (let str of no) if (regexp.test(str)) {
console.log(`Unexpected match for '${str}'`);
}
}
QuotingStyle
想象一下,你編寫(xiě)了一個(gè)故事海诲,自始至終都使用單引號(hào)來(lái)標(biāo)記對(duì)話(huà)》庇ǎ現(xiàn)在你想要將對(duì)話(huà)的引號(hào)替換成雙引號(hào),但不能替換在縮略形式中使用的單引號(hào)特幔。
思考一下可以區(qū)分這兩種引號(hào)用法的模式咨演,并手動(dòng)調(diào)用replace
方法進(jìn)行正確替換。
let text = "'I'm the cook,' he said, 'it's my job.'";
// Change this call.
console.log(text.replace(/A/g, "B"));
// → "I'm the cook," he said, "it's my job."
NumbersAgain
編寫(xiě)一個(gè)表達(dá)式蚯斯,只匹配 JavaScript 風(fēng)格的數(shù)字薄风。支持?jǐn)?shù)字前可選的正號(hào)與負(fù)號(hào)饵较、十進(jìn)制小數(shù)點(diǎn)、指數(shù)計(jì)數(shù)法(5e-3
或1E10
遭赂,指數(shù)前也需要支持可選的符號(hào))循诉。也請(qǐng)注意小數(shù)點(diǎn)前或小數(shù)點(diǎn)后的數(shù)字也是不必要的,但數(shù)字不能只有小數(shù)點(diǎn)撇他。例如.5
和5.
都是合法的 JavaScript 數(shù)字茄猫,但單個(gè)點(diǎn)則不是。
// Fill in this regular expression.
let number = /^...$/;
// Tests:
for (let str of ["1", "-1", "+15", "1.55", ".5", "5.",
"1.3e2", "1E-4", "1e+12"]) {
if (!number.test(str)) {
console.log(`Failed to match '${str}'`);
}
}
for (let str of ["1a", "+-1", "1.2.3", "1+1", "1e4.5",
".5.", "1f5", "."]) {
if (number.test(str)) {
console.log(`Incorrectly accepted '${str}'`);
}
}