在 JavaScript 中,正则表达式(Regular Expressions)也是对象。这些模式被用于 RegExp 的 exec 和 test 方法, 以及 String 的 match、replace、search 和 split 方法。
创建一个正则表达式
你可以使用以下两种方法之一构建一个正则表达式:
使用一个正则表达式字面量,其由包含在斜杠之间的模式组成,如下所示:
1 | var regex = /ab+c/; |
或者调用 RegExp 对象的构造函数,如下所示:
1 | let regex = new RegExp("ab+c"); |
正则表达式字符匹配攻略
正则表达式是匹配模式,要么匹配字符,要么匹配位置。
两种模糊匹配
如果正则只有精确匹配是没多大意义的,比如 /hello/ ,也只能匹配字符串中的 "hello" 这个子串。
1 | var regex = /hello/; |
正则表达式之所以强大,是因为其能实现模糊匹配。
而模糊匹配,有两个方向上的“模糊”:横向模糊和纵向模糊。
横向模糊匹配
横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的。其实现的方式是使用量词。譬如
{m,n},表示连续出现最少m次,最多n次。比如/ab{2,5}c/表示匹配这样一个字符串:第一个字符是“a”,接下来是2到5个字符“b”,最后是字符“c”。
测试如下:1
2
3
4var regex = /ab{2,5}c/g;
var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
console.log( string.match(regex) );
// => ["abbc", "abbbc", "abbbbc", "abbbbbc"]
Tips: 案例中用的正则是 /ab{2,5}c/g ,后面多了 g ,它是正则的一个修饰符。表示全局匹配,即在目标字符串中按顺序找到满足匹配模式的所有子串,强调的是“所有”,而不只是“第一个”。g 是单词 global 的首字母。
纵向模糊匹配
纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能。其实现的方式是使用字符组。譬如
[abc],表示该字符可以是字符 “a”、“b”、“c” 中的任何一个。比如/a[123]b/可以匹配如下三种字符串:”a1b”、”a2b”、”a3b”。
测试如下:1
2
3
4var regex = /a[123]b/g;
var string = "a0b a1b a2b a3b a4b";
console.log( string.match(regex) );
// => ["a1b", "a2b", "a3b"]
要掌握横向和纵向模糊匹配,基本能解决很大部分正则匹配问题。
字符组
需要强调的是,虽叫字符组(字符类),但只是其中一个字符。例如 [abc] ,表示匹配一个字符,它可以是 “a”、“b”、“c” 之一。
范围表示法
如果字符组里的字符特别多的话,怎么办?可以使用范围表示法。例如 [123456abcdefGHIJKLM] ,可以写成 [1-6a-fG-M] 。用连字符 - 来省略和简写。因为连字符有特殊用途,如果要匹配 “a”、“-”、“z” 这三者中任意一个字符,该怎么做呢?不能写成 [a-z] ,因为其表示小写字符中的任何一个字符。可以写成如下的方式:[-az] 或 [az-] 或 [a\-z] 。即要么放在开头,要么放在结尾,要么转义。总之不会让引擎认为是范围表示法就行了。
测试如下:1
2
3var regex = new RegExp(/[1-6a-fG-M]/);
console.log( regex.test('123456abcdefGHIJKLM') );
// => true
排除字符组
纵向模糊匹配,还有一种情形就是,某位字符可以是任何东西,但就不能是 “a”、”b”、”c” 。此时就是排除字符组(反义字符组)的概念。例如 [^abc] ,表示是一个除 “a”、”b”、”c” 之外的任意一个字符。字符组的第一位放 ^(脱字符),表示求反的概念。
测试如下:1
2
3var regex = new RegExp(/[^abc]/);
console.log( regex.test('abc') ); // => false
console.log( regex.test('222') ); // => true
常见的简写形式
有了字符组的概念后,一些常见的符号我们也就理解了。因为它们都是系统自带的简写形式。
\d就是[0-9]。表示是一位数字。记忆方式:其英文是digit(数字)。\D就是[^0-9]。表示除数字外的任意字符。\w就是[0-9a-zA-Z_]。表示数字、大小写字母和下划线。记忆方式:w是word的简写,也称单词字符。\W是[^0-9a-zA-Z_]。非单词字符。\s是[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。记忆方式:s是 space character 的首字母。\S是[^ \t\v\n\r\f]。 非空白符。.就是[^\n\r\u2028\u2029]。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。记忆方式:想想省略号…中的每个点,都可以理解成占位符,表示任何类似的东西。
量词
量词也称重复。掌握 {m,n} 的准确含义后,只需要记住一些简写形式。
简写形式
{m,}表示至少出现m次。{m}等价于{m,m},表示出现m次。?等价于{0,1},表示出现或者不出现。记忆方式:问号的意思表示,有吗?+等价于{1,},表示出现至少一次。记忆方式:加号是追加的意思,得先有一个,然后才考虑追加。*等价于{0,},表示出现任意次,有可能不出现。记忆方式:看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来。
测试如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// {m,} 表示至少出现m次。
var regex1 = new RegExp(/ac{2,}r/);
regex1.test('accr'); // => true
regex1.test('acr'); // => false
// {m} 等价于 {m,m},表示出现 m 次
var regex2 = new RegExp(/ac{3}r/);
regex2.test('acccr'); // => true
regex2.test('accr'); // => false
// ? 等价于{0,1},表示出现或者不出现。记忆方式:问号的意思表示,有吗?
var regex3 = new RegExp(/abc?r/);
console.log(regex3.test('abcr')); // => true
console.log(regex3.test('abbr')); // => false
console.log(regex3.test('abr')); // => true
// + 等价于{1,},表示出现至少一次。记忆方式:加号是追加的意思,得先有一个,然后才考虑追加。
var regex4 = new RegExp(/ac+r/);
console.log(regex4.test('acr')); // => true
console.log(regex4.test('accr')); // => true
console.log(regex4.test('abr')); // => false
// * 等价于{0,},表示出现任意次,有可能不出现。记忆方式:看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来。
var regex5 = new RegExp(/abc*r/);
console.log(regex5.test('abcr')); // => true
console.log(regex5.test('abccr')); // => true
console.log(regex5.test('abr')); // => true
console.log(regex5.test('ar')); // => false
贪婪匹配和惰性匹配
例子如下:1
2
3
4var regex = /\d{2,5}/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) );
// => ["123", "1234", "12345", "12345"]
其中正则 /\d{2,5}/ ,表示数字连续出现2到5次。会匹配2位、3位、4位、5位连续数字。但是其是贪婪的,它会尽可能多的匹配。你能给我6个,我就要6个。你能给我3个,我就要3个。反正只要在能力范围内,越多越好。
而惰性匹配,就是尽可能少的匹配:
1 | var regex = /\d{2,5}?/g; |
其中 /\d{2,5}?/ 表示,虽然2到5次都行,当2个就够的时候,就不在往下尝试了。
通过在量词后面加个问号就能实现惰性匹配,因此所有惰性匹配情形如下:
{m,n}?
{m,}?
??
+?
*?
对惰性匹配的记忆方式是:量词后面加个问号,问一问你知足了吗,你很贪婪吗?
多选分支
一个模式可以实现横向和纵向模糊匹配。而多选分支可以支持多个子模式任选其一。具体形式如下:(p1|p2|p3) ,其中 p1、p2 和 p3 是子模式,用 |(管道符)分隔,表示其中任何之一。例如要匹配 “good” 和 “nice” 可以使用 /good|nice/ 。
测试如下:
1 | var regex = /good|nice/g; |
但有个事实我们应该注意,比如我用 /good|goodbye/ ,去匹配 “goodbye” 字符串时,结果是 “good”
1 | var regex = /good|goodbye/g; |
而把正则改成 /goodbye|good/ ,结果是:
1 | var regex = /goodbye|good/g; |
也就是说,分支结构也是惰性的,即当前面的匹配上了,后面的就不再尝试了。
案例分析
匹配字符,无非就是字符组、量词和分支结构的组合使用。
下面找几个例子演练一下(其中,每个正则并不是只有唯一写法,可以有多种,就不一一列举了):
匹配16进制颜色值
要求匹配:
#ffbbad、#Fc01DF、#FFF、#ffE
分析:
表示一个16进制字符,可以用字符组 [0-9a-fA-F] 。
其中字符可以出现3或6次,需要是用量词和分支结构。
使用分支结构时,需要注意顺序。
1 | var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g; |
匹配时间
要求匹配:
23:59、02:07
分析:
共4位数字,第一位数字可以为 [0-2] 。
当第1位为2时,第2位可以为 [0-3] ,其他情况时,第2位为 [0-9] 。
第3位数字为 [0-5] ,第4位为 [0-9]
1 | var regex = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/; |
如果也要求匹配7:9,也就是说时分前面的0可以省略。
此时正则变成:
1 | var regex = /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/; |
匹配日期
比如
yyyy-mm-dd格式为例。要求匹配:2017-06-10
分析:
年,四位数字即可,可用 [0-9]{4} 。
月,共12个月,分两种情况01、02、……、09和10、11、12,可用 (0[1-9]|1[0-2]) 。
日,最大31天,可用 (0[1-9]|[12][0-9]|3[01]) 。
正则如下:
1 | var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/; |
Tips: /^[0-9] 这个 ^ 表示:匹配以数字开始的字符串(从左向右匹配)。$/ 这个 $ 表示:匹配以数字结尾的字符串(从后向前匹配)。
如果 ^ 在 [] 中出现,那么就是非的意思了,不再是从开头匹配的意思。
正则表达式位置匹配攻略
正则表达式是匹配模式,要么匹配字符,要么匹配位置。
什么是位置呢?
位置是相邻字符之间的位置。比如,下图中箭头所指的地方:
如何匹配位置呢?
在ES5中,共有6个锚字符:
^$\b\B(?=p)(?!p)
^和$
^(脱字符)匹配开头,在多行匹配中匹配行开头。$(美元符号)匹配结尾,在多行匹配中匹配行结尾。
比如我们把字符串的开头和结尾用”#”替换(位置可以替换成字符的!):
1 | var result = "hello".replace(/^|$/g, '#'); |
多行匹配模式时,二者是行的概念,这个需要我们的注意:
1 | var result = "I\nlove\njavascript".replace(/^|$/gm, '#'); |
Tips:JavaScript 正则标志 /g , /i , /m 说明
1、
/g表示该表达式将用来在输入字符串中查找所有可能的匹配,返回的结果可以是多个。如果不加/g最多只会匹配一个
2、/i表示匹配的时候不区分大小写
3、/m表示多行匹配,什么是多行匹配呢?就是匹配换行符两端的潜在匹配。影响正则中的^$符号。
\b 和 \B
\b 是单词边界,具体就是 \w 和 \W 之间的位置,也包括 \w 和 ^ 之间的位置,也包括 \w 和 $ 之间的位置。
比如一个文件名是”[JS] Lesson_01.mp4”中的 \b,如下:
1 | var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#'); |
为什么匹配结果是这样呢?分析如下:
首先,我们知道,\w 是字符组 [0-9a-zA-Z_] 的简写形式(单词字符),即 \w 是字母数字或者下划线的中任何一个字符。而 \W 是排除字符组 [^0-9a-zA-Z_] 的简写形式(非单词字符),即 \W 是 \w 以外的任何一个字符。
此时我们可以看看”[#JS#] #Lesson_01#.#mp4#”中的每一个”#”,是怎么来的。
第一个”#”,两边是”[“与”J”,是
\W和\w之间的位置。
第二个”#”,两边是”S”与”]”,也就是\w和\W之间的位置。
第三个”#”,两边是空格与”L”,也就是\W和\w之间的位置。
第四个”#”,两边是”1”与”.”,也就是\w和\W之间的位置。
第五个”#”,两边是”.”与”m”,也就是\W和\w之间的位置。
第六个”#”,其对应的位置是结尾,但其前面的字符”4”是\w,即\w和$之间的位置。
知道了 \b 的概念后,那么 \B 也就相对好理解了。\B 就是 \b 的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉 \b ,剩下的都是 \B 的。
具体说来就是 \w 与 \w 、 \W 与 \W 、^ 与 \W ,\W 与 $ 之间的位置。
比如上面的例子,把所有 \B 替换成 “#”:
1 | var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#'); |
(?=p) 和 (?!p)
(?=p),其中p是一个子模式,即p前面的位置。
比如(?=l),表示’l’字符前面的位置,例如:
1 | var result = "hello".replace(/(?=l)/g, '#'); |
而(?!p)就是(?=p)的反面意思,比如:
1 | var result = "hello".replace(/(?!l)/g, '#'); |
二者的学名分别是 positive lookahead 和 negative lookahead。
中文翻译分别是正向先行断言和负向先行断言。
ES6中,还支持 positive lookbehind 和 negative lookbehind。
具体是(?<=p)和(?<!p)。
比如(?=p),一般都理解成:要求接下来的字符与p匹配,但不能包括p的那些字符。
而在个人看来(?=p)就与^一样好理解,就是p前面的那个位置。
x(?=y)仅匹配被y跟随的x。 例如,/Jack(?=Sprat)/只有在 ‘Jack’ 后面紧跟着 ‘Sprat’ 时,才会匹配它。/Jack(?=Sprat|Frost)/只有在 ‘Jack’ 后面紧跟着 ‘Sprat’ 或 ‘Frost’ 时,才会匹配它。然而,’Sprat’ 或 ‘Frost’ 都不是匹配结果的一部分。
x(?!y)仅匹配不被y跟随的x。举个例子,/\d+(?!\.)/只会匹配不被点(.)跟随的数字。/\d+(?!\.)/.exec('3.141')匹配”141”,而不是”3.141”
相关断言(Assertions)学习资料可以看看这里:MDN、博客、百科
位置的特性
对于位置的理解,我们可以理解成空字符 “”。
比如”hello”字符串等价于如下的形式:
1 | "hello" === "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + ""; // => true |
也等价于:
1 | "hello" == "" + "" + "hello" // => true |
因此,把 /^hello$/ 写成 /^^hello$$$/ ,是没有任何问题的:
1 | var result = /^^hello$$$/.test("hello"); |
甚至可以写成更复杂的:
1 | var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello"); |
也就是说字符之间的位置,可以写成多个。
把位置理解空字符,是对位置非常有效的理解方式。
相关案例
不匹配任何东西的正则
让你写个正则不匹配任何东西/.^/
因为此正则要求只有一个字符,但该字符后面是开头。
数字的千位分隔符表示法
比如把”12345678”,变成”12,345,678”。
可见是需要把相应的位置替换成”,”。
思路是什么呢?
弄出最后一个逗号
使用 (?=\d{3}$) 就可以做到:
1 | // 在字符最后3个数字前面加一个逗号 |
弄出所有的逗号
因为逗号出现的位置,要求后面3个数字一组,也就是 \d{3} 至少出现一次。
此时可以使用量词+:
1 | var result = "12345678".replace(/(?=(\d{3})+$)/g, ',') |
匹配其余案例
写完正则后,多验证几个案例,此时我们会发现问题:
1 | var result = "123456789".replace(/(?=(\d{3})+$)/g, ',') |
因为上面的正则,仅仅表示把从结尾向前数,一但是3的倍数,就把其前面的位置替换成逗号。因此才会出现这个问题。
怎么解决呢?我们要求匹配到这个位置不能是开头。
我们知道匹配开头可以使用 ^ ,但要求这个位置不是开头怎么办?(?!^) 测试如下:
1 | var string1 = "12345678", string2 = "123456789"; |
123456.3435 如果要匹配这种数据格式呢?就是保留小数并且千分位逗号分割
1 | // 小数点前面的数字,每隔三个数加一个 ',' |
Tips: var a = 222122122.6754;var b = a.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,'); 这行代码,可以实现四舍五入保留2位小数点,并且千分位逗号分割,这是项目中很常见的一个需求
test, exec, match, replace 用法介绍
注:pattern 为 RegExp 的实例, str 为 String 的实例
| 用法 | 说明 | 返回值 |
|---|---|---|
pattern.test(str) |
判断 str 是否包含匹配结果 |
包含返回 true ,不包含返回 false |
pattern.exec(str) |
根据 pattern 对 str 进行正则匹配 |
返回匹配结果数组,如匹配不到返回 null |
str.match(pattern) |
根据 pattern 对 str 进行正则匹配 |
返回匹配结果数组,如匹配不到返回 null |
str.replace(pattern, replacement) |
根据 pattern 进行正则匹配,把匹配结果替换为 replacement |
返回一个新的字符串 |
str.search(pattern) |
根据 pattern 对 str 进行正则匹配 |
返回匹配到的位置索引,如匹配不到返回 -1 |
str.split(pattern) |
pattern 可以是一个字符串或正则表达式,使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法 |
返回源字符串以分隔符出现位置分隔而成的一个 Array |
Tips:当字符串为空时,
split()返回一个包含一个空字符串的数组,而不是一个空数组,如果字符串和分隔符都是空字符串,则返回一个空数组。
如果空字符串('')被用作分隔符,则字符串会在每个字符之间分割。
正则用法更多详情