整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

详解JavaScript中的RegExp:深入理解正

详解JavaScript中的RegExp:深入理解正则表达式

在JavaScript中,正则表达式(Regular Expression,简称RegExp)是一种强大的文本处理工具,它提供了查找、替换和提取字符串中模式匹配项的强大功能。本文将带领您深入理解JavaScript中的正则表达式,并通过实例演示其常见用法与高级特性。

1. 创建正则表达式

在JavaScript中创建一个正则表达式有两种方式:

  • 字面量形式:
let regex=/pattern/flags;

例如,查找所有的数字:

let numbersRegex=/\d+/;
  • 构造函数形式:
let regex=new RegExp('pattern', 'flags');

同样的例子,使用构造函数形式:

let numbersRegex=new RegExp("\d+", "");

注意:在构造函数中写入字符串时,需要对特殊字符进行转义。

2. 正则表达式模式

正则表达式的主体部分是“模式”,用于描述要匹配的文本结构。以下是一些基本模式示例

  • \d 匹配任何数字(等同于 [0-9])
  • \w 匹配字母、数字或下划线(等同于 [a-zA-Z0-9_])
  • \s 匹配任何空白字符(包括空格、制表符、换行符等)
  • . 匹配除换行符之外的任意单个字符
  • ^ 匹配字符串的开始位置
  • $ 匹配字符串的结束位置
  • * 表示前面的元素可以重复零次或多次
  • + 表示前面的元素至少重复一次
  • ? 表示前面的元素可选,出现零次或一次
  • {n} 表示前面的元素必须重复n次
  • {n,} 表示前面的元素至少重复n次
  • {n,m} 表示前面的元素重复次数在n到m之间

3. 标志位

标志位位于正则表达式末尾,用于改变匹配行为:

  • g 全局搜索,匹配字符串中的所有符合条件的位置,而不是只找到第一个就停止。
  • i 忽略大小写。
  • m 多行模式,使^和$可以匹配每一行的开始和结束。

4. 正则方法应用

在JavaScript中,我们可以使用以下内置的方法来操作正则表达式:

  • test():检测字符串是否符合某个正则表达式。
let isNumber=/\d+/.test("123");
console.log(isNumber); // true
  • match():在字符串上执行查找,返回匹配结果数组或者null。
let matches="Hello 123 World".match(/\d+/);
console.log(matches[0]); // "123"
  • exec():在字符串上执行查找,返回包含匹配信息的对象或者null。
let reg=/\d+/g;
let matchResult=reg.exec("Hello 123 World");
console.log(matchResult[0]); // "123"
  • search():返回字符串中第一个匹配项的索引,如果没有找到则返回-1。
let index="Hello 123 World".search(/\d+/);
console.log(index); // 6
  • replace():使用新的子串替换与正则表达式匹配的子串。
let replacedStr="Hello 123 World".replace(/\d+/, "456");
console.log(replacedStr); // "Hello 456 World"
  • split():根据正则表达式分隔字符串,返回一个由分割后的子串组成的数组。
let words="Hello World, How are you?".split(/\s*,\s*/);
console.log(words); // ["Hello World", "How are you?"]

5. 高级技巧

  • 非捕获组 (?:...):不捕获括号内的内容,仅作分组之用。
  • 负向前瞻 (?=...) 负向后瞻 (?!...):用来断言后面或前面的内容,但并不包含在匹配结果内。
  • 反向引用 \n:匹配第n个括号里的内容。
let regex=/(hello)\s+(world)(?=\!)/;
let str="hello world!";
let match=str.match(regex);
console.log(match[1]); // "hello"
console.log(match[2]); // "world"

6.小结

JavaScript中的正则表达式功能强大且灵活,熟练掌握这一工具能极大地提高开发效率,尤其是在数据清洗、验证和搜索等场景。希望这篇指南能帮助你更好地理解和运用正则表达式,实际操作中还需结合具体需求进行调整和优化。

片来源 | 电影《成长教育》剧照

正则表达式是描述一组字符串特征的模式,用来匹配特定的字符串。” ——Ken Thompson

什么是正则表达?

正则表达式其实就是一种pattern、一种模式、一种格式、一种规则,它主要是用在文本字符串处理的时候,想要在一堆文本中找到满足某种格式、规则的字符串

它起源于上个20世纪50年代科学家在数学领域做的一些研究工作,后来才被引入到计算机领域中。从它的命名我们可以知道,它是一种用来描述规则的表达式。

比如,你要写一个应用, 想设定一个用户命名的规则, 让用户名包含字符,数字,下划线和连字符,以及限制字符的个数,好让名字看起来没那么丑。那么,以下正则表达式来验证一个用户名:


以上的正则表达式可以接受 johndoe, jo-hndoe, john12_as. 但不匹配Jo, 因为它包含了大写的字母而且太短了。

那么,要想学正则表达试,初学者应该从哪些方面入手?

这里强烈安利的几个学习教程和资源:

1.正则表达式30分钟入门教程

http://deerchao.net/tutorials/regex/regex.htm

推荐理由:特别适合想要快速入门的同学,结合实例可以让你理解基本的原理和语法。

2.高效入门正则表达式

https://github.com/ziishaned/learn-regex/blob/master/translations/README-cn.md

推荐理由:Github上一篇简单的正则表达式教程,提供了十几种语言(包括中文),这篇教程覆盖到了你在实际应用中99%的场景。

3.regex101网站——能可视化展示正则匹配结果

https://regex101.com


4.regexper网站——能够直观展示正则表达式的状态机图

https://regexper.com

正则的进阶——从底层工作机制来理解正则表达式

很多同学在入门某个语言或工具时,总是习惯先从实例上手学习,往往忽略了对语言最底层的一些原理,这里引用@小胡子哥的“进阶正则表达式”一文,来帮助你更好的熟悉和运用正则表达式。

注:此次引用,在原文基础上做了删减,想看完整内容,请转至,

https://www.cnblogs.com/hustskyking/p/how-regular-expressions-work.html

正则表达式的工作机制

画了一个草图,简单的说明了下正则表达式的工作原理。

 | 编译 |
 +--------+
 |
 ↓
+----------------+
| 设置开始位置 |←---------+
+----------------+ ↑
 | |
 ↓ 其 |
+----------------+ 他 |
| 匹配 & 回溯 | 路 |
+----------------+ 径 |
 | |
 ↓ |
+----------------+ |
| 成功 
or
 失败 |---------→+
+----------------+

你写的任何一个正则直接量或者 RegExp 都会被浏览器编译为一个原生代码程序。第一次匹配是从头个字符开始,匹配成功时,他会查看是否还有其他的路径没有匹配到,如果有的话,回退到上一次成功匹配的位置,然后重复第二步操作,不过此时开始匹配的位置(lastIndex)是上次成功位置加 1.这样说有点难以理解,下面写了一个 demo,这个 demo 就是实现一个正则表达式的解析引擎,因为逻辑和效果的表现都太复杂了,所以只做了一个简单的演示:

Reg:

/H(i|ello), barret/g

Str:

Lalala. Hi, barret. Hello, John

如果上面的 demo 跑不起来,请戳这里:

http://qianduannotes.duapp.com/demo/regexp/index.html

如果要深入了解正则表达式的内部原理,必须先理解匹配过程的一个基础环节——回溯。他是驱动正则的一个基本动力,也是性能消耗、计算消耗的根源。

回溯

正则表达式中出现最多的是分支和量词。上面的 demo 中可以很清楚的看到 hi 和 hello 这两个分支,当匹配到第一个字符 h 之后,进入 (i | ello) 的分支选择,首先是进入 i 分支,当 i 分支匹配完了之后,再回到分支选择的位置,重新选择分支。简单点说,分支就是 | 操作符带来的多项选择问题,而量词指的是诸如 *,+?,{m,n} 之类的符号,正则表达式必须决定何时尝试匹配更多的字符。下面结合回溯详细说说分支和量词。

1.分支

继续分析上面那个案例。 "Lalala. Hi, barret. Hello, John".match(/H(i|ello), barret/g),首先会查找 H 字符,在第九位找到 H 之后,正则子表达式提供了两个选择 (i|ello),程序会先拿到最左边的那个分支,进入分支后,在第十位匹配到了 i,接着匹配下一个字符,下一个字符是逗号,接着刚才的位置又匹配到了这个逗号,然后再匹配下一个,依次类推,直到完整匹配到整个正则的内容,此时程序会在 Hi,barret后面做一个标记,表示在这里进行了一次成功的匹配。但程序到此并没有结束,因为后面加了一个全局参数,依然使用这个分支往后匹配,很显然,到了 Hello 的时候,Hi 分支匹配不了了,于是程序会回溯到刚才我们做标记的位置,并进入第二个分支,从做标记的位置重新开始匹配,依次循环。

只要正则表达式没有尝试完所有的可选项,他就会回溯到最近的决策点(也就是上次匹配成功的位置)。

2.量词

量词这个概念特别简单,只是在匹配过程中有贪婪匹配和懒惰匹配两种模式,结合回溯的概念理解稍微复杂。还是用几个例子来说明。

