JS编程建议——45:警惕嵌套量词和回溯失控

建议45:警惕嵌套量词和回溯失控
嵌套量词总是需要额外的关注和小心,以确保没有掩盖回溯失控问题。嵌套量词出现在一个自身被重复量词修饰的组中。
嵌套量词本身并不会造成性能危害,只是在尝试匹配字符串过程中,很容易不小心在内部量词和外部量词之间,产生一大堆分解文本的方法。例如,要匹配HTML 标签,使用了下面的正则表达式:
/<(?:1|"2"|'3')*>/
这也许过于简单,因为它不能正确地处理所有情况的有效和无效标记,但在处理有效HTML 片段时应该没什么问题。与更加简单的/<4*>/相比,它的优势在于涵盖了出现在属性值中的>符号。在非捕获组中它不使用第二和第三分支,仅匹配单引号和双引号包围的属性值,除特定的引号外允许所有字符出现。
虽然遇到了嵌套量词*,但目前还没有回溯失控的危险。在分组的每次重复过程中,由于第二和第三分支选项严格匹配一个带引号的字符串,所以潜在的回溯点数目随目标字符串长度而呈线性增长。
但是,查看非捕获组的第一分支:1,每次只匹配一个字符,效率似乎有些低。在字符类后面加一个量词会更好些,这样每次组重复过程就可以匹配多于一个的字符了。正则表达式可以在目标字符串的位置上发现一个匹配。通过每次匹配多个字符,正则表达式会在成功匹配的过程中跳过许多不必要的步骤。
如果正则表达式匹配一个“<”字符,但后面没有“>”,则可以令匹配成功完成,回溯失控就会进入“快车道”,因为内部量词和外部量词的排列组合产生了数量巨大的分支路径(跟在非捕获组之后)用以匹配“<”之后的文本。正则表达式在最终放弃匹配之前必须尝试所有的排列组合。
关于嵌套量词导致回溯失控,一个更加极端的例子是,在一大串A 上应用正则表达式/(A+A+)+B/。虽然这个正则表达式写成/AA+B/更好,但为了讨论方便,设想一下两个A 能够匹配同一个字符串的多少种模板。
当应用在一个由10 个A 组成的字符串上(“AAAAAAAAAA”)时,正则表达式首先使用第一个A+匹配所有10 个字符,然后正则表达式回溯一个字符,让第二个A+匹配最后一个字符。这个分组试图重复,但没有更多的A ,而且分组中的+量词已经符合匹配所需的最少一次,因此正则表达式开始查找B。虽然正则表达式没有找到B,但是还不能放弃,因为还有许多路径没有被测试过。如果第一个A+匹配8 个字符,第二个A+匹配2 个字符会怎么样呢?或者第一个匹配3个,第二个匹配2个,分组重复两次,又会怎么样呢?如果在分组的第一遍重复中,第一个A+匹配2个字符,第二个A+匹配3 个字符,然后在第二遍重复中,第一个匹配1 个,第二个匹配4个,又怎么样呢?
正则表达式在最坏情况下的复杂性是惊人的O(2n),也就是2 的n 次方。n 表示字符串的长度。在由10 个A 构成的字符串中,正则表达式需要1024 次回溯才能确定匹配失败,如果是20 个A,回溯的数字剧增到一百万以上。25 个A足以挂起Chrome、IE、Firefox和Opera 浏览器至少10 分钟(如果还没死机)用以处理超过34 000 000次回溯以排除正则表达式的各种排列组合。唯一的例外是最新的Safari浏览器,它能够检测到正则表达式陷入了循环,并快速终止匹配(Safari 浏览器还限定了回溯的次数,超出则终止匹配尝试)。
预防此类问题的关键是确保正则表达式的两个部分不能对字符串的同一部分进行匹配。这个正则表达式可重写为/AA+B/,但复杂的正则表达式可能难以避免此类问题。虽然还有其他解决办法,但是增加一个模拟原子组往往作为最后一招使用,如果可能,尽可能保持正则表达式简单易懂。如果这么做,此正则表达式将改成/((?=(A+A+))2)+B/,就可以彻底消除回溯问题。


  1. >"'
  2. "
  3. '
  4. >
时间: 2024-07-30 02:26:39

JS编程建议——45:警惕嵌套量词和回溯失控的相关文章

JS编程建议——46:提高正则表达式执行效率

建议46:提高正则表达式执行效率(1)关注如何让匹配更快失败正则表达式处理慢往往是因为匹配失败过程慢,而不是匹配成功过程慢.使用正则表达式匹配一个很大字符串的一小部分,情况更为严重,正则表达式匹配失败的位置比匹配成功的位置要多得多.一个修改使正则表达式匹配更快但失败更慢,例如,通过增加所需的回溯次数尝试所有分支的排列组合,这通常是一个失败的修改.(2)正则表达式以简单的.必需的字元开始最理想的情况是,一个正则表达式的起始字元应当尽可能快速地测试并排除明显不匹配的位置.用于此目的好的起始字元通常是

JS编程建议——1:警惕Unicode乱码

建议1:警惕Unicode乱码ECMA标准规定JavaScript语言基于Unicode标准进行开发,JavaScript内核完全采用UCS字符集进行编写,因此在JavaScript代码中每个字符都使用两个字节来表示,这意味着可以使用中文来命名变量或函数名.虽然ECMAScript v3标准允许Unicode字符出现在JavaScript程序的任何地方,但是在v1和v2中,ECMA标准只允许Unicode字符出现在注释或引号包含的字符串直接量中,在其他地方必须使用ASCII字符集,在ECMASc

JS编程建议——26:小心if嵌套的思维陷阱

建议26:小心if嵌套的思维陷阱人的思维是非常复杂的,这在一定程度上会增加if结构嵌套的复杂性.假设有4个条件,只有当这些条件全部成立时,才允许执行某件事情.遵循人的一般思维习惯,在检测这些条件时,常常会沿用下面这种结构嵌套: if(a){ if(b){ if(c){ if(d){ alert("所有条件都成立!"); } else{ alert("条件d不成立!"); } } else{ alert("条件c不成立!"); } } else{

JS编程建议——36:警惕字符串连接操作(1)

建议36:警惕字符串连接操作(1)字符串连接表现出惊人的"性能紧张".一个任务通过一个循环向字符串末尾不断地添加内容,以创建一个字符串.例如,创建一个HTML 表或一个XML 文档.此类处理在一些浏览器上表现得非常糟糕.当连接少量字符串时,这些问题都可以忽略,临时使用可选择最熟悉的操作.当合并字符串的长度和数量增加之后,有些函数开始显示出"威力".(1)+.+=+.+=运算符提供了连接字符串的最简单方法.除IE 7及其以前版本外,当前所有浏览器都对这种方法优化得很好

JS编程建议——36:警惕字符串连接操作(2)

建议36:警惕字符串连接操作(2)先将两个小字符串合并起来,然后将结果返回给大字符串.创建中间字符串s1 + s2与两次复制大字符串相比,对性能的"冲击"要轻得多.(2)编译期合并在赋值表达式中所有字符串连接都属于编译期常量,Firefox自动地在编译过程中合并它们.在以下这个方法中可看到这一过程: function foldingDemo() { var str = "compile" + "time" + "folding"

JS编程建议——8:谨慎使用运算符(1)

建议8:谨慎使用运算符(1)1.用===,而不用==JavaScript有两组相等运算符:===和!==.==和!=.===和!==这一组运算符会按照期望的方式工作.如果两个运算数类型一致且拥有相同的值,那么===返回true,而!==返回false.==和!=只有在两个运算数类型一致时才会做出正确的判断,如果两个运算数是不同的类型,会试图强制转换运算数的类型.转换的规则复杂且难以记忆,具体规则如下: '' == '0' // false 0 == '' // true 0 == '0' //

JS编程建议——48:慎用正则表达式修剪字符串

建议48:慎用正则表达式修剪字符串(1)使用两个子表达式修剪字符串去除字符串首尾的空格是一个简单而常见的任务,但到目前为止JavaScript 还没有实现它.正则表达式允许用很少的代码实现一个修剪函数,最好的全面解决方案可能是使用两个子表达式:一个用于去除头部空格,另一个用于去除尾部空格.这样处理简单而快速,特别是处理长字符串时.if(!String.prototype.trim) { String.prototype.trim = function() { return this.replac

JS编程建议——59:推荐动态调用函数

建议59:推荐动态调用函数调用函数更便捷的方式是使用Function对象的call和apply方法.apply与call方法在本质上没有太大区别,只不过它们传递给函数的参数方式不同, apply是以数组形式进行参数传递,而call方法可以同时传递多个值.如果某个函数仅能够接收多个参数列表,而现在希望把一个数组的所有元素作为参数进行传递,那么使用apply方法就显得非常便利.function max(){ var m = Number.NEGATIVE_INFINITY; // 声明一个负无穷大的

JS编程建议——2:正确辨析JavaScript句法中的词、句和段

建议2:正确辨析JavaScript句法中的词.句和段JavaScript语法包含了合法的JavaScript代码的所有规则和特征,它主要分为词法和句法.词法包括字符编码.名词规则.特殊词规则等.词法侧重语言的底层实现(如语言编码问题等),以及基本规则的定义(如标识符.关键字.注释等).它们都不是最小的语义单位,却是构成语义单位的组成要素.例如,规范字符编码集合.命名规则.标识符.关键字.注释规则.特殊字符用法等.句法定义了语言的逻辑和结构,包括词.句和段的语法特性,其中段体现逻辑的结构,句表达