正則表達式括號的作用

來源:正則表達式括號的作用
作者:老姚(轉載已獲得作者授權)


不管哪門語言中都有括號。正則表達式也是一門語言登舞,而括號的存在使這門語言更為強大糠赦。對括號的使用是否得心應手欣硼,是衡量對正則的掌握水平的一個側面標準。括號的作用映穗,其實三言兩語就能說明白窖张,括號提供了分組,便于我們引用它蚁滋。引用某個分組宿接,會有兩種情形:在JavaScript里引用它赘淮,在正則表達式里引用它。

本文內容雖相對簡單睦霎,但我也要寫長點梢卸。

內容包括:

  1. 分組和分支結構
  1. 捕獲分組
  2. 反向引用
  3. 非捕獲分組
  4. 相關案例

1. 分組和分支結構

這二者是括號最直覺的作用,也是最原始的功能副女。

1.1 分組

我們知道/a+/匹配連續(xù)出現(xiàn)的“a”蛤高,而要匹配連續(xù)出現(xiàn)的“ab”時,需要使用/(ab)+/碑幅。其中括號是提供分組功能襟齿,使量詞“+”作用于“ab”這個整體,測試如下:

var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) ); // ["abab", "ab", "ababab"]
1.2 分支結構

而在多選分支結構(p1|p2)中枕赵,此處括號的作用也是不言而喻的猜欺,提供了子表達式的所有可能。比如拷窜,要匹配如下的字符串:

I love JavaScript
I love Regular Expression

可以使用正則:

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

如果去掉正則中的括號开皿,即/^I love JavaScript|Regular Expression$/,匹配字符串是"I love JavaScript""Regular Expression"篮昧,當然這不是我們想要的赋荆。

2. 引用分組

這是括號一個重要的作用,有了它懊昨,我們就可以進行數(shù)據(jù)提取窄潭,以及更強大的替換操作。而要使用它帶來的好處酵颁,必須配合使用實現(xiàn)環(huán)境的API嫉你。以日期為例。假設格式是yyyy-mm-dd的躏惋,我們可以先寫一個簡單的正則:

var regex = /\d{4}-\d{2}-\d{2}/;

然后再修改成括號版的:

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

為什么要使用這個正則呢幽污?

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

比如提取出年、月簿姨、日距误,可以這么做:

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

match返回的一個數(shù)組,第一個元素是整體匹配結果扁位,然后是各個分組(括號里)匹配的內容准潭,然后是匹配下標,最后是輸入的文本域仇。(注意:如果正則是否有修飾符g刑然,match返回的數(shù)組格式是不一樣的)。

另外也可以使用正則對象的exec方法:

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

同時殉簸,也可以使用構造函數(shù)的全局屬性$1至$9來獲热蚣:

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

regex.test(string); // 正則操作即可沽讹,例如
//regex.exec(string);
//string.match(regex);

console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"
2.2 替換

比如,想把yyyy-mm-dd格式武鲁,替換成mm/dd/yyyy怎么做爽雄?

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result); // "06/12/2017"

其中replace中的,第二個參數(shù)里用$1沐鼠、$2挚瘟、$3指代相應的分組。等價于如下的形式:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function() {
    return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result); // "06/12/2017"

也等價于:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function(match, year, month, day) {
    return month + "/" + day + "/" + year;
});
console.log(result); // "06/12/2017"

3. 反向引用

除了使用相應API來引用分組饲梭,也可以在正則本身里引用分組乘盖。但只能引用之前出現(xiàn)的分組,即反向引用憔涉。還是以日期為例订框。比如要寫一個正則支持匹配如下三種格式:

2016-06-12
2016/06/12
2016.06.12

最先可能想到的正則是:

var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // true

其中/和.需要轉義。雖然匹配了要求的情況兜叨,但也匹配"2016-06/12"這樣的數(shù)據(jù)穿扳。

