JavaScript 編程精解 中文第三版 九删豺、正則表達(dá)式

九遭贸、正則表達(dá)式

原文:Regular Expressions

譯者:飛龍

協(xié)議:CC BY-NC-SA 4.0

自豪地采用谷歌翻譯

部分參考了《JavaScript 編程精解(第 2 版)》

一些人遇到問(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 次柬赐。下面的例子可以匹配neighbouru出現(xiàn)1次),也可以匹配neighboru沒(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è)+字符分別作用于boohooo字符皱碘,而第三個(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逝慧、getMonthgetDate啄糙、getHours笛臣、getMinutesgetSeconds隧饼。除了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蟀苛、cowchicken)或其復(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)你使用exectest時(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)制,字母af表示數(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)中使用lastIndexexec訪(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á)式更短小雳攘。

  1. carcat

  2. popprop

  3. ferret带兜、ferryferrari

  4. ious結(jié)尾的單詞

  5. 句號(hào)、冒號(hào)来农、分號(hào)之前的空白字符

  6. 多于六個(gè)字母的單詞

  7. 不包含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-31E10遭赂,指數(shù)前也需要支持可選的符號(hào))循诉。也請(qǐng)注意小數(shù)點(diǎn)前或小數(shù)點(diǎn)后的數(shù)字也是不必要的,但數(shù)字不能只有小數(shù)點(diǎn)撇他。例如.55.都是合法的 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}'`);
  }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末困肩,一起剝皮案震驚了整個(gè)濱河市划纽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锌畸,老刑警劉巖勇劣,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異潭枣,居然都是意外死亡芭毙,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)卸耘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)退敦,“玉大人,你說(shuō)我怎么就攤上這事蚣抗〕薨伲” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵翰铡,是天一觀的道長(zhǎng)钝域。 經(jīng)常有香客問(wèn)我,道長(zhǎng)锭魔,這世上最難降的妖魔是什么例证? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮迷捧,結(jié)果婚禮上织咧,老公的妹妹穿的比我還像新娘。我一直安慰自己漠秋,他們只是感情好笙蒙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著庆锦,像睡著了一般捅位。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天艇搀,我揣著相機(jī)與錄音尿扯,去河邊找鬼。 笑死焰雕,一個(gè)胖子當(dāng)著我的面吹牛姜胖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播淀散,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼右莱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了档插?” 一聲冷哼從身側(cè)響起慢蜓,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎郭膛,沒(méi)想到半個(gè)月后晨抡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡则剃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年耘柱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棍现。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡调煎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出己肮,到底是詐尸還是另有隱情士袄,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布谎僻,位于F島的核電站娄柳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏艘绍。R本人自食惡果不足惜赤拒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望诱鞠。 院中可真熱鬧挎挖,春花似錦、人聲如沸般甲。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)敷存。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間锚烦,已是汗流浹背觅闽。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涮俄,地道東北人蛉拙。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像彻亲,于是被迫代替她去往敵國(guó)和親孕锄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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