通過這篇簡短的指南,您可以學習關於JavaScript正則表達式的所有內容,了解最重要的概念並通過示例展示。
- 正則表達式介紹
- 困難但有用
- 正則表達式是什麼樣子
- 工作原理
- 錨定
- 匹配範圍內的項目
- 多次匹配範圍項目
- 否定模式
- 元字符
- 正則表達式選項
- 量詞
- 可選項目
- 分組
- 捕獲分組
- 不使用分組的匹配和執行
- 非捕獲分組
- 標誌
- 檢查正則表達式
- 轉義
- 字符串邊界
- 使用正則表達式替換
- 貪婪模式
- 先行斷言:根據後面的字符串進行匹配
- 後行斷言:根據前面的字符串進行匹配
- 正則表達式和Unicode
- Unicode屬性逃逸
- 示例
正則表達式介紹
正則表達式(也稱為regex)是一種以非常高效的方式處理字符串的方法。
通過使用特殊的語法來定義正則表達式,您可以:
- 在字符串中搜索文本
- 在字符串中替換子字符串
- 從字符串中提取信息
幾乎每一種編程語言都實現了正則表達式。每種實現之間有一些小差異,但是基本概念幾乎都是通用的。
正則表達式可以追溯到20世紀50年代,那時它被正式作為字符串處理算法的一種概念搜索模式。
它在grep、sed等UNIX工具中得到了實現,以及在流行的文本編輯器中使用,在Perl編程語言中引入了正則表達式,隨後在許多其他編程語言中引入。
JavaScript與Perl一起是具有內置的正則表達式支持的編程語言之一。
困難但有用
對於初學者來說,正則表達式可能完全看不懂,但很多時候即使對專業開發人員來說也是如此,除非他們願意投入必要的時間去理解它們。
由於寫出易於理解、易於閱讀和易於維護/修改的正則表達式非常困難,所以使用正則表達式是唯一明智的方法來執行某些字符串操作,因此它是一個非常有價值的工具。
本教程旨在以簡單的方式介紹JavaScript正則表達式,並提供閱讀和創建正則表達式所需的所有信息。
經驗法則是,簡單的正則表達式容易閱讀且易於撰寫,而複雜的正則表達式如果你不深入了解基礎知識,很快就會變得混亂不堪。
正則表達式是什麼樣子
在JavaScript中,正則表達式是一個對象,可以用兩種方式定義。
第一種方式是使用構造函數通過實例化一個新的RegExp對象來定義:
const re1 = new RegExp('hey')
第二種方式是使用正則字面量形式:
const re1 = /hey/
您知道JavaScript有對象字面量和數組字面量嗎?它還有正則字面量。
在上面的示例中,hey
被稱為模式。在字面形式中,它由斜杠分隔,而使用對象構造函數時則不是。
這是兩種形式之間的第一個重要差異,但我們稍後會看到其他差異。
工作原理
我們在上面定義的正則表達式re1
非常簡單。它在字符串hey
中搜索,沒有任何限制:字符串可以包含大量的文本和hey
在中間,正則表達式會返回匹配。它也可以只包含hey
,它也可以匹配。
這非常簡單。
您可以使用RegExp.test(String)
來測試正則表達式,它將返回一個布爾值:
// ✅
re1.test('hey')
re1.test('blablabla hey blablabla')
// ❌
re1.test('he')
re1.test('blablabla')
在上面的示例中,我們只是檢查了是否"hey"
滿足存儲在re1
中的正則表達式模式。
這是最簡單的情況,但是您已經了解了許多關於正則表達式的概念。
錨定
/hey/
在字符串中匹配hey
。
如果要匹配以hey
開始的字符串,請使用^
操作符:
/^hey/.test('hey') // ✅
/^hey/.test('bla hey') // ❌
如果要匹配以hey
結尾的字符串,請使用$
操作符:
/hey$/.test('hey') // ✅
/hey$/.test('bla hey') // ✅
/hey$/.test('hey you') // ❌
結合這兩個,匹配正好與hey
相同的字符串:
/^hey$/.test('hey') // ✅
要匹配以一個子字符串開始並以另一個子字符串結尾的字符串,可以使用.*
,它匹配任何字符的重複0次或多次:
/^hey.*joe$/.test('hey joe') // ✅
/^hey.*joe$/.test('heyjoe') // ✅
/^hey.*joe$/.test('hey how are you joe') // ✅
/^hey.*joe$/.test('hey joe!') // ❌
匹配範圍內的項目
與其匹配特定的字符串,您可以選擇匹配範圍內的任何字符,例如:
/[a-z]/ // a, b, c, ... , x, y, z
/[A-Z]/ // A, B, C, ... , X, Y, Z
/[a-c]/ // a, b, c
/[0-9]/ // 0, 1, 2, 3, ... , 8, 9
這些正則表達式匹配包含這些範圍內的字符的字符串:
/[a-z]/.test('a') // ✅
/[a-z]/.test('1') // ❌
/[a-z]/.test('A') // ❌
/[a-c]/.test('d') // ❌
/[a-c]/.test('dc') // ✅
範圍可以結合使用:
/[A-Za-z0-9]/
/[A-Za-z0-9]/.test('a') // ✅
/[A-Za-z0-9]/.test('1') // ✅
/[A-Za-z0-9]/.test('A') // ✅
多次匹配範圍項目
通過在組閉合括號之後放置正則表達式的重複字符,您可以檢查字符串是否只包含範圍內的一個字符,通過在開始使用^
並以$
字符結束:
/^[A-Z]$/.test('A') // ✅
/^[A-Z]$/.test('AB') // ❌
/^[A-Z]$/.test('Ab') // ❌
/^[A-Za-z0-9]$/.test('1') // ✅
/^[A-Za-z0-9]$/.test('A1') // ❌
否定模式
在範圍的開始處使用^
字符將其錨定到字符串的開頭。
在範圍內使用它時,這是它的否定形式,所以:
/[^A-Za-z0-9]/.test('a') // ❌
/[^A-Za-z0-9]/.test('1') // ❌
/[^A-Za-z0-9]/.test('A') // ❌
/[^A-Za-z0-9]/.test('@') // ✅
元字符
下列字符是特殊的:
\
/
[ ]
( )
{ }
?
+
*
|
.
^
$
它們是特殊的,因為它們是正則表達式模式中具有意義的控制字符,因此如果要將它們用作匹配字符,則需要將它們進行轉義,即在它們之前加上反斜槓符號:
/^\\$/
/^\^$/ // /^\^$/.test('^') ✅
/^\$$/ // /^\$$/.test('$') ✅
字符串邊界
\b
和\B
可讓您檢查字符串是否位於單詞的開始或結尾:
\b
:匹配單詞的開頭或結尾中的一組字符\B
:匹配不在單詞的開頭或結尾中的一組字符
例如:
'I saw a bear'.match(/\bbear/) // Array ["bear"]
'I saw a beard'.match(/\bbear/) // Array ["bear"]
'I saw a beard'.match(/\bbear\b/) // null
'cool\_bear'.match(/\bbear\b/) // null
使用正則表達式替換
我們已經看到了如何檢查字符串是否包含模式。
我們還看到了如何將字符串的部分提取到數組中,並找到匹配模式的。
現在讓我們看一下如何根據一個模式替換字符串的部分。
JavaScript中的String
對象具有一個replace()
方法,它可以在字符串上執行單一替換,可以在不使用正則表達式的情況下使用:
"Hello world!".replace('world', 'dog') // Hello dog!
"My dog is a good dog!".replace('dog', 'cat') // My cat is a good dog!
此方法還接受正則表達式作為參數:
"Hello world!".replace(/world/, 'dog') // Hello dog!
只有在使用g
標誌時,才能在JavaScript中通過正則表達式替換字符串中的多個匹配:
"My dog is a good dog!".replace(/dog/g, 'cat') // My cat is a good cat!
通過使用$1
引用匹配的分組,我們可以做更有趣的事情,例如移動字符串的部分:
"Hello, world!".replace(/(\w+), (\w+)!/, '$2: $1!!!')
// "world: Hello!!!"
您可以使用函數,而不是字符串,來完成更高級的事情。它將接收與String.match(RegExp)
或RegExp.exec(String)
返回的一樣的一系列參數,根據分組的數量而有所不同:
"Hello, world!".replace(/(\w+), (\w+)!/, (matchedString, first, second) => {
console.log(first);
console.log(second);
return `${second.toUpperCase()}: ${first}!!!`
})
// "WORLD: Hello!!!"
貪婪模式
預設情況下,正則表達式被認為是貪婪的。
這是什麼意思?
接受此模式之後,請看此正則表達式
/\$(.+)\s?/
它應該從字符串中提取一個金額
/\$(.+)\s?/.exec('This costs $100')[1]
//100
但是,如果我們在數字之後有更多的單詞,它將進行匹配:
/\$(.+)\s?/.exec('This costs $100 and it is less than $200')[1]
//100 and it is less than $200
為什麼?因為正則表達式模式在$
符號後面使用.+
匹配任何字符,並且它在達到字符串結束時才停止。然後,它在結束的位置結束,因為\s?
使結尾的空格是可選的。
要修復這個問題,我們需要告訴正則表達式是懶惰的,並且只執行可能的最小匹配數量。我們可以使用?
符號在量詞後進行這樣的操作:
/\$(.+?)\s/.exec('This costs $100 and it is less than $200')[1]
//100
我刪除了
\s
後面的?
,否則它只匹配第一個數字,因為空格是可選的
所以,?
根據位置的不同意義不同,因為它既可以是一個量詞,又可以是一個懶惰模式指示符。
先行斷言:根據後面的字符串進行匹配
使用?=
來匹配一個字符串後面跟著特定子字符串:
/Roger(?=Waters)/
?!
執行相反的操作,如果字符串不後跟著特定子字符串,則進行匹配:
/Roger(?!Waters)/
後行斷言:根據前面的字符串進行匹配
這是一個ES2018功能。
先行斷言使用?=
符號。後行斷言使用?<=
。
/(?<=Roger) Waters/
/(?<=Roger) Waters/.test('Pink Waters is my dog')
// false
/(?<=Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician')
// true
透過使用?<!
進行否定操作,則匹配字符串不以特定子字符串開頭:
/(?<!Roger) Waters/
/(?<!Roger) Waters/.test('Pink Waters is my dog') // true
/(?<!Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') // false
正則表達式和Unicode
在處理Unicode字符串時,使用u
標誌是強制性的,特別是當您可能需要處理平面字符串時。它們沒有包含在頭1600個Unicode字符中。
例如表情符號,但不僅限於此。
如果不添加該標誌,則該簡單的正則表達式,應該匹配一個字符,將無法正常工作,因為對於JavaScript而言,該表情符號由2個字符(請參閱JavaScript中的Unicode)內部表示。
因此,請始終使用u
標誌。
就像普通字符一樣,Unicode也可以處理範圍:
/[a-z]/.test('a') // ✅
/[1-9]/.test('1') // ✅
/[🐶-🦊]/u.test('🐺') // ✅
/[🐶-🦊]/u.test('🐛') // ❌
JavaScript檢查內部代碼表示,因此🐶 < 🐺 < 🦊,因為\u1F436
< \u1F43A
< \u1F98A
。請查看完整的Emoji列表以獲取這些代碼,並了解其順序(提示:macOS的Emoji選擇器中的某些表情符號按順序混合排列,不要依賴它)。
Unicode屬性逃逸
正如我們上面所見,您可以使用\d
來匹配任何數字,\s
來匹配任何非空格字符,\w
來匹配任何字母字符,等等。
Unicode屬性逃逸是一個ES2018功能,它將此概念擴展到所有Unicode字符上,引入了\p{}
及其否定形式\P{}
。
任何Unicode字符都有一組屬性。例如,Script
確定語言系列,ASCII
是一個boolean,對於ASCII字符為true,等等。您可以將此屬性放在大括號相對應的括號中,並且正則表達式在檢查屬性為真時進行測試:
/^\p{ASCII}+$/u.test('abc') // ✅
/^\p{ASCII}+$/u.test('ABC@') // ✅
/^\p{ASCII}+$/u.test('ABC🙃') // ❌
ASCII_Hex_Digit
是另一個布爾屬性,用於檢查字符串是否僅包含有效的十六進制數字:
/^\p{ASCII\_Hex\_Digit}+$/u.test('0123456789ABCDEF') // ✅
/^\p{ASCII\_Hex\_Digit}+$/u.test('h') // ❌
還有許多其他布爾屬性,只需在大括號內添加屬性名稱即可檢查它們,包括Uppercase
、Lowercase
、White_Space
、Alphabetic
、Emoji
等等:
/^\p{Lowercase}$/u.test('h') // ✅
/^\p{Uppercase}$/u.test('H') // ✅
/^\p{Emoji}+$/u.test('H') // ❌
/^\p{Emoji}+$/u.test('🙃🙃') // ✅
除了這些二進制屬性外,您還可以檢查任何Unicode字符的任何屬性以匹配特定值。在此示例中,我檢查字符串是否是希臘字母或拉丁字母:
/^\p{Script=Greek}+$/u.test('ελληνικά') // ✅
/^\p{Script=Latin}+$/u.test('hey') // ✅
詳細了解所有可用的屬性,您可以直接在TC39提案上了解詳細信息。
示例
從字符串中提取數字
假設您需要提取一個數字,字符串中只包含數字,可以使用/\d+/
:
'Test 123123329'.match(/\d+/) // Array [ "123123329" ]
匹配電子郵件地址
一種簡單的方法是檢查@
符號之前和之後的非空格字符,使用\S
:
/(\S+)@(\S+)\.(\S+)/
/(\S+)@(\S+)\.(\S+)/.exec('[[email protected]](/cdn-cgi/l/email-protection)')
//["[[email protected]](/cdn-cgi/l/email-protection)", "copesc", "gmail", "com"]
然而,這只是一個簡化的示例,因為許多無效的郵件地址仍會滿足該正則表達式。
捕獲雙引號之間的文本
假設您有一個字符串包含雙引號內的內容,並且您希望提取該內容。
最好的方法是使用一個捕獲分組,因為我們知道匹配從"
開始並以"
結束的字符,並且我們可以輕鬆地將其定位,但我們還希望從結果中刪除這些引號。
我們將在result[1]
中找到我們需要的內容:
const hello = 'Hello "nice flower"'
const result = /"([^']\*)"/.exec(hello)
//Array [ "\"nice flower\"", "nice flower" ]
獲取HTML標籤內的內容
例如,獲取span標籤內的內容,允許標籤內部有任意數量的參數:
/<span\b[^>]\*>(.*\?)<\/span>/
/<span\b[^>]\*>(.*\?)<\/span>/.exec('test')
// null
/<span\b[^>]\*>(.*\?)<\/span>/.exec('<span>test</span>')
// ["<span>test</span>", "test"]
/<span\b[^>]\*>(.*\?)<\/span>/.exec('<span class="x">test</span>')
// ["<span class="x">test</span>", "test"]