前端笔记—完整的正则表达式

对于一个前端开发者来说,正则表达式让人又爱又恨,爱是因为人人都会,恨又是一些问题而又不能很好解决。

我将在下文里,分享自己学习正则表达式笔记,
尽量将正则表达式的全部概念分享给大家,了解全部正则概念读此一篇足以应付所有问题,让大家有个系统的了解,共同学习。由于自己理解可能有偏差,如果有不妥之处,还望指出,一定拜谢。

能了解哪些内容

  1. 正则介绍:字符串是运用广泛的数据类型,正则是处理字符串的得力助手
  2. 搜索匹配:搜索内容时,忘记了具体拼写,可以使用通配符,如javasc**pt
  3. 灵活运用:开发过程中遇到匹配的问题,如何运用高效简介的正则表达式
  4. 高级用法:一些不常见但是非常简化开发的特殊用法,例如String.protorype.replace

详细介绍

  1. 正则表达式( Regular Expression )是种文本模式,在JS中是Object的一种( RegExp )
  2. 正则语法几乎运用在所有计算机语言中
  3. 正则不仅是用来校验的,更是用来截取字符串的,例如 /stud(y|ies)/ /stud(?:y|ies)/ 用来做校验没有任何区别,但是用来截取字符串,就有些区别了,学会正则能让你更善于操作字符串

普通字符

  1. 包括常见字符 a - Z A - Z 0 - 9 标点符号 ()[]{}<>《》等常见符号
  2. 一些诸如 ()[]<> 特别是有带有功能的 + . ? 必须使用反斜杠,或者用中括号包裹,如匹配问号 \?[?]

ep1. 从下列字符串中找到 JavaScript

1
2
3
var str = "I like JavaScript very much.";
var reg = /JavaScript/;
str.match(reg); // ["javascript", index: 7] (PS: 它真的是数组)

ep2. 是否能忽略 javascript 的大小写

1
2
3
4
5
6
var str = "I like JavaScript very much.";
var reg = /javascript/;
str.match(reg); // null
var reg2 = /javascript/i;
str.match(reg2) // ["javascript", index: 7]

i 表示忽略字符串中的字母大小写

ep3. 找到《设计模式》

1
2
3
4
var str = "我有一本 JavaScript 的《设计模式》。";
var reg = /《设计模式》/;
str.match(reg) // ["《设计模式》", index: 17]

任意符

. 点号表示任意符,可匹配所有的字符,如果仅想表示.,则需写成\.[.]

特殊字符( 元字符 )

  1. 如果你想找出一句话中所有数字,如果使用普通字符,可能写起来非常困难了。因此,有了元字符
  2. 元字符是一些特殊的符号,可以指代一个或者一类某种集合的符号
  3. 这里只介绍些叫常见的元字符,文末的附录中,有比较全的元字符表( 来自于菜鸟教程 )

比较常见的元字符有如下:

符号 描述
\d <==> [0-9] 匹配所有的数字中的一个
\D <==> [^0-9] 匹配不是数字中的字符集合的任意一个
\b 匹配一个单词边界,单词和空格间的位置,如 /er\b/ 可匹配”never”中的”er”,但不匹配 “verb”中的”er”
\B 匹配非单词边界,示例与 \b 匹配相反
\w <==> [a-zA-Z0-9_] 匹配大小写字母、数字、下划线中的一个
\W <==> [^a-zA-Z0-9_] 匹配不是大小写字母、数字、下划线的字符集中的任意一个
\s <==> [ \f\n\r\t\v] 匹配空白字符( 包括如中括号中列举依次为空格、换页符、换行符、回车符、制表符、垂直制表符 )中的一个
\S <==> [^ \f\n\r\t\v] 匹配非空白字符中的一个

ep1. 从下列字符中找到数字

1
2
3
var str = "I have 11 books related to javascript.";
var reg = /[\d]/;
str.match(reg); // ["1", index: 7]

  1. \d 表示所有数字
  2. 这里只找到了第一个“1”
1
2
3
var str = "I have 11 books related to javascript.";
var reg = /[\d]/g;
str.match(reg); // ["1", "2"]

g 表示匹配字符串中出现过的所有内容

限定符

限定符有 6 种,? + * {m} {m,} {m,n}

字符 含义 其他写法
? 匹配前面子表达式出现 0 次或 1 次 {0,1}
+ 匹配前面子表达式出现 至少 1 次 {1,}
* 匹配前面子表达式出现 自然数 次 {0, }
{m} 匹配前面子表达式出现 m 次 -
{m,} 匹配前面子表达式出现 至少 m 次 -
{m,n} 匹配前面子表达式出现 m 至 n 次 -

ep1. 匹配下列含有两个重复字母 o 的单词

1
2
3
4
5
6
var str = "I have 11 good books.";
var reg = /[a-zA-Z]*o{2}[a-zA-Z]*/i;
str.match(reg); // ["good", index: 10]
var reg = /[a-zA-Z]*o{2}[a-zA-Z]*/ig;
str.match(reg2); // ["good", "books"] // 匹配出所有的

  1. [a-z] 表示小写字母 a 至 z 中的一个
  2. [A-F] 表示大写字母 A 至 F 中的一个
  3. [0-8] 表示数字 0 至 8 中的一个
  4. [_7c] 表示字符 _ 7 c 中的一个
  5. 我们用中括号[]来匹配其中所有字符其中的一个
定位符号
  1. ^ 写在开始表示从 ^ 后面符号开始
  2. $ 写在结束表示从 $ 前面符号结束

注意 ^ 的用法不要与 负值符号 ^ 混淆,负值符号 ^ 用在中括号内,比如非数字集合 [^\d]

选择符

()选择符运用非常普遍,它可以精确再捕获()内部的内容,下文讲 match exec replace方法讲到具体用法,正因为能再捕获精确内容,我们很多时候是不需要这部分内容的

(?:) 在括号内的头部加上?: ,可以消除精确捕获,举个例子

  1. /stu(dent)/ 可以捕获 student 和 dent 两个字符串
  2. /stu(?:dent)/ 只能捕获 student 一个字符串

正向预查 (?!) (?=) 和反向预查 (?<=) (?<!) 将在下下节介绍

组与回溯引用( 也叫反向引用 )

组的概念很简单,有小括号包括的字符成为一个组,([0-9])([a-z])中 [0-9] 称为组1, [a-z]称为组2

下文我们可以知道,在 String.prototype.replace 中,可以用 $n (0<n<100) 表示某组,而在正则中,我们同样可以使用组的概念,用 \n (0<n<100) 来代表组

ep1. 手机号码的匹配,我们以 344 数字个数表示电话,中间可以同时用 - / 或不分隔等 4 种方式表示( 例如可以是 133-3333-3333 ,但不能是 133-3333/3333 )

1
2
3
4
5
6
7
var a = /1\d{2}([\-\/\ ]?)\d{4}\1\d{4}/
// 示例中的 \1 代表组1
a.test('133-4444-5555') // true
a.test('133-4444/5555') // false
a.test('133/4444/5555') // true
a.test('13344445555') // true
正向预查和反向预查

正向预查是 (?=) 正向肯定匹配和 (?!) 正向否定匹配

/student(?=s)/ 匹配 students 中的 student

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var p = /student(?=s)/g; // t 后面一定要带 s
var str = 'abc student, def students';
var p2 = /student(?:s)/g;
str.match(p2); // ["students"]
// 正向肯定匹配
str.match(p); // ["student"]
str.search(p); // 17 , 可以确定,上述只匹配 students 中的 student 字符串
// 正向否定匹配
var p3 = /student(?!s)/g;
str.match(p3); // ["student"]
str.search(p3); // 4 , 可以确定,上述匹配的 student 不能在 students 中

反向预查是 (?<=) 反向肯定匹配 和 (?<!) 反向否定匹配。正向预查是检查字符尾部,而反向预查是检查字符前面

1
2
3
4
5
6
7
8
9
10
11
var str = 'Heroes are greate. He is a hero.'
var p = /hero/ig // 可匹配两个 hero
str.match(p); // ["Hero", "hero"]
// 反向肯定匹配
var p1 = /(?<=a )hero/ig; // 只能匹配前面有 a 的小写的 hero
str.match(p1); // ["hero"]
// 反向否定匹配
var p2 = /(?<!a )hero/ig; // 只能匹配前面没有 a 的大写的 Hero
str.match(p2); // ["Hero"]

标志

  1. 正则表达式有四个可选标志,进行辅助筛选,可共同使用,无顺序区别
  2. 如不区分大小写匹配 Python 的,/python/i
  3. 以下是摘自 MDN Web Docs,事实上,我也只用过前两个,后面两个,具体功能我也不清楚
    | 标志 | 描述 |
    | :-: | :-: |
    | g | ( global ) 全局搜索 |
    | i | ( ignoreCase ) 不区分大小写搜索 |
    | m | ( multiline ) 多行搜索 |
    | y | 执行“粘性”搜索,匹配从目标字符串的当前位置开始,可以使用y标志 |

运算符优先级

  1. \ 转义符最高
  2. () [] 括号第二, (?:) (?=) 都属于 (), [^]属于 []
  3. ? + * {m} {m,} {m,n} 上文所讲的限定符
  4. ^ $ 定位符
  5. | 或符号

JavaScript中的正则

JS中的正则表达式的方法,一个是在 String.prototype 上,另外一个就是在 RegExp.prototype 上

JavaScript中有两种声明正则的方式

  1. 对象式 var r = new RegExp(pattern [,flag])

    • pattern 就是上述讲的包裹在 / / 内部的字符串
    • flag 就是上面提到的标志,如匹配不区分大小写的 Phython new RegExp('python', 'i')
  2. JavaScript的对象都有字面量形式,这个也不例外,上述字面量表示为 /python/i

正则的方法

关于正则所有方法,可以在 chrome 浏览器中输入

1
2
var r = new RegExp();
r. // 输入完点后,浏览器 console 可联想出所有 RegExp 实例的方法

比较常用的正则方法有 text exec

test

reg.test(str),验证 str 是否符合 reg 规则,返回值为 Boolean

ep1. 常见于前端校验写法中,校验 input 框内输入的字符是否符合8位16进制的卡片格式

1
2
3
var number = '329abcdf'
var r = /^[0-9a-fA-F]{8}$/;
r.test(number) // true

exec

reg.exec(str),找出正则规则中的字符串

ep1. 匹配所有包含 foo 的字符串

1
2
3
4
5
6
7
8
9
var str = 'table football, foosball';
var r = /foo*/g;
r.lastIndex; // 0
r.exec(str); // ["foo", index: 6]
r.lastIndex; // 9
r.exec(str); // ["foo", index: 16]
r.lastIndex; // 19
r.exec(str); // null
r.lastIndex; // 0

想必也看出了规律,正则实例 r 有一个 lastIndex 属性,指向当前匹配所在的位置

  1. 第一次匹配到 foo 时,r.exec(str) 返回了包含匹配到字符的数组,并将 lastIndex 值置为 9
  2. 知道匹配不出结果, r.exec(str) 返回了 null

ep2. 如果上述例子不匹配所有呢

1
2
3
4
5
6
7
8
9
var str = 'table football, foosball';
var r = /foo*/;
r.lastIndex; // 0
r.exec(str); // ["foo", index: 6]
r.lastIndex; // 0
r.exec(str); // ["foo", index: 6]
r.lastIndex; // 0
r.exec(str); // ["foo", index: 6]
r.lastIndex; // 0

每次都会从头开始找 foo ,而不会保留 lastIndex 的值

ep2. MDN Web Docs 上的例子

1
2
3
4
5
6
7
8
9
var regex1 = RegExp('foo*','g');
var str1 = 'table football, foosball';
var array1;
while ((array1 = regex1.exec(str1)) !== null) {
console.log(`Found ${array1[0]}. Next starts at ${regex1.lastIndex}.`);
// expected output: "Found foo. Next starts at 9."
// expected output: "Found foo. Next starts at 19."
}

字符串的相关方法

比较常用的与能使用正则表达式的字符串方法有 match search replace split

match

str.match(patten) 此方法与 RegExp.prototype.exec 非常类似,故下面对比说明两者区别

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
29
30
31
var str = '我喜欢看书,我看过了《围城》,《红楼梦》和《三国演义》';
/**
* 1. 此正则类似找书名 /\《[.]\》/ ,但是不能是 《》
* 2. 书名号中间的 () 是为了精确匹配出不带书名号的内容
*/
var patten = /\《([^\》]+)\》/;
var patten2 = /\《([^\》]+)\》/g;
/**
* 非全局匹配
* 结论:在非全局匹配下,可以认为没有区别
*/
str.match(patten); // ["《围城》", "围城", index: 10]
patten.exec(str); // 与上面一致
/**
* 全局匹配
* 结论:
* - RegExp.prototype.exec 略有优势,凭借 RegExp 独有的 lastIndex 记录功能
* - 对 () 包裹了精确查找的内容,使用 exec 更有优势
*/
str.match(patten2) // [ '《围城》', '《红楼梦》', '《三国演义》' ]
var array;
while ((array = patten2.exec(str)) !== null) {
console.log(array);
console.log(patten2.lastIndex);
}
// array: [ '《围城》', '围城', index: 10 ] patten2.lastIndex: 14
// array: [ '《红楼梦》', '红楼梦', index: 15 ] patten2.lastIndex: 20
// array: [ '《三国演义》', '三国演义', index: 21] patten2.lastIndex: 27

str.search(patten) 该方法非常简单,值得一提的是,它的速度快于 match 方法,非常适合仅查找,如需抓取详细信息字段,还需要用到match

1
2
3
4
var patten = /school/;
var str = 'I like to go to school.';
str.search(patten); // 16, 返回首次查到正则表达符的位置
str.search('money'); // -1, 表示未找到匹配字段
replace( 功能比较强大 👍 )

str.replace(regexp|substr, newSubStr|function) 第一个参数可以是正则或字符串,第二个参数可以是字符串或函数,且匹配后原字符串不改变

