正则之坑知多少

cover

原文地址: 又双叒叕学习了一遍正则表达式javascript

前两天在 Twitter 上看到了题图,感受又是个大坑,在此介绍正则自己和在 JavaScript 中使用正则的坑。若有错误,烦请指正。java

首先说说 JavaScript 中正则的坑。正则表达式

字面量 VS RegExp()

在 JavaScript 中建立正则表达式有两种方式:编程

// 正则字面量
var pattern1 = /\d+/;

// 构造 RegExp 实例,以字符串形式传入正则
var pattern2 = new RegExp('\\d+');
复制代码

两种方式建立出的正则没有任何差异。从建立方式上看,正则字面量可读性更优,由于正则中常用 \ 反斜杠在字符串中是一个转义字符,想以字符串中表示反斜杠的话,须要使用 \\ 两个反斜杠。数组

可是,须要注意,每一个正则表达式都有一个独立的对象表示,每次建立正则表达式,都会为其建立一个新的正则表达式对象,这和其它类型(字符串、数组)不一样浏览器

咱们能够经过让正则表达式只编译一次并将其保存在一个变量中以供后续使用来实现优化。安全

所以,第一段代码将建立三个正则表达式对象,并进行了三次编译,虽然表达式是相同的。而第二段代码则性能更高。编程语言

console.log(/abc/.test('a'));
console.log(/abc/.test('ab'));
console.log(/abc/.test('abc'));

var pattern = /abc/;
console.log(pattern.test('a'));
console.log(pattern.test('ab'));
console.log(pattern.test('abc'));
复制代码

这其中有性能隐患。先记住这一点,咱们继续往下看。post

冷知识 lastIndex

这里咱们来解释下题图中的状况是怎么回事。性能

cover

这实际上是全局匹配的坑,也就是正则后的 /g 符号。

var pattern = /abc/g;
console.log(pattern.global) // true
复制代码

/g 标识的正则做为全局匹配,也就拥有了 global 属性并致使了题图中呈现的异常行为。

全局正则表达式的另外一个属性 lastIndex 用于存放上一次匹配文本以后的第一个字符的位置。

RegExp.prototype.exec()RegExp.prototype.test() 方法都以 lastIndex 属性中所存储的位置做为下次正则匹配检索的起点。连续调用这两个方法就能够遍历字符串中的全部匹配文本。

lastIndex 属性可读写,当 RegExp.prototype.exec()RegExp.prototype.test() 再也找不到能够匹配的文本时,会自动把 lastIndex 属性重置为 0。所以使用这两个方法来检索文本,是能够无限执行下去的。咱们也就明白了题图中为什么每次执行 RegExp.prototype.test() 返回的结果都不同。

不只如此,看看下面这段代码,能看出来有什么问题吗?

var count = 0;
while (/a/g.test('ababc')) count++;
复制代码

不要轻易拷贝到控制台中尝试,会把浏览器卡死的。

因为每一个循环中 /a/g.test('ababc') 都建立了新的正则表达式对象,每次匹配都是从新开始,这一操做会无限执行下去,造成死循环。

正确的写法是:

var count = 0;
var regex = /a/g;
while (regex.test('ababc')) count++;
复制代码

这样,每次循环中操做的都是同一个正则表达式对象,随着每次匹配后 lastIndex 的增长,等到将整个字符串匹配完成后,就跳出循环了。

给以上知识点画个重点

  1. 将正则表达式保存到变量中,只在逻辑中使用这个变量,不只性能更高,还安全。
  2. 谨慎使用全局匹配,RegExp.prototype.exec()RegExp.prototype.test()这两个方法的执行结果可能每次都不一样。
  3. 作到了以上两点后,还要谨慎在循环中使用正则匹配。

回溯陷阱 Catastrophic Backtracking

回溯陷阱是正则表达式自己的一个坑了,会致使很是严重的性能问题,事故现场能够参看《一个正则表达式引起的血案,让线上 CPU100% 异常!》

简单介绍一下回溯陷阱的问题源头,正则引擎分为 NFA(肯定型有穷自动机)DFA(不肯定型有穷自动机)DFA 是从匹配文本入手,同一个字符不会匹配两次(能够理解为手里捏着文本,挨个字符拿去匹配正则),时间复杂度是线性的,它的功能有限,不支持回溯。大多数编程语言选用的都是 NFA,至关于手里拿着正则表达式,去匹配文本。

/(a(bdc|cbd|bcd)/ 中已经有三种匹配路径,在 NFA 中,以文本 'abcd' 为例,将花费 7 步才能匹配成功:

regex101
(图中还包括了字符边界的匹配步骤,所以多了三步)

  1. 正则中的第一个字符 a 匹配到 'abcd' 中的第一个字母 'a',匹配成功。
  2. 此时遇到了匹配路径的分叉口,bdc 或 cbd 或 bcd,先使用 bdc 来匹配。
  3. bdc 中的第一个字符 b 匹配到了 'abcd' 中的第二个字母 'b',匹配成功。
  4. bdc 中的第二个字符 d 与 'abcd' 中的第三个字母 'c' 不匹配,这条路径匹配失败,此时将发生回溯(backtrack),把 'b' 还回去。选择第二条路径 cbd 进行匹配。
  5. cbd 的第一个字符 'c' 就与 'b' 匹配失败。开始第三条路径 bcd 的匹配。
  6. bcd 的第一个字符 'b' 与文本 'b' 匹配成功。
  7. bcd 的第一个字符 'c' 与文本 'c' 匹配成功。
  8. bcd 的第一个字符 'd' 与文本 'd' 匹配成功。

至此匹配完成。

可想而知,若是正则中再多一些匹配路径或者匹配本文再长一点,匹配步骤将多到难以控制。

好比用 /(a*)*bc/ 来匹配 'aaaaaaaaaaaabc' 都会致使性能问题,匹配文本中每增长一个 'a',都会致使执行时间翻倍。

禁止这种回溯陷阱的方法有两种:

  1. 占有优先量词(Possessive Quantifiers)
  2. 原子分组(Atomic Grouping)

惋惜 JavaScript 不支持这两种语法,有兴趣能够 Google 自行了解下。

在 JavaScript 中咱们没有方法能够直接禁止回溯陷阱,咱们只能:

  1. 避免量词嵌套 (a*)* => a*
  2. 减小匹配路径

除此以外,咱们也能够把正则匹配放到 Service Worker 中进行,从而避免影响页面性能。

查资料的时候发现,回溯陷阱不只会致使性能问题,也有安全问题,有兴趣能够看看先知白帽大会上的《WAF是时候跟正则表达式说再见》分享。

参考资料

相关文章
相关标签/搜索