1)贪婪

str="AB1111BA111BA";
reg=/AB[\s\S]+BA/;
console.log(str.match(reg));

首先是匹配AB,遇到了 [\s\S]+,这是贪婪模式的匹配,他会一口吞掉后面所有的字符,也就是如果 reg 的内容为 AB[\s\S]+,那后面的就不用看了,直接全部匹配。而往后看,正则后面还有B字符,所以他会先回溯到倒数第一个字符,匹配看是否为 B,显然倒数第一个字符不是B,于是他又接着回溯,找到了B字母,找到之后就不继续回溯了,而是往后继续匹配,此刻匹配的是字符A,程序发现紧跟B后的字母确实是A,那此时匹配就结束了。如果没有看明白,可以再读读下面这个图:

REG: 
/AB[\s\S]+BA/
MATCH: A 匹配第一个字符
 AB 匹配第二个字符
 AB1111BA111BA [\s\S]+ 贪婪吞并所有字符
 AB1111BA111BA 回溯,匹配字符B
 AB1111BA111B 找到字符B,继续匹配A
 AB1111BA111BA 找到字符A,匹配完成,停止匹配

2) 懒惰(非贪婪)

str="AB1111BA111BA";
reg=/AB[\s\S]+?BA/;
console.log(str.match(reg));

与上面不同的是,reg 中多了一个 ? 号,此时的匹配模式为懒惰模式,也叫做非贪婪匹配。此时的匹配流程是,先匹配AB,遇到[\s\S]+?,程序尝试跳过并开始匹配后面的字符B,往后查看的时候,发现是数字1,不是要匹配的内容,继续往后匹配,知道遇到字符B,然后匹配A,发现紧接着B后面就有一个A,于是宣布匹配完成,停止程序。

 REG: 
/AB[\s\S]+BA/
MATCH: A 匹配第一个字符
 AB 匹配第二个字符
 AB [\s\S]+? 非贪婪跳过并开始匹配B
 AB1 不是B,回溯,继续匹配
 AB11 不是B,回溯,继续匹配
 AB111 不是B,回溯,继续匹配
 AB1111 不是B,回溯,继续匹配
 AB1111B 找到字符B,继续匹配A
 AB1111BA 找到字符A,匹配完成,停止匹配

如果匹配的内容是 AB1111BA,那贪婪和非贪婪方式的正则是等价的,但是内部的匹配原理还是有区别的。为了高效运用正则,必须搞清楚使用正则时会遇到那些性能消耗问题。

逗比的程序

//去测试下这句代码
"TTTTTTTT".match(/(T+T+)+K/);
//然后把前面的T重复次数改成30
//P.S:小心风扇狂转,CPU暴涨

我们来分析下上面这段代码,上面使用的都是贪婪模式,那么他会这样做:

REG: (T+T+)+K
MATCH: ①第一个T+匹配前7个T,第二个T+匹配最后一个T,没找到K,宣布失败,回溯到最开始位置
 ②第一个T+匹配前6个T,第二个T+匹配最后两个T,没找到K,宣布失败,回溯到最开始位置
 ③...
 ... 接着还会考虑(T+T+)+后面的 + 号,接着另一轮的尝试。
 ⑦...
 ...

这段程序并不会智能的去检测字符串中是否存在 K。如果匹配失败,他会选择其他的匹配方式(路径)去匹配,从而造成疯狂的回溯和重新匹配,结果可想而知。这是回溯失控的典型例子。

前瞻和反向引用

1.前瞻和引用

前瞻有两种。一种是负向前瞻,JS中使用 (?!xxx) 来表示,他的作用是对后面要匹配的内容做一个预判断,如果后面的内容是xxx,则此段内容匹配失败,跳过去重新开始匹配。另一种是正向前瞻,(?=xxx),匹配方式和上面相反,还有一个长的类似的是 (?:xxx),这个是匹配xxx,他是非捕获性分组匹配,即匹配的内容不会创建反向引用。具体内容可以去文章开头提到的文档中查看。

反向引用,这个在 replace 中用的比较多,在 replace 中:

而在正则表达中,主要就是 , 之类的数字引用。前瞻和反向引用使用恰当可以大大的减少正则对资源的消耗。举个例子来简单说明下这几个东西:

问题:使用正则匹配过滤后缀名为 .css 和 .js 的文件。

如:test.wow.js test.wow.css test.js.js等等。

有人会立马想到使用负向前瞻,即:

//过滤js文件
/(?!.+\.js$).*/.exec("test.wow.js")
//过滤js和css文件
/(?!.+\.js$|.+\.css$).*/.exec("test.wow.js")
/(?!.+\.js$|.+\.css$).*/.exec("test.wow.html")

但是你自己去测试下,拿到的结果是什么。匹配非js和非css文件可以拿到正确的文件名,但是我们期望这个表达式对js和css文件的匹配结果是null,上面的表达式却做不到。问题是什么,因为(?!xxx)和(?=xxx)都会消耗字符,在做预判断的时候把 .js 和 .css 给消耗了,所以这里我们必须使用非捕获模式。

/(?:(?!.+\.js$|.+\.css$).)*/.exec("test.wow.html");
/(?:(?!.+\.js$|.+\.css$).)*/.exec("test.wow.js");

我们来分析下这个正则:

(?:(?!.+\.js$|.+\.css$).)*
--- ---------------- -
| | |
+----------------------+
 ↓ |
非捕获,内部只有一个占位字符
 |
 ↓
 负向前瞻以.js和.css结尾的字符串

最后一个星号是贪婪匹配,直接吞掉全部字符。

这里讲的算是有点复杂了,不过在稍复杂的正则中,这些都是很基础的东西了,想在这方面提高的童鞋可以多研究下。

2.原子组

JavaScript的正则算是比较弱的,他没有分组命名、递归、原子组等功能特别强的匹配模式,不过我们可以利用一些组合方式达到自己的目的。上面的例子中,我们实际上用正则实现了一个或和与的功能,上面的例子体现的还不是特别明显,再写个例子来展示下:

str1="我(wo)叫(jiao)李(li)靖(jing)";
str2="李(li)靖(jing)我(wo)叫(jiao)";
reg=/(?=.*?我)(?=.*?叫)(?=.*?李)(?=.*?靖)/;
console.log(reg.test(str1)); //true
console.log(reg.test(str2)); //true

不管怎么打乱顺序,只要string中包含“我”,“是”,“李”,“靖”这四个字,结果都是true。

类似(?=xxx),就相当于一个原子组,原子组的作用就是消除回溯,只要是这种模式匹配过的地方,回溯时都不会到这里和他之前的地方。上面的程序 "TTTTTTTT".match(/(T+T+)+K/);可以通过原子组的方式处理:

"TTTTTTTT".match(/(?=(T+T+))\2+K/);

如此便能彻底消除回溯失控问题。

- 完 -


与其他程序设计语言一样,学习正则表达式的关键是实践,实践,再实践。 ——本?福塔(Ben Forta)

[美] 本?福塔(Ben Forta)著 门佳 杨涛 等 (译)

  • 紧贴实战需求,化繁为简,高效解决编程难题
  • 如果想快速上手正则表达式,那么除了本书,没有第二种选择
  • 相比上一版,书中完善了正则表达式的用法,丰富了提示、注意、警告等信息。

本书从简单的文本匹配开始,循序渐进地介绍了很多复杂内容,包括反向引用、条件评估、环视等。每章都配有许多简明实用的示例,有助于全面、系统、快速掌握正则表达式,并运用它们解决实际问题。

做项目或者代码编写过程中,一般会遇到验证电话、邮箱等格式是否正确合法的问题。通常我们会使用正则表达式,自己写很麻烦,且正则表达式又不是那么容易记住。所以现在分享几条常用的正则表达式,需要的时候直接复制即可。

// uri正则表达式
const urlRegex=/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
// 邮箱正则表达式
const emailRegex=/^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})$/;
// 手机号码正则表达式
const mobileRegex=/^1[0-9]{10}$/;
// 电话号码正则表达式
const phoneRegex=/^([0-9]{3,4}-)?[0-9]{7,8}$/;
// URL地址正则表达式
const urlRegex=/^http[s]?:\/\/.*/;
// 小写字母正则表达式
const lowerCaseRegex=/^[a-z]+$/;
// 大写字母正则表达式
const upperCaseRegex=/^[A-Z]+$/;
// 大小写字母正则表达式
const alphabetsRegex=/^[A-Za-z]+$/;
// 身份证号码正则表达式
const cardidRegex=/(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
// 判断姓名是否正确
const regNameRegex=/^[\u4e00-\u9fa5]{2,4}$/;
// 判断是否为整数
const numRegex=/[^\d.]/g;
// 判断是否为小数
const numordRegex=/[^\d.]/g;
// 数字或者小数
const numberRegex=/^[0-9]+(\.[0-9]{1,3})?$/;

如果大家有其它常用的正则表达式,可以分享在评论区!