假設我們想要求分割符前后一致怎么辦?此時需要使用反向引用:

var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false

注意里面的\1国旷,表示的引用之前的那個分組(-|/|.)矛物。不管它匹配到什么(比如-),\1都匹配那個同樣的具體某個字符跪但。我們知道了\1的含義后履羞,那么\2和\3的概念也就理解了,即分別指代第二個和第三個分組屡久。

看到這里忆首,此時,恐怕你會有三個問題涂身。

3.1 括號嵌套怎么辦雄卷?

以左括號(開括號)為準。比如:

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

我們可以看看這個正則匹配模式:

第一個字符是數(shù)字蛤售,比如說1,
第二個字符是數(shù)字妒潭,比如說2悴能,
第三個字符是數(shù)字,比如說3雳灾,

接下來的是\1漠酿,是第一個分組內容,那么看第一個開括號對應的分組是什么谎亩,是123炒嘲,
接下來的是\2宇姚,找到第2個開括號,對應的分組夫凸,匹配的內容是1浑劳,
接下來的是\3,找到第3個開括號夭拌,對應的分組魔熏,匹配的內容是23,
最后的是\4鸽扁,找到第3個開括號蒜绽,對應的分組,匹配的內容是3桶现。

這個問題躲雅,估計仔細看一下,就該明白了骡和。

3.2 \10表示什么呢吏夯?

另外一個疑問可能是,即\10是表示第10個分組即横,還是\1和0呢噪生?答案是前者,雖然一個正則里出現(xiàn)\10比較罕見东囚。測試如下:

var regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
var string = "123456789# ######"
console.log( regex.test(string) );
3.3 引用不存在的分組會怎樣跺嗽?

因為反向引用,是引用前面的分組页藻,但我們在正則里引用了不存在的分組時桨嫁,此時正則不會報錯,只是匹配反向引用的字符本身份帐。例如\2璃吧,就匹配"\2"。注意"\2"表示對2進行了轉意废境。

var regex = /\1\2\3\4\5\6\7\8\9/;
console.log( regex.test("\1\2\3\4\5\6\7\8\9") ); 
console.log( "\1\2\3\4\5\6\7\8\9".split("") );

chrome瀏覽器打印的結果:


4. 非捕獲分組

之前文中出現(xiàn)的分組畜挨,都會捕獲它們匹配到的數(shù)據(jù),以便后續(xù)引用噩凹,因此也稱他們是捕獲型分組巴元。

如果只想要括號最原始的功能,但不會引用它驮宴,即逮刨,既不在API里引用,也不在正則里反向引用堵泽。此時可以使用非捕獲分組(?:p)修己,例如本文第一個例子可以修改為:

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

5. 相關案例

至此括號的作用已經(jīng)講完了恢总,總結一句話,就是提供了可供我們使用的分組睬愤,如何用就看我們的了片仿。

5.1 字符串trim方法模擬

trim方法是去掉字符串的開頭和結尾的空白符。有兩種思路去做戴涝。

第一種滋戳,匹配到開頭和結尾的空白符,然后替換成空字符啥刻。如:

function trim(str) {
    return str.replace(/^\s+|\s+$/g, '');
}
console.log( trim("  foobar   ") ); // "foobar"

第二種奸鸯,匹配整個字符串,然后用引用來提取出相應的數(shù)據(jù):

function trim(str) {
    return str.replace(/^\s*(.*?)\s*$/g, "$1");
}
console.log( trim("  foobar   ") ); // "foobar"

這里使用了惰性匹配*?可帽,不然也會匹配最后一個空格之前的所有空格的娄涩。

當然,前者效率高映跟。

5.2 將每個單詞的首字母轉換為大寫
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"

思路是找到每個單詞的首字母蓄拣,當然這里不使用非捕獲匹配也是可以的。

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

首字母不會轉化為大寫的努隙。其中分組(.)表示首字母球恤,單詞的界定,前面的字符可以是多個連字符荸镊、下劃線以及空白符咽斧。正則后面的?的目的,是為了應對str尾部的字符可能不是單詞字符躬存,比如str是'-moz-transform '张惹。

5.4 中劃線化
function dasherize(str) {
    return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
}
console.log( dasherize('MozTransform') ); // -moz-transform

駝峰化的逆過程。

5.5 html轉義和反轉義
// 將HTML特殊字符轉換成等值的實體
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ù)生成的正則岭洲,然后替換相應的格式就行了宛逗,這個跟本文沒多大關系。倒是它的逆過程盾剩,使用了括號雷激,以便提供引用,也很簡單彪腔,如下:

// 實體字符轉換為等值的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獲取相應的分組引用,然后作為對象的鍵德挣。

5.6 匹配成對標簽

要求匹配:
<title>regular expression</title>
<p>laoyao bye bye</p>

不匹配:

<title>wrong!</p>

匹配一個開標簽,可以使用正則<[^>]+>快毛,
匹配一個閉標簽格嗅,可以使用</[^>]+>番挺,

但是要求匹配成對標簽,那就需要使用反向引用屯掖,如:

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

其中開標簽<[>]+>改成<([>]+)>玄柏,使用括號的目的是為了后面使用反向引用,而提供分組贴铜。閉標簽使用了反向引用粪摘,</\1>。另外[\d\D]的意思是绍坝,這個字符是數(shù)字或者不是數(shù)字徘意,因此,也就是匹配任意字符的意思轩褐。

后記

正則中使用括號的例子那可是太多了椎咧,不一而足。
重點理解括號可以提供分組把介,我們可以提取數(shù)據(jù)勤讽,應該就可以了。
例子中的代碼拗踢,基本沒做多少分析脚牍,相信你都能看懂的。
感謝你看到這里巢墅,本文也要結束了诸狭。
如果有更好的例子,也可以幫我補充補充砂缩。有可能會收入在正則的相關API使用那篇里作谚。
最后,我們該想到庵芭,陸游詩人對前端做的最大貢獻是:
紙上得來終覺淺妹懒,絕知此事要躬行。
本文完双吆!

往期回顧

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末眨唬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子好乐,更是在濱河造成了極大的恐慌匾竿,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔚万,死亡現(xiàn)場離奇詭異岭妖,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門昵慌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來假夺,“玉大人,你說我怎么就攤上這事斋攀∫丫恚” “怎么了?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵淳蔼,是天一觀的道長侧蘸。 經(jīng)常有香客問我,道長鹉梨,這世上最難降的妖魔是什么讳癌? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮俯画,結果婚禮上析桥,老公的妹妹穿的比我還像新娘。我一直安慰自己艰垂,他們只是感情好泡仗,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著猜憎,像睡著了一般娩怎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胰柑,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天截亦,我揣著相機與錄音,去河邊找鬼柬讨。 笑死崩瓤,一個胖子當著我的面吹牛,可吹牛的內容都是我干的踩官。 我是一名探鬼主播却桶,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蔗牡!你這毒婦竟也來了颖系?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤辩越,失蹤者是張志新(化名)和其女友劉穎嘁扼,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體黔攒,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡趁啸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年强缘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莲绰。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡欺旧,死狀恐怖姑丑,靈堂內的尸體忽然破棺而出蛤签,到底是詐尸還是另有隱情,我是刑警寧澤栅哀,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布震肮,位于F島的核電站,受9級特大地震影響留拾,放射性物質發(fā)生泄漏戳晌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一痴柔、第九天 我趴在偏房一處隱蔽的房頂上張望沦偎。 院中可真熱鬧,春花似錦咳蔚、人聲如沸豪嚎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽侈询。三九已至,卻和暖如春糯耍,著一層夾襖步出監(jiān)牢的瞬間扔字,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工温技, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留革为,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓舵鳞,卻偏偏與公主長得像震檩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子系任,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內容