JS基础系列之 —— 正则表达式(js基础代码大全)
前言
正则表达式一直是困扰很多程序员的一门技术,当然也包括曾经的我。大多数时候我们在开发过程中要用到某些正则表达式的时候,都会打开谷歌或百度直接搜索然后拷贝粘贴。
作为一门用途很广的技术,我相信深入理解正则表达式并能融会贯通是值得的。所以,有了这次分享,希望通过这次分享,大家可以理解正则,并在遇到正则相关问题的时候可以不借助搜索引擎,自己就可以解决。
感受正则的魅力
1. 取到文本中的所有数字
let title = "现在是2021年08月22日12点10分"; // 普通方式拿到上面一段文字中的数字 let time = [...title].filter((item) => !Number.isNaN(Number.parseInt(item))); // 拿到数字 console.log(time.join("")); // 使用正则获取数字,正则比上面的要简洁的多 let numbs = title.match(/\d/g); console.log(numbs.join("")); // 打印结果完全相同 console.log(time.join("") === numbs.join("")); //true复制代码
2. 如何判断输入的手机号是否是有效的手机号码?
/^1[3456789]\d{9}$/ /^1(3|4|5|6|7|8|9)\d{9}$/ ^1\d{10}复制代码
对比分析 与普通函数操作字符来比较,正则表达式可以写出更简洁、功能强大的代码。
0x01 是什么
正则表达式(Regular Expression)其实就是一门工具,目的是为了字符串模式匹配,从而实现搜索和替换功能。它起源于20世纪50年代科学家在数学领域做的一些研究工作,后来才被引入到计算机领域中。从它的命名我们可以知道,它是一种用来描述规则的表达式。而它的底层原理也十分简单,就是使用状态机的思想进行模式匹配。
简单地说,实现正则表达式引擎有两种方式:DFA 自动机(Deterministic Final Automata 确定型有穷自动机)和 NFA 自动机(Non deterministic Finite Automaton 不确定型有穷自动机)。
0x02 基础知识
1. 字符
单个字符
最简单的正则表达式可以由简单的数字和字母组成,没有特殊的语义,纯粹就是一一对应的关系。如想在'apple'这个单词里找到‘a'这个字符,就直接用/a/
这个正则就可以了。
有时候我们想去找到 /
字符,但是它在正则中有着特殊的意义该怎么办呢?所以就需要用到转义符\
。
\ 反斜杠表示转义符
转义用于改变字符的含义,用来对某个字符有多种语义时的处理。
如果本来这个字符不是特殊字符,使用转义符号就会让它拥有特殊的含义。我们常常需要匹配一些特殊字符,比如空格,制表符,回车,换行等, 而这些就需要我们使用转义字符来匹配。
多个字符
单个字符的映射关系是一对一的,即正则表达式的被用来筛选匹配的字符只有一个。而这显然是不够的,只要引入集合区间和通配符的方式就可以实现一对多的匹配了。
2. 重复匹配修饰符
如果要重复匹配一些内容时我们要使用重复匹配修饰符,包括以下几种。
基本使用
let name = "sooooo"; // + :一个或者多个 console.log(name.match(/so+/)); // sooooo // * :零个或者多个 console.log(name.match(/so*/)); // sooooo // ? :有或者没有 console.log(name.match(/so?/)); // so // {1,2} :一个到两个,最少一个,最多两个 console.log(name.match(/so{1,2}/)); // soo复制代码
重复匹配对原子组影响
let name = "sososososososos"; // 连续匹配so,使用原子组包起来是里面的内容就变成了一个整体 console.log(name.match(/(so)+/g)); // ["sososososososo"]复制代码
禁止贪婪(懒惰模式)
正则表达式在进行重复匹配时,默认是贪婪匹配模式,也就是说会尽量匹配更多内容,但是有的时候我们并不希望他匹配更多内容,这时可以通过 ? 进行修饰来禁止重复匹配
let name = "soooooo"; // *:零个或多个,加上问号表示匹配0个 let reg = /so*?/g; console.log(name.match(reg)); // ["s"] // +:一个或多个,加上问号表示只匹配1个 reg = /so+?/g; console.log(name.match(reg)); // ["so"] // ?:0个或者1个,再加上问号表示只匹配0个 reg = /so??/g; console.log(name.match(reg)); // ["s"] // {2,5} 表示匹配2到5个,加上问号表示匹配2个 reg = /so{2,5}?/g; console.log(name.match(reg)); // ["soo"]复制代码
独占模式
如果在表达式后加上一个加号(+),则会开启独占模式。同贪婪模式一样,独占模式一样会匹配最长。不过在独占模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯。
以下是对三种模式的表达式:
3. 边界符
单词是构成句子和文章的基本单位,一个常见的使用场景是把文章或句子中的特定单词找出来。如:
The cat scattered his food all over the room. 复制代码
我想找到cat
这个单词,但是如果只是使用/cat/
这个正则,就会同时匹配到cat
和scattered
这两处文本。这时候我们就需要使用边界正则表达式\b
,其中 b 是 boundary 的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。
上面的例子改写成/\bcat\b/
这样就能匹配到cat
这个单词了。
4. 模式修饰符
正则表达式在执行时会按他们的默认执行方式进行,但有时候默认的处理方式总不能满足我们的需求,所以可以使用模式修正符更改默认方式。
g 全局匹配
i 不区分大小写
m 多行匹配
s 将字符串视为单行匹配
y 模式表示匹配到不符合的就停掉,不会继续往后匹配,必须连续的符合条件的
5. 选择符
|
用一个竖杠表示或者, 选择修饰符,
let title = "xiaoming"; // 一个竖表示或者,两边的表达式满足任意条件即可 console.log(/xiao|m/.test(title)); // 检测电话是否是上海或北京的坐机 //错误结果:只匹配 | 左右两边任一结果 console.log(tel.match(/010|020\-\d{7,8}/)); //正确结果:所以需要放在原子组中使用 let tel = "020-9999999"; console.log(/(010|020)\-\d{7,8}/.test(tel));复制代码
0x03 高级用法
字符匹配我们介绍的差不多了,更加高级的用法就得用到子表达式了。通过嵌套递归和自身引用可以让正则发挥更强大的功能。
从简单到复杂的正则表达式演变通常要采用分组、回溯引用和逻辑处理的思想。利用这三种规则,可以推演出无限复杂的正则表达式
1. 原子组
分组
其中分组体现在:所有以(
和)
元字符所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。如果只是使用简单的(regex)
匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。
分组匹配时没有添加 g
模式修正符时只匹配到第一个,匹配到的信息包含以下数据
在match
中使用原子组匹配,会将每个组数据返回到结果中
let nh = "nihaoya.com"; console.log(nh.match(/nihao(ya)\.(com)/)); 复制代码
回溯引用
所谓回溯引用(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像
\1
,\2
,....,其中\1
表示引用的第一个子表达式,\2
表示引用的第二个子表达式,以此类推。而\0
则表示整个表达式。
假设现在要在下面 dom 中匹配["
标题一
", "
标题二
"],你要怎么做呢?
let dom = ` <h1>标题一</h1> <h2>标题二</h2> `; // 一个小括号包起来的东西被称为原子组,\1表示与第一个原子组相同的内容 // /<(h[1-6])>[\s\S]*<\/(h[1-6])>/ let reg = /<(h[1-6])>[\s\S]*<\/\1>/g; console.log(dom.match(reg)); // ["<h1>标题一</h1>", "<h2>标题二</h2>"]复制代码
利用回溯引用,我们可以很容易地写出/<(h[1-6])>[\s\S]*<\/\1>/g
这样的正则。
回溯引用在替换字符串中十分常用,语法上有些许区别,用$1
,$2
...来引用要被替换的字符串。
替换字符串可以插入下面的特殊变量名:
下面以js代码作演示:
字符替换
var str = 'abc abc 123'; str.replace(/(ab)c/g,'$1f'); // 得到结果 'abf abf 123'
在你好前后添加三个=
let hd = "=你好="; console.log(hd.replace(/你好/g, "‘`‘`&'$'"));
把电话号用 - 连接
let hd = "(010)99999999 (020)8888888"; console.log(hd.replace(/((\d{3,4}))(\d{7,8})/g, "1−1-1−2"));
嵌套分组和不记录分组
嵌套分组就是有很多的/(1(2(3)))/
,回溯引用时的状态如图所示
如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)
这样就可以避免浪费内存。
原子组里面加上 ?: 表示不记录该原子组,但是原子组的功能仍然生效
var str = 'scq000'. str.replace(/(scq00)(?:0)/, '$1,$2') // 返回scq00,$2 // 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2复制代码
分组别名
如果希望返回的组数据更清晰,可以为原子组编号,结果将保存在返回的 groups
字段中
let hd = "<h1> nihaoya.com </h1>"; console.dir(hd.match(/<(?<tag>h[1-6])[\s\S]*<\/\1>/));复制代码
使用 ? 起原子组的别名
使用 $ 读取别名
2. 断言匹配
有时,我们需要限制回溯引用的适用范围。那么通过断言匹配就可以达到这个目的。
断言虽然写在括号中但它不是组,所以不会在匹配结果中保存,可以将断言理解为正则中的条件。
断言用来声明一个应该为真的事实。正则表达式中只有当断言为真时才会继续进行匹配
零宽先行断言
(?=exp)
正向肯定查找
用来检查接下来出现的是不是某个特定的字符集。
张三先生和张三女士字体变颜色
// 张三先生和张三女士是天造地设的一对 <script> // 用小括号包括起来,前面加上?=表示该正则右侧等于某个值得元素 let main = document.body; // 匹配张三 右侧 是先生的字段 let men = /张三(?=先生)/g; // 匹配张三 右侧 是女士的字段 let wumen = /张三(?=女士)/g; main.innerHTML = main.innerHTML.replace( men, `<span style="color:blue">$&</span>` ); main.innerHTML = main.innerHTML.replace( wumen, `<span style="color:pink">$&</span>` ); </script> 复制代码
下面是将价格后面 添加上 .00
<script> let lessons = ` css,200元,300次 js,300.00元,100次 node.js,180元,260次 `; let reg = /(\d+)(.00)?(?=元)/gi; lessons = lessons.replace(reg, (v, ...args) => { args[1] = args[1] || ".00"; return args.splice(0, 2).join(""); }); console.log(lessons); </script>复制代码
零宽后行断言
?<=exp
反向肯定查找
用来检查前面出现的是不是某个特定的字符集。
匹配前面是shijei 的数字
let hd = "nihao789shijei666"; let reg = /(?<=shijei)\d+/i; console.log(hd.match(reg)); // 666复制代码
匹配前后都是数字的内容
let hd = "nihao789shijei666"; let reg = /(?<=\d)[a-z]+(?=\d{3})/i; console.log(hd.match(reg)); // shijei复制代码
获取标题中的内容
let hd = `<h1>nihaoya</h1>`; let reg = /(?<=<h1>).*(?=<\/h1>)/g; console.log(hd.match(reg));复制代码
零宽负向先行断言
(?!exp)
正向否定查找
用来检查接下来不应该出现匹配内容的字符集。
使用 (?!exp)字母后面不能为两位数字
let reg = /abc(?!de)/; reg.test('abcdefg'); // false; reg.test('abcd'); // true; reg.test('abcabc'); // true;复制代码
零宽负向后行断言
(?<!exp)
反向否定查找
用来检查前面不应该出现的字符集。
0x04 应用
1. 利用正则替换字符
使用断言模糊电话号
有时候我们需要对手机号码进行模糊展示,中间四位用 * 代替
// 不使用断言 let tels = `15036999999`; let reg = /(\d{3})(\d{4})(\d+)/g; tels = tels.replace(reg, (v, ...args) => { args[1] = "*".repeat(4); return args.splice(0, 3).join(""); }); console.log(tels); // 150****9999 // 使用断言 let newtel = `15036999999`; function hideTel(tel) { // 匹配前面是由3位数字组成,后面是由4位数字组成的字段 let newReg = /(?<=\d{3})\d{4}(?=\d{4})/g; return tel.replace(newReg, (v) => { // 将这个字段替换成4个*号 return "*".repeat(4); }); } console.log(hideTel(newtel)); // 150****9999复制代码
时间字符串的格式化处理
有时候后端返回的时间字符串统一为 '2021-8-13 16:32:2' 这种格式的,想转换成 '2021年08月13日16时32分02秒'
/** * formatTime: 时间字符串的格式化处理 * @param { string } templete 时间 * @param { string } templete 期望获取日期格式的模板 * 模板格式: {0}->年 {1~5}->月日时分秒 * @returns { string } 格式化后的时间字符串 */ function formatTime(time, templete = "{0}年{1}月{2}日{3}时{4}分{5}秒") { // 首先获取时间字符串中的年月日等信息 let timeAry = time.match(/\d+/g); return templete.replace(/\{(\d+)\}/g, (...[, $1]) => { // =>content: 当前本次大正则匹配的信息 $1: 本次小分组单独匹配的信息 // 以$1的值作为索引,到 timeAry 中找到对应的时间(如果没有则用"00"补) let time = timeAry[$1] || "00"; return time.length < 2 ? "0" + time : time; }); } let time = "2021-8-13 16:32:2"; // time = "2021/8/26"; console.log(formatTime(time)); // 2021年08月13日16时32分02秒 console.log(formatTime(time, "{0}年{1}月{2}日")); // 2021年08月13日 console.log(formatTime(time, "{0}/{1}/{2}日 {3}:{4}:{5}")); // 2021/08/13日 16:32:02 console.log(formatTime(time, "{1}-{2} {3}:{4}")); // 08-13 16:32 let time1 = "2021/8/26"; console.log(formatTime1(time1)); // 2021年08月26日00时00分00秒 console.log(formatTime1(time1, "{0}年{1}月{2}日")); // 2021年08月26日 console.log(formatTime1(time1, "{1}-{2} {3}:{4}")); // 08-26 00:00复制代码
2. 表单验证
微信号正则
//微信号正则,6至20位,以字母开头,字母,数字,减号,下划线 var wxPattern = /^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$/; //输出 true console.log(wxPattern.test("nihaoya_com"));复制代码
验证数字,可正,可负,可为0
^0$|^-?[1-9]\d*$复制代码
思考:数字价格千分位分割
123456789 => 123,456,789
思路 - 步骤:
从后往前每三个数字前加一个逗号
开头不能加逗号(比如:123 最后不能变成,123)
是不是很符合(?=p)的规律呢?p可以表示每三个数字,要添加的逗号所处的位置正好是(?=p)匹配出来的位置。
步骤一:把最后一个逗号弄出来
let price = '123456789' let priceReg = /(?=\d{3}$)/ console.log(price.replace(proceReg, ',')) // 123456,789复制代码
步骤二:把所有逗号弄出来
let price = '123456789' let priceReg = /(?=(\d{3})+$)/g console.log(price.replace(priceReg, ',')) // ,123,456,789复制代码
步骤三:去掉首位的逗号
let price = '123456789' let priceReg = /(?!^)(?=(\d{3})+$)/g console.log(price.replace(priceReg, ',')) // 123,456,789复制代码
结果:
0x05 总结
对于正则来说,合理的使用正则的话可以大大的简化我们的代码,也可以帮助我们去实现更加强大的功能,希望通过这次分享, 让大家可以去熟悉正则,以后遇到可以很好的去使用正则
0x06 推荐正则可视化工具 :
1. regex101.com/
2. regexper.com/
3. jex.im/regulex
作者:得到前端团队
链接:https://juejin.cn/post/7036179203615621150