正则表达式一直是我心中的痛, 一是没有系统的学习方法, 二是总觉得并不是什么时候都能用上. 但是我相信大多数不太懂正则表达式的人, 都跟我有一样的想法. 要用上的时候, 又发现它十分重要. 好在我发现了一本开源书JavaScript正则表达式, 篇幅不长, 却非常体系化. 十分推荐大家好好读一下.😝
今天靠这篇文章一起来系统地梳理一下正则表达式的方方面面, 让你对正则表达式有一个整体的认识, 能够在平时的业务开发中用上, 甚至在看别人的项目和源码的时候, 不至于眉头紧锁
总览(插图)
匹配
正则表达式: 要么匹配字符, 要么匹配位置
匹配字符
模糊匹配
横向模糊
允许匹配长度不固定, 可以有多种情况
量词: {n, m}
, 意思是表示某个字符可以出现最少n次, 最多m次.
- 如图, 正则会横向去匹配满足条件的字符
纵向模糊
匹配到一个字符时, 可以是多种不同的字符, 也可以是多种可能
- 如图, 这个字符可以是
a
,b
,c
.
匹配字符组
纵向模糊
[abcdefg]
: 匹配任意一个字符
范围表示法 -
[a-g]
: 表示从字母a到g的任意一个字符
如果需要匹配符号
-
, 则需要转义\-
范围表示法的部分简写
-
digit(数字):
\d
表示[0-9]
-
word(单词):
\w
表示[0-9a-zA-Z]
数字、大小写字母和下划线 -
space(空白符)
\s
表示 空白符 如: 空格、水平制表符、垂直制表符、换行符、回车符、换页符。
如果将上述符号改为大写, 则表示非。
\D
则表示非数字,[^0-9]
排除字符组
-
^
:[^abc]
则表示不匹配abc中的任意一个
修饰符
修饰符的使用是可以叠加的
-
g
全局匹配: 在目标字符串中按顺序找到满足匹配模式的所有子串, g修饰符, 也会对一些api产生影响, 稍后再进行阐述. 如果没有加全局修饰符, 则在找到第一个符合条件的字符串后就会退出. -
m
多行匹配 -
i
忽略字母大小写 -
u
Unicode模式: 启用Unicode匹配 -
s
dotAll模式: 表示元字符.
, 匹配任何字符
let str = 'I\nLove\nYou' // 注意存在换行符 👈
str.replace(/^|$/, '#') // 输出: '#I\nLove\nYou' 非全局, 仅匹配一次
str.replace(/^|$/g, '#') // 输出: '#I\nLove\nYou#'
str.replace(/^|$/gm, '#') // 输出: '#I#\n#Love#\n#You#' 多行模式下有了行的概念, 所以按行查询
匹配位置
上述匹配都是找到对应的字符, 但是正则还可以匹配位置
什么是位置?
相邻的字符之间的位置, 既是本文中提到的位置
正则边界
在多行模式(
g
)下,^
,$
表示为开头语结尾
-
^
: 此符号不仅有表示非, 也有表示开头字符的意思在带有修饰符的情况下,
^
表示匹配开头, 在纵向匹配内, 则代表非.如:
/[^123]/
表示非123字符中的一个./^123/
以123字符开头. -
$
: 表示匹配字符结尾的意思如:
/123$/
带有该符号, 则表示匹配字符要求以123
结尾
单词边界
b
的是 boundary 的简写
\b
: 表示为单词的边界, 具体就是 \w
与 \W
之间的位置,也包括 \w
与 ^
之间的位置,和 \w
与 $
之间的位置
举个栗子:
首先,\w
的意思是 [0-9a-zA-Z]
, 基于这点来看一个例子
let str = `a*bc`
str.replace(/\b/g, '#') // replace 先简单地理解为替换就可以了
// 输出 "#a#*#bc#"
- 我们现在匹配的是位置, 所以它在所有边界处都插入了
#
。 在bc
之间, 由于这个位置两边都是单词, 它并不是单词的边界, 所以不会匹配到
\B
: 表示为非单词的边界
let str = `a*bc`
str.replace(/\b/g, '#') // replace 先简单地理解为替换就可以了
// 输出 "a*b#c"
- 注意插入的位置, 就可以很好理解了。 非单词的边界, 那就是两个单词之间
指定边界
以下两者匹配的都是模式
p
前的位置,p
可以是单个字符, 可以是一个分组
(?=p)
: p 前面的位置
举个栗子: 在 a 的前面插入一个字符 #
let str = `123abc`
str.replace(/(?=a)/g, '#')
// "123#abc"
(?!p)
: 非 p 前面的位置
let str = `123abc`
str.replace(/(?!a)/g, '#')
// "#1#2#3a#b#c#"
量词
在纵向匹配中, 我们可以根据
{m, n}
去决定至多至少应该匹配到多少次, 对于这种匹配模式也有简写
-
?
(有吗?)等价于
{0, 1}
表示出现或者不出现 -
+
(得先有一个)等价于
{1, }
表示至少出现一个, 并且可以有任意个 -
*
(随意)等价于
{0, }
表示出现任意次, 有可能也不出现
匹配模式
贪婪匹配模式
在满足条件的基础上,尽可能地多匹配
var regex = /\d{2,5}/g; // 匹配连续的数字至少2到5个
var string = "123 1234 12345 123456";
console.log( string.match(regex) ); // => ["123", "1234", "12345", "12345"]
惰性匹配模式
只要满足条件就不再往下匹配, 在量词后面加上一个
?
,就可以转变为惰性匹配
- 假设我们要获取一个节点的id
let str = '<div id="container" class="main"></div>'
let reg = /id=".*"/
str.match(reg) // 输出: [id="container" class="main"]
.
为通配符, 也就是任意字符都能够被匹配到,*
是至任意字符可以出现任意次, 并且该字符是贪婪的, 他就会尝试尽可能多的匹配, 这就导致了它匹配的是最后的双引号
- 转为惰性匹配
let str = '<div id="container" class="main"></div>'
let reg = /id=".*?"/ // 👈 加一个问号
str.match(reg) // 输出: [id="container"]
- 由于在匹配到 container 后的双引号就可以满足条件了, 所以不再继续往下匹配
分组
()
通过括号将括号内的正则表达式视为一个整体
捕获括号
在分组结构中, 存放在括号内的正则表达式匹配到的内容, 会被捕获存储起来, 我们称之为引用
那么, 怎么拿到被捕获到的内容呢?
- 通过正则构造函数的属性去访问
let str = 'abc-abcd-abcde'
str.match(/(\w{3})-(\w{4})-(\w{5})/g)
// 以正则从左到右出现的分组排序
RegExp.$1 // abc
RegExp.$2 // abcd
RegExp.$3 // abcde
当然, 也并非一定通过这种形式, 如文章一开始举例的一样,在replace函数中, 也可以直接使用分组捕获的内容, 另外除了数字类型的简写, 还有$_
等形式的简写
全名 | 简写 | 说明 |
---|---|---|
input | $_ | 最后搜索的字符串 |
lastMatch | $& | 最后匹配的文本 |
lastParen | $+ | 最后匹配的捕获组 |
leftContext | $` | input字符串中出现在lastMatch前面的文本 |
rightContext | $' | input字符串出现在lastMatch后面的文本 |
但是需要注意的是, RegExp的构造函数属性并不被推荐使用, JavaScript高级程序设计中的一句话
非捕获括号
(?:p)
:只匹配不捕获
const str = 'abc123abc'
const reg = /(\d+)/g
str.match(reg)
RegExp.$1 // 👈 '123'
const reg = /(?:\d+)/g // 非捕获
// or
const reg = /\d+/g
str.match(reg)
RegExp.$1 // 👈 ''
分支
分支结构其实就是通过
|
(管道符), 对匹配模式进行拆分, 当成 JavaScript 中的或
来理解即可.
如: (p1|p2|p3)
其中p1, p2, p3 就是子模式
APIs
string.match(regex) 匹配
如果正则带有g符号, 则返回的是一个匹配字符串数组, 而不是一个标准的匹配格式
const str = '123-ABC-123'
const reg1 = /ABC/
const reg2 = /ABC/g
str.match(reg1) // ['ABC', index: 4, input: '123-ABC-123', groups: undefined]
str.match(reg2) // ['ABC']
- 捕获
const str = '123-ABC-123'
const reg = /(ABC)/
str.match(reg) // ['ABC', 'ABC', index: 4, input: '123-ABC-123', groups: undefined]
注: 需要注意的一点是, match会将传入的字符串参数转为一个正则. (如下方的例子, 参数被误认为一个通配符. 这可能跟初始目的不符)
let str = '12.4'
str.match('.') // ['1', index: 0, input: '12.4', groups: undefined]
string.search(regex)
返回第一次匹配成功的索引, 否则则返回 -1
注: search方法与match类似, 会做一次隐式转换, 所以要注意上述问题
regex.exec(string)
功能与match相似, 但是比起match要更加强大, 在带有全局表示
g
的情况下, exec也会返回一个标准的匹配格式. 并且exec方法是有状态的(会记住上次匹配的位置)
const str = '123-ABC-456-EFG'
const reg = /\d{3,}/g
reg.exec(str) // ['123', index: 0, input: '123-ABC-123-EFG-456', groups: undefined]
reg.lastIndex // 3
// 第二次执行
reg.exec(str) // ['456', index: 8, input: '123-ABC-123-EFG-456', groups: undefined]
reg.lastIndex // 11
// 第三次执行
reg.exec(str) // null
reg.lastIndex // 0
// 此时再次执行 就会开始下一轮循环
regex.test(string)
如果带有全局修饰符
g
, test方法同样是"有状态"的
let reg = /a/g
let str = 'abc-abc-abc'
console.log(reg.test(str), reg.lastIndex)
// true 1
console.log(reg.test(str), reg.lastIndex)
// true 5
console.log(reg.test(str), reg.lastIndex)
// true 9
string.replace(regex, <function | string>)
replace方法虽叫做替代, 但是其实他非常强大, 因为我们可以假借替换之名做很多其实事情- 我 replace的第二个参数可以是一个回调函数, 也可以是一个字符串. 如果是一个回调函数则可以接收到5个参数
match
匹配到的内容$1~$9
捕获到的分组(这个参数存在的多少, 与你设置的捕获分组数量有关)index
当前索引input
输入的文本
由于我设置了两个捕获分组, 所有这里作为捕获内容的引用有 $1
, $2
两个参数
const str = "1234 2345 3456"
const reg = /(\d)\d{2}(\d)/g
str.replace(reg, function (match, $1, $2, index, input) {
console.log([match, $1, $2, index, input]);
})
/*
['1234', '1', '4', 0, '1234 2345 3456']
['2345', '2', '5', 5, '1234 2345 3456']
['3456', '3', '6', 10, '1234 2345 3456']
*/
怎么书写?
假设我们要匹配一个固定电话.
055188888888
0551-88888888
(0551)88888888
1. 了解各个部分的模式规则
这三个字符串的共同点在于, 由4位区号和8位号码组成, 而我们知道区号以0开头, 号码不以0开头. 所以
- 区号的的匹配规则为:
0\d{2, 3}
- 号码的匹配规则为:
[1-9]\d{6, 7}
因此, 匹配这三个字符串的正则为
-
/^0\d{2, 3}[1-9]\d{6, 7}$/
-
/^0\d{2, 3}-[1-9]\d{6, 7}$/
-
/^\(0\d{2, 3}\)[1-9]\d{6, 7}$/
2. 找到所有需要匹配的字符串的可能性后, 明确他们之间的关系.
因此可以得出这样的正则
/^0\d{2, 3}[1-9]\d{6, 7}$|^0\d{2, 3}-[1-9]\d{6, 7}$|^\(0\d{2, 3}\)[1-9]\d{6, 7}$/
这里只是粗暴地将上述三个正则用或|
粗暴的叠加在一起
3. 提取公共部分, 就像平时敲代码, 抽象一些功能
将号码部分向外抽离
/^(0\d{2, 3}|0\d{2, 3}-|\(0\d{2, 3}\))[1-9]\d{6, 7}$/
继续优化, 将区号的-
用?
进行判断
/^(0\d{2, 3}-?|\(0\d{2, 3}\))[1-9]\d{6, 7}$/
🤭到这里基本上业务开发中我们需要了解的内容就都差不多了, 但是其实还有很多细节, 我可能没有谈到. 比如正则表达的回溯原理, 这并不是一个很难懂的概念, 只是我觉得敲了又删, 还是觉得作者讲得好. 所以还希望您能好好读一下原文, 更深入地理解正则!
感谢😘
如果觉得文章内容对你有帮助:
-
❤️欢迎关注点赞哦! 我会尽最大努力产出高质量的文章
个人公众号: 前端Link
联系作者: linkcyd 😁
往期: