阅读 299

JavaScript正则表达式

前言

正则表达式是用于匹配字符串中字符组合的模式。在 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']

复制代码

量词

CharactersMeaning
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

原因分析

  1. a* 由于贪婪模式可以直接匹配整个字符串, 但是由于b的存在,所以需要回溯,但是无论怎么回溯都不可能成功,但是NFA是机器,会一直不断的进行回溯,由于 (a*)* 可以认为是两层的量词组合,所以复杂度会随着字符串的长度指数级的升高。

由于是有限状态机,所以并不会死循环,只是会占用大量的cpu,在一定时间之后会完成。

工具

在线网站:

regex101.com/

regexr.com/

付费软件:

www.regexbuddy.com/buynow.html…

文章分类
前端
文章标签