理解Javascript的正則表達式

前言

本文4089字,閱讀大約需要12分鐘。

總括: 本文基於Javascript的正則表達式,結合筆者個人的思考來對正則表達式的特性進行講解。

事親以敬,美過三牲。

正文

相信很多人第一次見到正則表達式的第一印象都是懵逼的,對新手而言一個正則表達式就是一串毫無意義的字符串,讓人摸不着頭腦。但正則表達式是個非常有用的特性,不管是Javascript、PHP、Java還是Python都有正則表達式。儼然正則表達式已經發展成了一門小語言。作爲編程語言的一部分,它不想變量,函數,對象這種概念那麼容易理解。很多人對於正則表達式的理解都是基於簡單的匹配,等到業務中用到完全靠從網上copy來解決問題。不得不說,隨着各種開源技術社區的發展,靠copy的確能解決業務中絕大多數的問題,但作爲一名有追求的程序員,是絕對不會讓自己僅僅依靠Ctrl C + Ctrl V來編程的。本文基於Javascript的正則表達式,結合筆者個人的思考和社區內一些優秀正則表達式文章來對正則表達式進行講解。

Javascrip中的正則表達式使用方法

簡單介紹下,在Javascript中使用正則表達式有兩種方式:

  1. **構造函數:**使用內置的RegExp構造函數;
  2. **字面量:**使用雙斜槓(//);

使用構造函數:

var regexConst = new RegExp('abc');

使用雙斜槓:

var regexLiteral = /abc/;

匹配方法

Javascript中的正則表達式對象主要有兩個方法,testexec

test()方法接受一個參數,這個參數是用來與正則表達式匹配的字符串,如下例子:

var regex = /hello/;
var str = 'hello world';
var result = regex.test(str);
console.log(result);
// returns true

exec()方法在一個指定字符串中執行一個搜索匹配。返回一個結果數組或 null

var regex = /hello/;
var str = 'hello world';
var result = regex.exec(str);
console.log(result);
// returns [ 'hello', index: 0, input: 'hello world', groups: undefined ]
// 匹配失敗會返回null
// 'hello' 待匹配的字符串
// index: 正則表達式開始匹配的位置
// input: 原始字符串

下文都用test()方法來進行測試。

標誌

標誌是用來表示搜索字符串範圍的一個參數,主要有6個標誌:

標誌 描述
g 全局搜索。
i 不區分大小寫搜索。
m 多行搜索。
s 允許 . 匹配換行符。
u 使用unicode碼的模式進行匹配。
y 執行“粘性”搜索,匹配從目標字符串的當前位置開始,可以使用y標誌。

雙斜槓語法:

var re = /pattern/flags;

構造函數語法:

var re = new RegExp("pattern", "flags");

看下實例:

var reg1 = /abc/gi;
var reg2 = new RegExp("abc", "gi");
var str = 'ABC';
console.log(reg1.test(str)); // true
console.log(reg2.test(str)); // true

正則表達式的思考

正則表達式是對字符串進行匹配的一種模式。

請記住,正則表達式是對字符串的操作,所以一般具有字符串類型的編程語言都會有正則表達式。

對於字符串而言,是由兩部分構成的:內容和位置

比如一個字符串:

'hello World';

它的內容就是:

'h', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'

如上字符串中每一個獨立的字母就是這個字符串的內容,而位置指的是:

位置所指就是相鄰字符之間的位置,也就是上圖中箭頭的位置。

匹配內容相比匹配位置來說更爲複雜,先看下簡單的匹配方式:

簡單匹配

最簡單的匹配方式就是完整的去匹配一個字符串:

 var regex = /hello/;
 console.log(regex.test('hello world'));
 // true

複雜匹配

正則表達式中有很多特殊字符用來匹配字符串,解決的就是匹配多少(按位置匹配)和匹配誰(按內容匹配)的問題。我們先來看下用來決定匹配誰的一些特殊字符:

匹配內容

簡單的特殊字符

簡單的匹配內容有如下的特殊字符:

  • [xyz]:字符集,用來匹配方括號中的任意一個字符,比如:

    var regex = /[bt]ear/;
    console.log(regex.test('tear'));
    // returns true
    console.log(regex.test('bear'));
    // return true
    console.log(regex.test('fear'));
    // return false
    

    **注意:**除了特殊字符^外,其它所有的特殊字符在字符集(方括號中)都會失去它的特殊含義。

  • [^xyz]:這也是個字符集,和上面字符集不同的事,它用來匹配所有不在方括號中的字符。比如:

    var regex = /[^bt]ear/;
    console.log(regex.test('tear'));
    // returns false
    console.log(regex.test('bear'));
    // return false
    console.log(regex.test('fear'));
    // return true
    

    針對小寫字母,大寫字母和數字這三種非常常用的字符,還提供了比較簡便的寫法:

  • \d:相當於[0-9],匹配數字字符。

  • \D:相當於[^0-9],匹配非數字的字符。

  • \w:相當於[a-zA-Z0–9_],匹配數字、小寫字母、大寫字母和下劃線。

  • \W:相當於[^A-Za-z0-9_],匹配非數字、非小寫字母、非大寫字母和非下劃線。

  • [a-z]:假如我們想匹配所有的字母,一個笨辦法就是將所有的字母都寫到方括號裏,但很明這種實現很不優雅,不易讀而且很容易遺漏字母。這裏有一種更簡單的實現方案,就是指定字符範圍,比如**[a-h]就是匹配字母a到字母h之間所有的字母,除了小寫字母還可以匹配數字和大寫字母,[0-9]匹配0到9之間的數字,[A-Z]**匹配A到Z之間所有的大寫字母。比如:

    var regex = /[a-z0-9A-Z]ear/;
    console.log(regex.test('fear'));
    // returns true
    console.log(regex.test('tear'));
    // returns true
    console.log(regex.test('1ear'));
    // returns true
    console.log(regex.test('Tear'));
    // returns true
    
  • x|y:匹配x或是y。比如:

    var regex = /(green|red) apple/;
    console.log(regex.test('green apple'));
    // true
    console.log(regex.test('red apple'));
    // true
    console.log(regex.test('blue apple'));
    // false
    
  • .: 匹配除換行符之外的任何單個字符,如果標誌中有s則也會匹配換行符例子:

    var regex = /.n/ ;
    console.log(regex.test('an'));
    // true
    console.log(regex.test('no'));
    // false
    console.log(regex.test('on'));
    // true
    console.log(regex.test(`
    n`));
    // false
    console.log(/.n/s.test(`
    n`)); // 注意這裏的正則
    // true
    
  • \:這個特殊字符是用來轉義的,比如我們想匹配方括號,就可以用\轉義,同樣相匹配\也可以用\轉義,比如:

    var regex = /\[\]/;
    console.log(regex.test('[]')); // true
    

上面的特殊字符都只能匹配某個目標字符串一次,但很多場景下我們需要匹配目標字符串多次,比如我們想匹配無數個a,上面的特殊字符就無法滿足我們的需求了,因此匹配內容的特殊字符裏還有一部分是用來解決這個問題的:

  • {n}:匹配大括號之前的字符n次。例子:

    var regex = /go{2}d/;
    console.log(regex.test('good'));
    // true
    console.log(regex.test('god'));
    // false
    

    很好理解,上面的正則相當於/good/

  • {n,}:匹配大括號之前的字符至少n次。例子:

    var regex = /go{2,}d/;
    console.log(regex.test('good'));
    // true
    console.log(regex.test('goood'));
    // true
    console.log(regex.test('gooood'));
    // true
    
  • {n,m}:匹配大括號之前的字符至少n次,至多m次。例子:

    var regex = /go{1,2}d/;
    console.log(regex.test('god'));
    // true
    console.log(regex.test('good'));
    // true
    console.log(regex.test('goood'));
    // false
    

爲了更爲方便的使用,還提供了三個比較常用規則更爲方便的寫法:

  • *:相當於{0,}。表示前面的字符至少出現0次,也就是出現任意次。
  • +:相當於{1,}。表示前面的字符至少出現1次。
  • ?:相當於{0,1}。表示前面的字符不出現或是出現1次。

使用以上內容匹配普通的字符已經可以滿足需求了,但像換行符、換頁符和回車等特殊的符號以上的特殊字符無法滿足需求,因此正則表達式還提供了專門用來匹配特殊符號的特殊字符:

  • \s:匹配一個空白字符,包括空格、製表符、換頁符和換行符。看下例子:

    var reg = /\s/;
    console.log(reg.test(' ')); // true
    
  • \S:匹配一個非空白字符;

  • \t:匹配一個水平製表符 。

  • \n:匹配一個換行符。

  • \f:匹配一個換頁符。

  • \r:匹配一個回車符。

  • \v:匹配一個垂直製表符。

  • \0:匹配 NULL(U+0000)字符。

  • [\b]:匹配一個退格。

  • \cX:當X是處於A到Z之間的字符的時候,匹配字符串中的一個控制符。

內容匹配進階
  • (x): 匹配x並記住x,括號內的內容被稱爲捕獲組。這個括號裏強大的是可以支持子表達式,就是說可以在括號裏去寫正則,然後作爲一個整體去匹配。這裏還有一個特殊字符叫\n,這個n和前面換行符不一樣,這是個變量指的是數字,用來記錄捕獲組序號的。例子:

    console.log(/(foo)(bar)\1\2/.test('foobarfoobar')); // true
    console.log(/(\d)([a-z])\1\2/.test('1a1a')); // true
    console.log(/(\d)([a-z])\1\2/.test('1a2a')); // false
    console.log(/(\d){2}/.test('12')); // true
    

    在正則表達式的替換環節,則要使用像 $1$2、…、$n 這樣的語法,例如,'bar foo'.replace(/(...) (...)/, '$2 $1')$& 表示整個用於匹配的原字符串。

  • (?:x):匹配 ‘x’ 但是不記住匹配項。被稱爲非捕獲組。這裏的\1不會生效,會把它當做普通字符處理。例子:

    var regex = /(?:foo)bar\1/;
    console.log(regex.test('foobarfoo'));
    // false
    console.log(regex.test('foobar'));
    // false
    console.log(regex.test('foobar\1'));
    // true
    

匹配位置

再次強調,這裏的位置是前面圖裏箭頭的位置。

  • ^:匹配字符串的開始位置,也就是我們前面位置圖的第一個箭頭的位置。注意和[^xy]中的^區分,兩個含義完全不同,看^例子:

    var regex = /^g/;
    console.log(regex.test('good'));
    // true
    console.log(regex.test('bad'));
    // false
    console.log(regex.test('tag'));
    // false
    

    上面正則的含義即匹配字母g開頭的字符串。

  • $:匹配字符串的結束位置,例子:

    var regex = /.com$/;
    console.log(regex.test('[email protected]'));
    // true
    console.log(regex.test('test@testmail'));
    // false
    

    上面正則的含義即匹配以.com爲結尾的字符串

  • \b:匹配一個詞的邊界。注意匹配的是一個詞的邊界,這個邊界指的是一個詞不被另外一個“字”字符跟隨的位置或者前面跟其他“字”字符的位置。也就是符合要求的某個位置兩邊不全是正常字符或不全是特殊符號的。看例子:

    console.log(/\bm/.test('moon')); // true 匹配“moon”中的‘m’,\b的左邊是空字符串,右邊是'm'
    console.log(/oo\b/.test('moon')); // false 並不匹配"moon"中的'oo',因爲 \b左邊上oo,右邊是n,全是正常字符
    console.log(/oon\b/.test('moon')); // true 匹配"moon"中的'oon',\b左邊是oon,右邊是空字符串
    console.log(/n\b/.test('moon   ')); // true 匹配"moon"中的'n',\b左邊是n,右邊是空格
    console.log(/\bm/.test('   moon')); // true 匹配"moon"中的'm',\b左邊是空字符串 右邊是m
    console.log(/\b/.test('  ')); // false 無法匹配空格,\b左邊是空格或空字符串,右邊是空格或是空字符串,無法滿足不全是正常字符或是不全是正常字符
    

    這個如果不好理解,可以先看\B,更好理解一點。

  • \B: 匹配一個非單詞邊界,和\b相反,也就是說匹配的是左右兩邊全是正常字符或全是特殊符號的位置。看例子:

    console.log(/\B../.test('moon')); // true 匹配'moon'中的'oo' \B左邊是m,右邊是o
    console.log(/\B./.exec('  ')); // true 匹配'  '中的' ' \B左邊是空字符串,右邊是空格' '
    
  • x(?!y):僅僅當’x’後面不跟着’y’時匹配’x’,這被稱爲正向否定查找。例子:

    var regex = /Red(?!Apple)/;
    console.log(regex.test('RedOrange')); // true
    
  • (?<!y)x:僅僅當’x’前面不是’y’時匹配’x’,這被稱爲反向否定查找。例子:

    var regex = /(?<!Red)Apple/;
    console.log(regex.test('GreenApple')); // true
    
  • x(?=)y:匹配’x’僅僅當’x’後面跟着’y’.這種叫做先行斷言。例子:

    var regex = /Red(?=Apple)/;
    console.log(regex.test('RedApple')); // true
    
  • (?<=y)x:匹配’x’僅僅當’x’前面是’y’.這種叫做後行斷言。例子:

    var regex = /(?<=Red)Apple/;
    console.log(regex.test('RedApple')); // true
    

JS中可以使用正則表達式的方法

方法 描述
RegExp.prototype.exec 一個在字符串中執行查找匹配的RegExp方法,它返回一個數組(未匹配到則返回 null)。
RegExp.prototype.test 一個在字符串中測試是否匹配的RegExp方法,它返回 true 或 false。
String.prototype.match 一個在字符串中執行查找匹配的String方法,它返回一個數組,在未匹配到時會返回 null。
String.prototype.matchAll 一個在字符串中執行查找所有匹配的String方法,它返回一個迭代器(iterator)。
String.prototype.search 一個在字符串中測試匹配的String方法,它返回匹配到的位置索引,或者在失敗時返回-1。
String.prototype.replace 一個在字符串中執行查找匹配的String方法,並且使用替換字符串替換掉匹配到的子字符串。
String.prototype.split 一個使用正則表達式或者一個固定字符串分隔一個字符串,並將分隔後的子字符串存儲到數組中的 String 方法。

練習

  • 匹配任意10位數:
var regex = /^\d{10}$/;
console.log(regex.test('9995484545'));
// true

分析下上面的正則:

  1. 我們匹配想要跨越整個字符串,不能字符串中有我們要匹配的內容就可以,因此使用^$限制了開頭和結尾;
  2. \d用來匹配數字,它相當於[0-9];
  3. {10}匹配了\d表達式,即\d重複10次;
  • 匹配日期格式DD-MM-YYYYDD-MM-YY
var regex = /^(\d{1,2}-){2}\d{2}(\d{2})?$/;
console.log(regex.test('10-01-1990'));
// true
console.log(regex.test('2-01-90'));
// true
console.log(regex.test('10-01-190'));

分析上面的正則:

  1. 同理我們使用^$限制了開頭和結尾;
  2. \d{1,2},表示匹配1位或2位數字;
  3. -來匹配連字符,無特殊含義;
  4. ()包裹了一個子表達式,也叫捕獲組;
  5. {2}表示匹配上面的子表達式兩次;
  6. \d{2}匹配兩位數字;
  7. (\d{2})?子表達式中匹配兩位數字,然後匹配子表達式一次或是不匹配;
  • 駝峯命名轉下劃線命名:
var reg = /(\B[A-Z])/g;
'oneTwoThree'.replace(reg, '_$1').toLowerCase();

分析上面的正則:

  1. \B避免將首字母大寫的字符也轉換掉;
  2. ([A-Z])捕獲組捕獲大寫字母;
  3. 然後replace裏使用$n這樣的語法來表示前面的捕獲;
  4. 調用toLowerCase轉爲小寫字母;

結論

正則表達式各種規則很難記,希望本篇文章可以幫大家更好的去記憶這些特殊字符,也希望大家能寫出牛叉的正則表達式。共勉。最後提供一個練習正則表達式的鏈接:https://www.hackerrank.com/domains/regex和一個工具網站:https://regex101.com/

以上。


能力有限,水平一般,歡迎勘誤,不勝感激。

訂閱更多文章可關注公衆號「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍

前端進階學習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章