前言
正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript中,正则表达式也是对象。这些模式被用于 RegExp 的 exec 和 test 方法, 以及 String 的 match、matchAll、replace、search 和 split 方法。
本章介绍 JavaScript 正则表达式。
通过使用正则表达式,可以:
-
测试字符串内的模式,可以测试输入字符串,以查看字符串内是否出现电话号码模式或信用卡号码模式。这称为数据验证。
-
替换文本, 可以使用正则表达式来识别文档中的特定文本,完全删除该文本或者用其他文本替换它。
-
基于模式匹配从字符串中提取子字符串, 可以查找文档内或输入域内特定的文本。
基础用法
创建一个正则表达式
使用一个正则表达式字面量,其由包含在斜杠之间的模式组成,如下所示:
var re = /ab+c/;
复制代码
脚本加载后,正则表达式字面量就会被编译。当正则表达式保持不变时,使用此方法可获得更好的性能。
或者调用 RegExp
对象的构造函数,如下所示:
var re = new RegExp("ab+c");
复制代码
在脚本运行过程中,用构造函数创建的正则表达式会被编译。如果正则表达式将会改变,或者它将会从用户输入等来源中动态地产生,就需要使用构造函数来创建正则表达式。
js里使用正则
方法 | 描述 |
---|---|
exec | 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回 null)。 |
test | 一个在字符串中测试是否匹配的RegExp方法,它返回 true 或 false。 |
match | 一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。 |
matchAll | 一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。 |
search | 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。 |
replace | 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。 |
split | 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。 |
-
Exec 和 match的区别
-
分别是RegExp类和String类的方法
-
exec 只会匹配第一个符合的字符串(意味着g对其不起作用), match 是否返回所有匹配的数组跟正则表达式里是否带着g有关系
-
const str = 'd3aish hello world d5aisy';
const reg = /\dai/g;
// 先看没有g的情况
console.log(str.match(reg)); // ['3ai', '5ai']
console.log(reg.exec(str)); // ['3ai']
// 不带g
const str = 'd3aish hello world d5aisy';
const reg = /\dai/;
// 先看没有g的情况
console.log(str.match(reg)); // ['3ai']
console.log(reg.exec(str)); // ['3ai']
复制代码
量词
Characters | Meaning |
---|---|
x* |
将前面的项“x”匹配0次或更多次。 例如,/bo*/匹配“A ghost booooed”中的“boooo”和“A bird warbled”中的“b”,但在“A goat grunt”中没有匹配。 |
x+ |
将前一项“x”匹配1次或更多次。等价于{1,}。 例如,/a+/匹配“candy”中的“a”和“caaaaaaandy”中的“a”。 |
x? |
将前面的项“x”匹配0或1次。 例如,/e?le?/匹配angel中的el和angle中的le。 如果立即在任何量词*、+、?或{}之后使用,则使量词是非贪婪的(匹配最小次数),而不是默认的贪婪的(匹配最大次数)。 |
x{n} |
其中“n”是一个正整数,与前一项“x”的n次匹配。 例如,/a{2}/ 不匹配“candy”中的“a”,但它匹配“caandy”中的所有“a”,以及“caaandy”中的前两个“a”。 |
x{n,} |
其中,“n”是一个正整数,与前一项“x”至少匹配“n”次。 例如,/a{2,}/不匹配“candy”中的“a”,但匹配“caandy”和“caaaaaaandy”中的所有a。 |
x{n,m} |
其中,“n”是0或一个正整数,“m”是一个正整数,而m > n至少与前一项“x”匹配,最多与“m”匹配。例如,/a{1,3}/不匹配“cndy”中的“a”,“candy”中的“a”,“caandy”中的两个“a”,以及“caaaaaaandy”中的前三个“a”。注意,当匹配“caaaaaaandy”时,匹配的是“aaa”,即使原始字符串中有更多的“a”。 |
x*? x+? x?? x{n}? x{n,}? x{n,m}? |
默认情况下,像 * 和 + 这样的量词是“贪婪的”,这意味着它们试图匹配尽可能多的字符串。?量词后面的字符使量词“非贪婪”:意思是它一旦找到匹配就会停止。例如,给定一个字符串“some new thing”:/<.*>/ will match ” new “/<.*?>/ will match “” |
标志符
正则表达式有六个可选参数 ( flags
) 允许全局和不分大小写搜索等。这些参数既可以单独使用也能以任意顺序一起使用, 并且被包含在正则表达式实例中。
标志 | 描述 |
---|---|
g |
全局搜索。 |
i |
不区分大小写搜索。 |
m |
多行搜索。 |
s |
允许 . 匹配换行符。 |
u |
使用unicode码的模式进行匹配。 |
y |
执行“粘性( sticky )”搜索,匹配从目标字符串的当前位置开始。 |
为了在正则表达式中包含标志,请使用以下语法:
var re = /pattern/flags;
复制代码
或者
var re = new RegExp("pattern", "flags");
复制代码
值得注意的是,标志是一个正则表达式的一部分,它们在接下来的时间将不能添加或删除。
标志符g
const reg = /abc/gi;
const str = 'helloabc';
reg.test(str) // true
reg.test(str) // false
reg.test(str) // true
reg.test(str) // false
const reg = /abc/i;
const str = 'helloabc';
reg.test(str) // true
reg.test(str) // true
reg.test(str) // true
reg.test(str) // true
复制代码
全局正则表达式的另一个属性 lastIndex
用于存放上一次匹配文本之后的第一个字符的位置。
RegExp.prototype.exec()
和 RegExp.prototype.test()
方法都以 lastIndex
属性中所存储的位置作为下次正则匹配检索的起点。连续调用这两个方法就可以遍历字符串中的所有匹配文本。
lastIndex
属性可读写,当 RegExp.prototype.exec()
或 RegExp.prototype.test()
再也找不到可以匹配的文本时,会自动把 lastIndex 属性重置为 0。
因此使用这两个方法来检索文本,是可以无限执行下去的。
标志符y
执行“粘性( sticky
)”搜索,匹配从目标字符串的当前位置开始
var searchStrings, stickyRegexp;
stickyRegexp = /foo/y;
searchStrings = [
"foo",
" foo",
" foo",
];
searchStrings.forEach(function(text, index) {
stickyRegexp.lastIndex = 1;
console.log("found a match at", index, ":", stickyRegexp.test(text));
});
// found a match at 0 : false
// found a match at 1 : true
// found a match at 2 : false
// 如果把y改成g
// found a match at 0 : false
// found a match at 1 : true
// found a match at 2 : true
复制代码
可以理解为必须为在lastIndex开头去匹配,即index为1时开始匹配 /^abc/
,实现更精准的位置控制。
高级用法
贪婪模式和非贪婪模式
var str='aacbacbc';
var reg=/a.*b/;
var res=str.match(reg);
// aacbacb index为0
console.log(res);
复制代码
上例中,匹配到第一个a后,开始匹配.*,由于是贪婪模式,它会一直往后匹配,直到最后一个满足条件的b为止,因此匹配结果是aacbacb
var str='aacbacbc';
var reg=/a.*?b/;
var res=str.match(reg);
// acbacb index为1
console.log(res);
复制代码
第一个匹配的是a,然后再匹配下一个字符a时,和正则不匹配,因此匹配失败,index挪到1,接下来匹配成功了ac,继续往下匹配,由于是贪婪模式,尽可能多的去匹配结果,直到最后一个符合要求的b为止,因此匹配结果是acbacb
捕获组
对于要重复单个字符,非常简单,直接在字符后加上限定符即可,例如 a+ 表示匹配1个或一个以上的a,a?表示匹配0个或1个a。
但是我们如果要对多个字符进行重复怎么办呢?此时我们就要用到分组,我们可以使用小括号”()”来指定要重复的子表达式,然后对这个子表达式进行重复,例如:(abc)? 表示0个或1个abc 这里一 个括号的表达式就表示一个分组 。
非捕获组有很多种形式,其中包括:零宽度断言和模式修正符
反向引用
引用的是前面捕获组中的文本而不是正则,也就是说反向引用处匹配的文本应和前面捕获组中的文本相同。如
/(["'])(abc).*\1/
其中使用了分组,\1就是对引号这个分组的引用,它匹配包含在两个引号或者两个单引号中的所有字符串,如,”abc” 或 ” ‘ ” 或 ‘ ” ‘ ,但是请注意,它并不会对” a’或者 ‘a”匹配。平时开发的时候也常用于html标签的匹配
命名捕获组
捕获组其实是分为编号捕获组 Numbered Capturing Groups
和命名捕获组 Named Capturing Groups
的,我们上面说的捕获组,默认指的是编号捕获组。命名捕获组,也是捕获组,只是语法不一样。命名捕获组的语法如下: (?<name>group)
或 (?'name'group)
,其中 name
表示捕获组的名称, group
表示捕获组里面的正则。
非捕获组
语法:(?:Pattern)
如:匹配indestry或者indestries
我们可以使用indestr(y|ies)或者indestr(?:y|ies)
以 (?) 开头的组是纯的 非捕获 组,它不捕获文本 ,也不针对组合计进行计数。就是说, 如果小括号中以?号开头,那么这个分组就不会捕获文本,当然也不会有组的编号 ,因此也不存在反向引用。
我们通过捕获组就能够得到我们想要匹配的内容了,那为什么还要有非捕获组呢?
原因是捕获组捕获的内容是被存储在内存中,可供以后使用,比如反向引用就是引用的内存中存储的捕获组中捕获的内容。而非捕获组则不会捕获文本,也不会将它匹配到的内容单独分组来放到内存中。所以,使用非捕获组较使用捕获组更节省内存。
- 实际应用场景,可以快速提取想要的信息
'https://www.toutiao.com'.match(/(?:https?:\/\/)(.*)/)
// ["https://www.toutiao.com", "www.toutiao.com"]
复制代码
断言
零宽度断言
(?=y) | 匹配’x’仅仅当’x’后面跟着’y’.这种叫做先行断言。 例如,/Jack(?=Sprat)/会匹配到’Jack’仅当它后面跟着’Sprat’。 /Jack(?=Sprat|Frost)/匹配‘Jack’仅当它后面跟着’Sprat’或者是‘Frost’。 但是‘Sprat’和‘Frost’都不是匹配结果的一部分。 |
(?<=y)x | 匹配’x’仅当’x’前面是’y’.这种叫做后行断言。 例如,/(?<=Jack)Sprat/会匹配到’ Sprat ‘仅仅当它前面是’ Jack ‘。 /(?<=Jack|Tom)Sprat/匹配‘ Sprat ’仅仅当它前面是’Jack’或者是‘Tom’。但是‘Jack’和‘Tom’都不是匹配结果的一部分。 |
x(?!y) | 仅仅当’x’后面不跟着’y’时匹配’x’,这被称为正向否定查找。 例如,仅仅当这个数字后面没有跟小数点的时候,/\d+(?!.)/ 匹配一个数字。 正则表达式/\d+(?!.)/.exec(“3.141”)匹配‘141’而不是‘3.141’ |
(?<!y)x | 仅仅当’x’前面不是’y’时匹配’x’,这被称为反向否定查找。 例如, 仅仅当这个数字前面没有负号的时候,/(?<!-)\d+/ 匹配一个数字。 /(?<!-)\d+/.exec(‘3’) 匹配到 “3”。 /(?<!-)\d+/.exec(‘-3’) 因为这个数字前有负号,所以没有匹配到。 |
这四个非捕获组用于匹配表达式X,但是不包含表达式的文本。
例子
如何把一串整数转换成千位分隔形式,例如10000000000,转换成10,000,000,000。
除了常规的方法,可以使用正则解这个题
const str = "100000000000";
const reg= /(?=(\B\d{3})+$)/g;
console.log(str.replace(reg, ","));
复制代码
回溯
原字符串
“Regex”
贪婪匹配过程分析
".*"
第一个 "
取得控制权,匹配正则中的 "
,匹配成功,控制权交给 .*
.
取得控制权后,匹配接下来的字符。.
代表匹配任何字符,代表可匹配可不匹配,这属于贪婪模式的标识符,会优先尝试匹配,于是接下来从1位置处的R开始匹配,依次成功匹配了R,e,g,e,x,接着继续匹配最后一个字符 "
,匹配成功,这时候已经匹配到了字符串的结尾,所以 .*
匹配结束,将控制符交给正则式中最后的 "
"
取得控制权后,由于已经是到了字符串的结尾,因此匹配失败,向前查找可供回溯的状态,控制权交给 .*
,.*
让出一个字符 "
,再把控制权交给"
,此时刚好匹配成功。
至此,整个正则表达式匹配完毕,匹配结果为”Regex”,匹配过程中回溯了1次
“ | “ |
---|---|
“.* | “Re |
“.* | “Reg |
“.* | “Rege |
“.* | “Rege |
“.* | “Regex |
“.* | “Regex” |
“.*” | “Regex” |
“.* | “Regex |
“.*” | “Regex” |
回溯陷阱
下面的例子会让你的浏览器的cpu达到100%,就是回溯太多的导致的。
console.time('reg')
var reg = /(a*)*b/
var str = 'a'.repeat(28); // aaaaaaaaaaaaa...
reg.exec(str)
console.timeEnd('reg')
复制代码
先简单了解一下正则的实现引擎,主要分为DFA和NFA
DFA与NFA
原因分析
a*
由于贪婪模式可以直接匹配整个字符串, 但是由于b的存在,所以需要回溯,但是无论怎么回溯都不可能成功,但是NFA是机器,会一直不断的进行回溯,由于(a*)*
可以认为是两层的量词组合,所以复杂度会随着字符串的长度指数级的升高。
由于是有限状态机,所以并不会死循环,只是会占用大量的cpu,在一定时间之后会完成。
工具
在线网站:
付费软件: