最近重看 Vue 源码,发现一段很长很复杂的正则表达式,正则虽然博大精深,但是直接看语法总会不知所谓,所以干脆借着这段代码,重温一下正则表达式语法。
背景
在 HTML 标签中,通常包含各种属性。这些属性主要有四种形式:
- 双引号包裹的属性值:
<div class="example"></div> - 单引号包裹的属性值:
<div class='example'></div> - 无引号的属性值:
<div class=example></div> - 无值的布尔属性:
<input disabled />
现在需要一个正则,能同时匹配 class="example" class='example' class=example 和 disabled 这四种形式,并提取出属性名和属性值。
为了准确匹配这些不同形式的属性,Vue 使用了一个复杂的正则表达式。接下来让我们来分析这个正则表达式:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
看不懂很正常,我们后面会一步一步解析,首先我们给正则上色来更容易看清:
这个正则表达式可以分解为以下几个部分
第一部分:匹配任意个空白字符
^\s* 表示匹配属性名之前,任意个空白字符。
-
^它是一个锚点(anchor),指定匹配必须从输入字符串的起始位置开始。锚字符
有一些正则表达式的元素不用来匹配实际的字符,而是匹配指定的位置。我们称这些元素为正则表达式的锚。 正则表达式中的锚字符包括:
^用来匹配字符串的开始,多行检索时匹配一行的开头。$用来匹配字符串的结束,多行检索时匹配一行的结尾。\b用来匹配单词的边界,就是\w和\W之间的位置,或者\w和字符串的开头或结尾之间的位置。\B匹配非单词边界的位置。
例如
/\bJava\b/可以匹配Java却不匹配JavaScript。 -
\s*匹配零个或多个空白字符(比如空格、制表符等),表示允许属性之前有空白字符。\s是一个特殊的字符类,用于匹配任意空白字符。字符类
在正则中,如果不想匹配某一个特定的字符而是想匹配某一类字符,则需要使用字符类。
通过将直接量字符放入方括号内,可以组成字符类(character class)。一个字符类可以匹配它所包含任意 一个 字符。如
[abc]可以匹配 a,b,c 中任意一个字符。使用
^作为方括号中第一个字符来定义否定字符集,它匹配所有不包含在方框括号内的字符。字符类可以使用连字符来表示字符范围。比如匹配小写字母
[a-z],匹配任何字母和数字可以用[a-zA-Z0-9]。 一些常用的字符类,在 JavaScript 中有特殊的转义字符来表达它们。[...]匹配方括号内任意字符[^...]匹配不在方括号内任意字符.匹配除了换行符和其他 Unicode 行终止符之外的任意字符\w等价于[a-zA-Z0-9_]\W等价于[^a-zA-Z0-9_]\s匹配任何 Unicode 空白符\S任何非 Unicode 空白符的字符\d等价于[0-9]\D等价于[^0-9]
*表示匹配前一项 0 次或多次,所以\s*表示匹配 0 次或多次空白字符。重复
当一个模式需要被多次匹配的时候,正则表达式提供了表示重复的正则语法。
{n,m}匹配前一项至少 n 次,但不能超过 m 次{n,}匹配前一项至少 n 次{n}匹配前一项 n 次?匹配前一项 0 次或 1 次,等价于{0,1}+匹配前一项 1 次或多次,等价于{1,}*匹配前一项 0 次或多次,等价于{0,}
第二部分:匹配属性名
([^\s"'<>\/=]+) 第一个捕获组,用于匹配属性名。
[^...]是前面说的字符集中的否定字符集(negated character set),表示"匹配任何不在方括号内列出的字符",[^abc]表示匹配除了a、b、c以外的任何字符。这是一种用于排除特定字符的模式匹配方法。()表示一个分组并默认捕获在 JavaScript 正则中,圆括号有两个作用:
-
分组(Grouping),圆括号可以把单独的项组合成子表达式,以便可以像一个独立的单元用
|、*、+或者?对单元内的项进行处理 -
捕获(Capturing):默认情况下,圆括号会创建"捕获组",将匹配到的内容存储起来供后续使用:
- 在正则表达式内部,可以通过
\1,\2等反向引用之前的捕获组
// 匹配 HTML 标签对,确保开始和结束标签相同 // \1 是对第一个捕获组的反向引用,确保结束标签与开始标签相同 const tagRegex = /<([a-z]+)>.*?<\/\1>/; /<([a-z]+)>.*?<\/\1>/.test('<div>内容</div>'); // true /<([a-z]+)>.*?<\/\1>/.test('<div>内容</span>'); // false- 在JavaScript代码中,可以通过
match()或exec()方法的返回数组访问捕获的内容
// 匹配日期格式:YYYY-MM-DD const dateRegex = /(\d{4})-(\d{2})-(\d{2})/; const text = "今天是2023-11-25"; const match = text.match(dateRegex); console.log(match[0]); // "2023-11-25" (整个匹配) console.log(match[1]); // "2023" (第一个捕获组:年份) console.log(match[2]); // "11" (第二个捕获组:月份) console.log(match[3]); // "25" (第三个捕获组:日期) - 在正则表达式内部,可以通过
-
综上,这段正则表示捕获一个或多个(+)不是空白字符(\s)、引号("')、尖括号(<>)、斜杠(/)或等号(=)的任意字符,确保提取出完整的属性名称如"class"、"id" 或 "disabled"。
第三部分:匹配属性值
(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? 这部分匹配 ="value" ='value' =value 和没有属性值这几种情况。
首先看到这部分最外层是这个形式: (?: ... )?
如上面所说,默认情况下,圆括号会创建"捕获组",将匹配到的内容存储起来供后续使用,而如果我们需要创建一个分组,但是不想它被捕获,可以通过 (?: ... ) 的形式。
非捕获组
非捕获组是正则表达式中的一种特殊分组语法,使用
(?:...)表示。它与普通的捕获组(...)有以下关键区别:
- 不存储匹配内容:非捕获组不会将匹配的内容存储在内存中,也不会在结果数组中创建引用。
- 语法:使用
(?:pattern)而不是(pattern)。- 性能优势:由于不需要存储匹配结果,非捕获组通常比捕获组性能更好,特别是在有大量分组的复杂正则表达式中。
在分组后面添加 ?,上面重复一节有说过,表示这段内容是可选的,可以出现也可以不出现。
整个非捕获组用于处理属性值,属性值可以有,也可以没有。
3.1、等号
\s*(=)\s*匹配等号和周围的空白,因为class="example"和class = "example"都是合法的。\s*匹配零个或多个空白字符,允许等号左右有空白。(=)第二个捕获组,匹配属性名和属性值之间的等号。
3.2、属性值
(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)) 这部分用于匹配属性值,不论值是否被引号包围。
-
(?:)是非捕获分组,上面已经说过了。 -
(?:...|...|...)用|把这个分组分成了三部分,匹配任意一部分即可。选择
字符
|用于分隔供选择的模式,匹配时会尝试从左到右匹配每一个分组,直到发现匹配项。如/ab|bc|cd/可以匹配字符串'ab'、'bc'和'cd'。- 选项1:
"([^"]*)"+- 双引号值,整个正则的第三个捕获组- 开始的
"匹配一个双引号 ([^"]*)是捕获组,匹配双引号内的内容(任意个非双引号字符,遇到双引号则证明属性值结束)- 结尾的
"+匹配一个或多个双引号(理论上这里只需要匹配一个双引号,猜测这里的+可能是处理书写不规范的情况)
- 开始的
- 选项2:
'([^']*)'+- 单引号值,整个正则的第四个捕获组- 开始的
'匹配一个单引号 ([^']*)是捕获组,匹配单引号内的内容(任意个非单引号字符)- 结尾的
'+匹配一个或多个单引号
- 开始的
- 选项3:
([^\s"'=<>`]+)- 无引号值,整个正则的第五个捕获组- 这是一个捕获组,匹配一个或多个不是以下字符的任意字符:
\s空白字符,"双引号,'单引号,=等号,<小于号,>大于号,`反引号
- 这是一个捕获组,匹配一个或多个不是以下字符的任意字符:
- 选项1:
捕获组
这个正则一共有五个捕获组,分别表示:属性名、等号、双引号属性值、单引号属性值、无引号属性值,我们拿不同的属性来进行测试:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
attribute.exec(`class="example"`)
// ['class="example"', 'class', '=', 'example', undefined, undefined]
attribute.exec(`class='example'`)
// ["class='example'", 'class', '=', undefined, 'example', undefined]
attribute.exec(`class=example`)
// ['class=example', 'class', '=', undefined, undefined, 'example']
attribute.exec(`disabled`)
// ['disabled', 'disabled', undefined, undefined, undefined, undefined]
可以看到,不同形式的属性值,都会被匹配,且会匹配到不同的分组。对于无属性值的形式,则只会匹配到属性名。
总结
通过一个正则,我们学习了 “锚”,“字符类”,“重复”,“分组”,“捕获”,“非捕获组”,“选择”等知识点,不得不说,阅读源码确实能学到东西。
最后,如果对文章有任何问题欢迎留言:D