模式和修饰符
- 正则表达式(可叫作“regexp”或者“reg”)包含 模式 和可选的 修饰符。创建一个正则表达式对象有两种语法。
- 较长的语法:regexp = new RegExp("pattern", "flags"); //允许从字符串中动态地构造模式。
- 较短的语法,使用斜杠"/":regexp = /pattern/; //斜杠 "/" 会告诉 JavaScript 我们正在创建一个正则表达式。它的作用类似于字符串的引号。但是它不接受任何变量。
- 修饰符
- i:使用此修饰符后,搜索时不区分大小写;
- g:使用此修饰符后,搜索时会查找所有的匹配项,而不只是第一个;
- m:多行模式;
- u:开启完整的 unicode 支持;
- y:粘滞模式;
字符类
- 字符类(Character classes):是一个特殊的符号,匹配特定集中的任何符号。对于每个字符类,都有一个“反向类”,用相同的字母表示,但要以大写书写形式。“反向”表示它与所有其他字符匹配。
- \d —— 从 0 到 9 的数字;(digit)
- \D —— 非数字;
- \s —— 空格符号:包括空格,制表符 \t,换行符 \n 和其他少数稀有字符,例如 \v,\f 和 \r;空格是一个字符。与其他字符同等重要;(space)
- \S —— 除了 \s ;
- \w —— 拉丁字母,数字,下划线 '_';(word)
- \W —— 除了 \w;
- . —— 带有“s”标志时点字符类严格匹配任何字符,否则为除换行符 \n之外的任何字符;点表示“任何字符”,而不是“缺少字符”。必须有一个与之匹配的字符;
- 任意字符:/./s [\s\S] [\d\D] [^]
- 备注:正则表达式可能同时包含常规符号和字符类;
Unicode:修饰符 “u” 和 class \p{...}
- Unicode 中的每一个字符都具有很多的属性。它们描述了一个字符属于哪个“类别”,包含了各种关于字符的信息。查找具有某种属性的字符,写作 \p{…}。为了顺利使用 \p{…},一个正则表达式必须使用修饰符 u。修饰符 u 在正则表达式中提供对 Unicode 的支持。
- 这意味着两件事:
- 4 个字节长的字符被以正确的方式处理:被看成单个的字符,而不是 2 个 2 字节长的字符。
- Unicode 属性可以被用于查找中 \p{…}。
- 有了 unicode 属性我们可以查找给定语言中的词,特殊字符(引用,货币)等等。
锚点(Anchors):字符串开始 ^ 和末尾 $
- 锚点 ^ 和 $ 属于测试。它们的宽度为零。换句话来说,它们并不匹配一个具体的字符,而是让正则引擎测试所表示的条件(文本开头/文本末尾)。
- 这两个锚点 ^...$ 放在一起常常被用于测试一个字符串是否完全匹配一个模式。
- ^$:匹配空字符串,开始紧跟着结束。
Flag "m" — 多行模式
- 通过 flag /.../m 可以开启多行模式。这仅仅会影响 ^ 和 $ 锚符的行为。在多行模式下,它们不仅仅匹配文本的开始与结束,还匹配每一行的开始与结束。
词边界:\b
- 词边界 \b 是一种检查,就像 ^ 和 $ 一样。
- 当正则表达式引擎(实现搜索正则表达式的程序模块)遇到 \b 时,它会检查字符串中的位置是否是词边界。
- 有三种不同的位置可作为词边界:
- 在字符串开头,如果第一个字符是单词字符 \w。
- 在字符串中的两个字符之间,其中一个是单词字符 \w,另一个不是。
- 在字符串末尾,如果最后一个字符是单词字符 \w。
- \b 既可以用于单词,也可以用于数字。词边界 \b 不适用于非拉丁字母
转义,特殊字符
- 反斜杠 "\" 是用来表示匹配字符类的。所以它是一个特殊字符。还存在其它的特殊字符,这些字符在正则表达式中有特殊的含义。它们可以被用来做更加强大的搜索。
- 所有特殊字符的列表:[ \ ^ $ . | ? * + ( )
- 转义:如果要把特殊字符作为常规字符来使用,只需要在它前面加个反斜杠。这种方式也被叫做“转义一个字符”。
- 斜杠符号 '/' 并不是一个特殊符号,但是它被用于在 Javascript 中开启和关闭正则匹配:/...pattern.../,所以我们也应该转义它。
- 如果我们使用 new RegExp 来创建一个正则表达式实例,那么我们需要对其做一些额外的转义。
- 在字符串中的反斜杠表示转义或者类似 \n 这种只能在字符串中使用的特殊字符。这个引用会“消费”并且解释这些字符,比如说:
- \n —— 变成一个换行字符,
- \u1234 —— 变成包含该码位的 Unicode 字符,
- 其它有些并没有特殊的含义,就像 \d 或者 \z,碰到这种情况的话会把反斜杠移除。
- 所以调用 new RegExp 会获得一个没有反斜杠的字符串。就像函数会解析并执行一样。如果要修复这个问题,我们需要双斜杠,因为引用会把 \\ 变为 \:
- let regStr = "\\d\\.\\d";
- alert(regStr); // \d\.\d (correct now)
- let regexp = new RegExp(regStr);
- alert( "Chapter 5.1".match(regexp) ); // 5.1
- 小结
- 要在字面(意义)上搜索特殊字符 [ \ ^ $ . | ? * + ( ),我们需要在它们前面加上反斜杠 \(“转义它们”)。
- 如果我们在 /.../ 内部(但不在 new RegExp 内部),还需要转义 /。
- 传递一个字符串(参数)给 new RegExp 时,我们需要双倍反斜杠 \\,因为字符串引号会消费其中的一个。
集合和范围 [...]
- 集合:在方括号 […] 中的几个字符或者字符类意味着“搜索给定的字符中的任意一个”。集合可以在正则表达式中和其它常规字符一起使用。请注意尽管在集合中有多个字符,但它们在匹配中只会对应其中的一个
- 范围:方括号也可以包含字符范围。比如说,[a-z] 会匹配从 a 到 z 范围内的字母,[0-5] 表示从 0 到 5 的数字。可以在 […] 里面使用字符类。
- 字符类是某些字符集的简写
- \d —— 和 [0-9] 相同,
- \w —— 和 [a-zA-Z0-9_] 相同,
- \s —— 和 [\t\n\v\f\r ] 外加少量罕见的 unicode 空格字符相同。
- 排除范围:[^…]
- 在 […] 中不转义,绝大多数特殊字符可以在不转义的情况下使用,除了在方括号中有特殊含义的字符外,其它所有特殊字符都是允许不添加反斜杠的。
- 表示一个点符号 '.'。
- 表示一个加号 '+'。
- 表示一个括号 '( )'。
- 在开头或者结尾表示一个破折号(在这些位置该符号表示的就不是一个范围) `pattern:’-’。
- 在不是开头的位置表示一个插入符号(在开头位置该符号表示的是排除)'^'。
- 表示一个开口的方括号符号 '['。
- 但是如果你为了“以防万一”转义了它们,这也不会有任何问题。
- 但是如果你为了“以防万一”转义了它们,这也不会有任何问题。
量词 `+,*,?` 和 `{n}`
- 数量 {n}
- 缩写
贪婪量词和惰性量词
- 贪婪模式
- 默认情况下,正则表达式引擎会尝试尽可能多地重复量词。例如,\d+ 检测所有可能的字符。当不可能检测更多(没有更多的字符或到达字符串末尾)时,然后它再匹配模式的剩余部分。如果没有匹配,则减少重复的次数(回溯),并再次尝试。
- 贪婪搜索,为了查找到一个匹配项,正则表达式引擎采用了以下算法:
- 对于字符串中的每一个字符
- 用这个模式来匹配此字符。
- 若无匹配,移至下一个字符
- 在贪婪模式下(默认情况下),量词都会尽可能地重复多次。正则表达式引擎尝试用 .+ 去获取尽可能多的字符,然后再一步步地筛选它们。对于这个问题,我们想要另一种结果,这也就是懒惰量词模式的用途。
- 懒惰模式
- 通过在量词后添加问号 ? 来启用。在每次重复量词之前,引擎会尝试去匹配模式的剩余部分。
- 懒惰模式中的量词与贪婪模式中的是相反的。它想要“重复最少次数”。
- 我们能够通过在量词之后添加一个问号 '?' 来启用它,所以匹配模式变为 *? 或 +?,甚至将 '?' 变为 ??。
- 这么说吧:通常,一个问号 ? 就是一个它本身的量词(0 或 1),但如果添加另一个量词(甚至可以是它自己),就会有不同的意思 —— 它将匹配的模式从贪婪转为懒惰。
- 懒惰模式只能够通过带 ? 的量词启用,没加的依旧保持贪婪模式。
- 懒惰模式并不是针对贪婪搜索的灵丹妙药。另一种方式是“微调”贪婪搜索,我们很快就会见到更多的例子。
捕获组
- 模式的一部分可以用括号括起来 (...)。这称为“捕获组(capturing group)”。这有两个影响:
- 它允许将匹配的一部分作为结果数组中的单独项。
- 如果我们将量词放在括号后,则它将括号视为一个整体。
- 嵌套组:括号可以嵌套。在这种情况下,编号也从左到右。零索引始终保持完全匹配,然后按左括号将组从左到右编号。
- 可选组:即使组是可选的并且在匹配项中不存在(例如,具有数量词 (...)?),也存在相应的 result 数组项,并且等于 undefined。
- 搜索所有具有组的匹配项:matchAll
- matchAll 是一个新方法,可能需要使用 polyfill:https://github.com/ljharb/String.prototype.matchAll
- 当我们搜索所有匹配项(标志 g)时,match 方法不会返回组的内容。例如,查找字符串中的所有标签:
- let str = '<h1> <h2>';
- let tags = str.match(/<(.*?)>/g);
- alert( tags ); // <h1>,<h2></h2>
- 结果是一个匹配数组,但没有每个匹配项的详细信息。但是实际上,我们通常需要在结果中获取捕获组的内容。要获取它们,我们应该使用方法 str.matchAll(regexp) 进行搜索。就像 match 一样,它寻找匹配项,但有 3 个区别:
- 它返回的不是数组,而是一个可迭代的对象。
- 当标志 g 存在时,它将每个匹配组作为一个数组返回。
- 如果没有匹配项,则不返回 null,而是返回一个空的可迭代对象。
- 由 matchAll 所返回的每个匹配,其格式与不带标志 g 的 match 所返回的格式相同:它是一个具有额外的 index(字符串中的匹配索引)属性和 input(源字符串)的数组:
- 为什么 matchAll 的结果是可迭代对象而不是数组?
- 为什么这个方法这样设计?原因很简单 — 为了优化。
- 调用 matchAll 不会执行搜索。相反,它返回一个可迭代的对象,最初没有结果。每当我们对它进行迭代时才会执行搜索,例如在循环中。
- 因此,这将根据需要找到尽可能多的结果,而不是全部。
- 例如,文本中可能有 100 个匹配项,但是在一个 for..of 循环中,我们已经找到了 5 个匹配项,然后觉得足够了并做出一个 break。这时引擎就不会花时间查找其他 95 个匹配。
- 命名组:用数字记录组很困难。对于简单模式,它是可行的,但对于更复杂的模式,计算括号很不方便。我们有一个更好的选择:给括号起个名字。这是通过在开始括号之后立即放置 ?<name> 来完成的。
- 替换捕获组:
- 方法 str.replace(regexp, replacement) 用 replacement 替换 str 中匹配 regexp 的所有捕获组。这使用 $n 来完成,其中 n 是组号。
- 对于命名括号,引用为 $<name>。
- 非捕获组 ?:
- 有时我们需要括号才能正确应用量词,但我们不希望它们的内容出现在结果中。可以通过在开头添加 ?: 来排除组。
- 例如,如果我们要查找 (go)+,但不希望括号内容(go)作为一个单独的数组项,则可以编写:(?:go)+。
- 总结
- 括号将正则表达式的一部分组合在一起,以便量词可以整体应用。
- 括号组从左到右编号,可以选择用 (?<name>...) 命名。
- 可以在结果中获得按组匹配的内容:
- 方法 str.match 仅当不带标志 g 时返回捕获组。
- 方法 str.matchAll 始终返回捕获组。
- 如果括号没有名称,则匹配数组按编号提供其内容。命名括号还可使用属性 groups。
- 我们还可以使用 str.replace 来替换括号内容中的字符串:使用 $n 或者名称 $<name>。
- 可以通过在组的开头添加 ?: 来排除编号组。当我们需要对整个组应用量词,但不希望将其作为结果数组中的单独项时这很有用。我们也不能在替换字符串时引用此类括号。
模式中的反向引用:\N 和 \k<name></name>
- 不仅可以在结果或替换字符串中使用捕获组 (...) 的内容,还可以在模式本身中使用它们。
- 按编号反向引用:\N,可以使用 \N 在模式中引用一个组,其中 N 是组号。
- 按命名反向引用:\k<name>
选择(OR)|
- 选择是正则表达式中的一个术语,实际上是一个简单的“或”。在正则表达式中,它用竖线 | 表示。
前瞻断言与后瞻断言
- 当我们想根据前面/后面的上下文筛选出一些东西的时候,前瞻断言和后瞻断言(通常被称为“环视断言”)对于简单的正则表达式就很有用。
- 有时我们可以手动处理来得到相同的结果,即:匹配所有,然后在循环中按上下文进行筛选。请记住,str.matchAll 和reg.exec 返回的匹配结果有 .index 属性,因此我们能知道它在文本中的确切位置。但通常正则表达式可以做得更好。
- x(?=y) 前瞻肯定断言 x ,仅当后面跟着 y
- x(?!y) 前瞻否定断言 x ,仅当后面不跟 y
- (?<=y)x 后瞻肯定断言 x ,仅当跟在 y 后面
- (?<!y)x 后瞻否定断言 x ,仅当不跟在 y 后面
灾难性回溯
- 有些正则表达式看上去很简单,但是执行起来耗时非常非常非常长,甚至会导致 JavaScript 引擎「挂起」。
- 开发者们很容易一不小心就写出这类正则表达式,所以我们迟早会面对这种意外问题。
- 如何解决问题?
- 第一种去试着减少各种排列组合的数量。
- 有时候重写正则会比较麻烦,而且要推敲如何重写正则恐怕也并非易事。另一种思路是禁止量词的回溯。有些正则表达式我们人眼一看就知道无法匹配成功,但正则引擎还是会硬去尝试很多它的排列组合。
- 用前瞻视角解决问题,我们可以使用前瞻断言来防止回溯。
- 当我们需要禁止 + 进行回溯的话,我们只要把 (?=(\w+))\1 中的 \w 替换成更复杂的正则表达式就能实现了。
- “灾难性回溯(catastrophic backtracking)”,又译作“回溯陷阱”。我们有 2 种处理它的思路:
- 重写正则表达式,尽可能减少其中排列组合的数量。
- 防止回溯。
粘性标志 "y"
- y 标志允许在源字符串中的指定位置执行搜索。
- 标记 y 使 regexp.exec 正好在 lastIndex 位置,而不是在它之前,也不是在它之后。
正则表达式(RegExp)和字符串(String)的方法
- str.match(regexp):在字符串 str 中找到匹配 regexp 的字符。它有 3 种模式:
- 如果 regexp 不带有 g 标记,则它以数组的形式返回第一个匹配项,其中包含分组和属性 index(匹配项的位置)、input(输入字符串,等于 str):
- 如果 regexp 带有 g 标记,则它将所有匹配项的数组作为字符串返回,而不包含分组和其他详细信息。
- 如果没有匹配项,则无论是否带有标记 g ,都将返回 null。这是一个重要的细微差别。如果没有匹配项,我们得到的不是一个空数组,而是 null。忘记这一点很容易出错。如果我们希望结果是一个数组,我们可以这样写:let result = str.match(regexp) || [];
- str.matchAll(regexp):是 str.match “新改进的”变体。它主要用来搜索所有组的所有匹配项。与 match 相比有 3 个区别:
- 它返回包含匹配项的可迭代对象,而不是数组。我们可以用 Array.from 从中得到一个常规数组。
- 每个匹配项均以包含分组的数组形式返回(返回格式与不带 g 标记的 str.match 相同)。
- 如果没有结果,则返回的不是 null,而是一个空的可迭代对象。
- 注:如果我们用 for..of 来循环 matchAll 的匹配项,那么我们就不需要 Array.from 了。
- str.split(regexp|substr, limit):使用正则表达式(或子字符串)作为分隔符来分割字符串。我们可以用 split 来分割字符串,也可以用正则表达式。
- str.search(regexp):返回第一个匹配项的位置,如果未找到,则返回 -1;重要限制:search 仅查找第一个匹配项。
- str.replace(str|regexp, str|func):这是用于搜索和替换的通用方法,是最有用的方法之一。它是搜索和替换字符串的瑞士军刀。有一个陷阱。当 replace 的第一个参数是字符串时,它仅替换第一个匹配项。要匹配所有,应使用带 g 标记的正则表达式。第二个参数是一个替代字符串。我们可以在其中使用特殊字符:(对于需要“智能”替换的场景,第二个参数可以是一个函数。使用函数可以为我们提供终极替代功能,因为它可以获取匹配项的所有信息,可以访问外部变量,可以做任何事。)
- 符号:替换字符串中的操作
- $&:插入整个匹配项
- $`:在匹配项之前插入字符串的一部分
- $':在匹配项之后插入字符串的一部分
- $n:如果 n 是一个 1 到 2 位的数字,则插入第 n 个分组的内容,详见 捕获组
- $<name>:插入带有给定 name 的括号内的内容,详见 捕获组
- $$:插入字符 $
- regexp.exec(str):返回字符串 str 中的 regexp 匹配项。与以前的方法不同,它是在正则表达式而不是字符串上调用的。根据正则表达式是否带有标志 g,它的行为有所不同。如果没有 g,那么 regexp.exec(str) 返回的第一个匹配与 str.match(regexp) 完全相同。如果有标记 g,那么:
- 调用 regexp.exec(str) 会返回第一个匹配项,并将紧随其后的位置保存在属性 regexp.lastIndex 中。 -下一次同样的调用会从位置 regexp.lastIndex 开始搜索,返回下一个匹配项,并将其后的位置保存在 regexp.lastIndex 中。
- ...以此类推。 -如果没有匹配项,则 regexp.exec 返回 null,并将 regexp.lastIndex 重置为 0。
- more...
- 因此,重复调用会挨个返回所有的匹配项,属性 regexp.lastIndex 用来跟踪当前的搜索位置。
- 过去,在将 str.matchAll 方法添加到 JavaScript 之前,在循环中是通过调用 regexp.exec 来获取分组的所有匹配项。这个现在也可以使用,尽管对于较新的浏览器来说,str.matchAll 通常更方便。
- 我们可以通过手动设置 lastIndex,用 regexp.exec 从给定位置进行搜索。
- 如果正则表达式带有标记 y,则搜索将精确地在 regexp.lastIndex 位置执行,不会再继续了。这个方法在某些场景下很方便,例如需要用正则表达式从字符串的精确位置来“读取”字符串(而不是其后的某处)
- regexp.test(str):查找匹配项,然后返回 true/false 表示是否存在。
- 如果我们在不同的源字符串上应用相同的全局表达式,可能会出现错误的结果,因为 regexp.test 的调用会增加 regexp.lastIndex 属性值,因此在另一个字符串中的搜索可能是从非 0 位置开始的。
- 这正是因为在第二个测试中 regexp.lastIndex 不为零。
- 如要解决这个问题,我们可以在每次搜索之前设置 regexp.lastIndex = 0。或者,不调用正则表达式的方法,而是使用字符串方法 str.match/search/...,这些方法不用 lastIndex。