Ps: 第一个参数如果是字符串 substr ,仅仅第一个被匹配的字符串会被替换,如( ‘hello’.replace(‘l’, ‘f’) 结果是 ‘heflo’ ),如需完全匹配需使用正则表达式带上g参数,如 ‘hello’.replace(/l/g, ‘f’)

替换字符串 newSubStr 是特殊用法

第二个参数,可以插入下面的特殊变量名:

变量名 代表的值
$$ 由于 $ 有特殊功能,此写法相当于插入一个 “$”
$& 插入匹配的子串
$` 插入当前匹配的子串左边的内容
$’ 插入当前匹配的子串右边的内容
$n 假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串

第二个参数为函数时,函数有如下参数(如果第一个参数是正则表达式, 并且其为全局匹配模式, 那么这个方法将被多次调用, 每次匹配都会被调用)

变量名 代表的值
match 匹配的子串,等同于上面的 $&
p1,p2, … 等同于上面的$n,个数取决于正则内括号匹配的个数
offset 匹配到的子字符串在原字符串中的偏移量
string 被匹配的原字符串

以上是 MDN 的介绍,是不是有点迷糊,下面通过一些例子来细细讲解一下:

ep1: 简单的匹配替换

1
2
3
4
var patten = /\//g;
var str = '2018/04/14 10:00:00';
str.replace(patten, '-'); // 2018-04-14 10:00:00
// 如果patten中不指定 g, 则结果为 2018-04/14 10:00:00

ep2: 交换两个单词位置

1
2
3
var patten = /(\w+)\s(\w+)/; // 此处使用两个括号匹配,可用 $1, $2 表示
var str = 'Zhang San';
str.replace(patten, '$2 $1'); // 'San Zhang'

ep3:

  • 将文中的价格( 如 $1.21 )全部保存成带两位小数点的人名币货币单位( 如 ¥1.21 )
  • 针对括号匹配 ( 回答文中开头说的 /stud(y|ies)/ /stud(?:y|ies)/ 的区别 )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 正常思路
var str = '这本书需要 $3,那瓶水需要 $1.5,这个只要$0.5';
var patten = /\$(\d([.]\d*)?)/g;
str.replace(patten, function(match, p1, p2, offset, string) {
return '¥' + Number(p1).toFixed(2);
});
/**
* 结果:"这本书需要 ¥3.00,那瓶水需要 ¥1.50,这个只要¥0.50"
* 1. 第一个括号匹配为了选中 $ 后面数字
* 2. 第二个括号匹配为了同时匹配是否有小数,但是是没必要 作为 $2 被使用的
*/
// 故可以将 patten 改写成如下形式,添加 ?: 在匹配括号内部开头,第二个函数参数的 p2 也不需要了
var patten2 = /\$(\d(?:[.]\d*)?)/g;
str.replace(patten, function(match, p1, offset, string) {
return '¥' + Number(p1).toFixed(2);
});
// 结果:"这本书需要 ¥3.00,那瓶水需要 ¥1.50,这个只要¥0.50"

符号 ?: 的用法在元字符列表中有介绍,同时这个符号也诠释了,括号符可以匹配文字,但是不需要被使用的用法

ep4: 将下列句子每句话的单词首字母变为大写

1
2
3
4
5
var str = 'hello. i like school. I want to go to school. i like classmate too.';
var patten = /(?:\w+\ ?)+[.?!]/g;
str.replace(patten, function(match, offset, string) {
return match[0].toUpperCase() + match.slice(1);
});

split

str.split(patten [, limit]) 第一个参数是以什么方式分割,第二个参数表示获取的数组内容最大长度

该方法也比较简单,下面借用 MDN Web Docs 的例子

1
2
3
4
5
6
7
8
var names = "Harry Trump ;Fred Barney; Helen Rigby ; Bill Abel ;Chris Hand ";
console.log(names);
var re = /\s*;\s*/;
var nameList = names.split(re);
// [ "Harry Trump", "Fred Barney", "Helen Rigby", "Bill Abel", "Chris Hand " ]
// 注意最后一个单词的尾部空格保留了

有一个特殊的用法需要记住,当使用了捕获括号 () 时,同样看 MDN 例子

1
2
3
4
5
6
var myString = "Hello 1 word. Sentence number 2.";
var splits = myString.split(/(\d)/);
console.log(splits);
// [ "Hello ", "1", " word. Sentence number ", "2", "." ]
// 分隔符也会包含在输出内

正则的一些特殊用法列举

  1. () 括号选择器,可以筛选精确的匹配,String.prototype.replace 有详细介绍,(str (ing))中,可以用 $1, $2 分别表示 str, ing,在(str) 开始中使用 (?: str),str 将无法被 $n 捕获到
  2. 分组
  3. 反向引用
  4. 回溯引用等

附录

元字符表

菜鸟教程云字符表

菜鸟教程元字符表

参考

  1. MDN Web Docs
  2. 菜鸟教程