正则表达式是程序员的有力武器,但对于复杂的正则表达式,很多人可能感到困惑。今天,我们来分析一段高级正则表达式,并探讨它的内涵与应用场景。
const regexp = /("[^"\\]*(?:\\.[^"\\]*)*")|('[^'\\]*(?:\\.[^'\\]*)*')|(\/\*[^\/\*]*(?:(?:\*|\/)[^\/\*]*)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))|(,\s*[}\]])/g;
这个表达式非常长,我们可以先从大体上拆分一下它的结构:
/(...)|(...)|(...)|(...)|(...)/
可以看到整个正则表达式就是有五个分组构成,我们分别对这五个分组进行详细拆解:
"[^"\\]*(?:\\.[^"\\]*)*"
**"
**:匹配双引号;[^"\\]*
**:匹配非双引号和非反斜杠的字符零次或多次;(?:\\.[^"\\]*)*
**:非捕获分组,匹配转义字符后的任意字符,以及随后的非双引号和非反斜杠的字符零次或多次;这个分组可以重复零次或多次;"
**:匹配双引号。这个正则的作用就是匹配一个双引号字符串,比如:
"Hello World"
我们先不管它怎么实现,如果自己要实现怎么办?简单的思路就是:
/"[^"]*"/
这个我相信大多数人都能写出来,无非就是匹配两个引号和中间的内容。但这里的问题就在于转义字符,比如我这个字符串是这样的:
"Hello \" World"
如果用上面的正则去匹配,在遇到 \"
就终止了,实际上难点就是把这些转义字符也识别出来,不能让他们阻断整个表达式。
识别转义字符很简单,就是一个反斜杠加上任意一个字符:
/\\./
在转义字符之后还有任意的字符,甚至还能有转义字符,所以剩余的匹配部分就是:
/[^\\"]*/
两个加起来,我们就能匹配到后面那个字符串了:
/\\.[^\\"]*/
这个匹配的是 `\" World`
再把这个模式重复多次,就可以匹配到多个转义字符以及后面的字符了:
/(\\.[^\\"]*)*/
最后,由于这个分组并不需要捕获,所以我们加上 ?:
来提高性能,其实这里最难的正则就出来了:
/(?:\\.[^\\"]*)*/
'[^'\\]*(?:\\.[^'\\]*)*'
**这个和1是一样的,只是双引号变成了单引号,就不赘述了。
\/\*[^\/\*]*(?:(?:\*|\/)[^\/\*]*)*?\*\/
**\/\*
**:匹配 **/*
**;[^\/\*]*
**:匹配非星号和非斜杠的字符零次或多次;(?:(?:\*|\/)[^\/\*]*)*?
**:非捕获分组,匹配星号或斜杠后的非星号和非斜杠的字符零次或多次;这个分组可以重复零次或多次,但尽量少重复(懒惰匹配);\*\/
**:匹配 */
。同样我们可以想象一下自己要匹配块注释怎么写,很自然地想到:
/\/\*(.*?)\*\//
这个正则可以匹配一般的注释,但是无法匹配嵌套注释的情况。
因此,我们和前面一样,把关键词先排除:
/\/\*[^\/\*]*\*\//
但这样会导致遇到 /*
就停止了,所以我们还得匹配 /*
:
/((\*|\/)[^\/\*]*)*?/
其实这样就实现了匹配一个 /
+ 一些其他字符,或者是 *
加上其他字符,并且是非贪婪匹配,这样能够匹配到最后的那个 */
。
把这几个加起来,再加上非捕获分组,就是这里的正则表达的含义了。
\/{2,}.*?(?:(?:\r?\n)|$)
**\/{2,}
**:匹配两个或更多的斜杠;.*?
**:匹配任意字符零次或多次,但尽量少重复(懒惰匹配);(?:(?:\r?\n)|$)
:非捕获分组,匹配换行符(\r\n
或 **\n
**)或字符串末尾。行注释就简单地多了,只需要匹配两个斜杠开头,然后一直匹配到换行符或者整个字符串的末尾就行。
,\s*[}\]]
**,
**:匹配逗号;\s*
**:匹配空白字符(空格、制表符、换行符等)零次或多次;[}\]]
**:匹配右大括号或右方括号。这个相信大家都能看懂了。
function stripComments(content) {
return content.replace(regexp, function (match, _m1, _m2, m3, m4, m5) {
// Only one of m1, m2, m3, m4, m5 matches
if (m3) {
// A block comment. Replace with nothing
return '';
} else if (m4) {
// Since m4 is a single line comment is is at least of length 2 (e.g. //)
// If it ends in \r?\n then keep it.
const length = m4.length;
if (m4[length - 1] === '\n') {
return m4[length - 2] === '\r' ? '\r\n' : '\n';
}
else {
return '';
}
} else if (m5) {
// Remove the trailing comma
return match.substring(1);
} else {
// We match a string
return match;
}
});
}
VSCode利用这段正则移除给定内容中的注释。以下是 stripComments
函数的逐行解析:
content.replace(regexp, ...)
方法查找并替换 content
中与正则表达式 regexp
匹配的内容。这个方法的第二个参数是一个回调函数,它根据匹配结果来决定替换内容。match
** 是整个匹配的字符串,**_m1
**, _m2
, m3
, m4
, m5
分别对应正则表达式中的捕获分组。这里,我们只关心 m3
, m4
, **m5
**,因为它们分别代表块注释、单行注释和多余的逗号。if (m3) { ... }
判断是否匹配到了块注释。如果是,返回空字符串(**''
**),即将块注释移除。else if (m4) { ... }
判断是否匹配到了单行注释。如果是,首先获取单行注释的长度。然后判断单行注释是否以换行符(**\r?\n
)结尾。如果以换行符结尾,则保留换行符;否则返回空字符串(''
**),即将单行注释移除。else if (m5) { ... }
判断是否匹配到了多余的逗号。如果是,返回匹配字符串去掉首字符的子字符串,即将多余的逗号移除。这个函数在VSCode中用来去除 json
中的注释,因为 json
本身是不支持注释的。
关于为什么 JSON 不支持注释,Douglas Crockford 在他的许多演讲和访谈中都谈到了这个问题。他的解释主要包括以下几点:
如果一定要用注释,可以像VSCode这样提供一个去除注释的方法,也可以使用 JSON5
。
JSON5
的起源可以追溯到 2012 年,当时 JSON
已经成为许多 Web 开发者的首选数据交换格式。然而,尽管 JSON
的简洁性和跨平台兼容性使其在许多场景中非常实用,但其严格的语法规则使得在某些方面使用起来不够便捷。
为了解决这些问题,JSON5
的创建者 Michael Bolin
开发了一个基于 JSON
的扩展,旨在使 JSON
更容易阅读和编写。Michael Bolin
受到了 ECMAScript 5
的启发,因此将其命名为 JSON5
。JSON5
的设计目标是继承原始 JSON
的优点,同时添加一些类似 JavaScript 对象字面量的功能,以提高灵活性和易用性。
JSON5
的主要特性包括支持注释、宽松的字符串引号规则、尾随逗号、更灵活的数字表示、未引用的属性名以及多行字符串。这些特性使得 JSON5
在阅读和编写方面更加友好,尤其适用于需要添加注释或使用更接近 JavaScript 语法的场景。
不过目前很多解析器都不支持 JSON5
,为了保证更高效简洁的性能,多半还是采用 VSCode
这种minify
的方式,在最后将注释剔除。
本文介绍了VSCode中如何实现去除JSON注释。由于JSON本身不支持注释,因此需要使用正则表达式去除注释。VSCode使用了一个很复杂的正则表达式的多个分组,分别用于匹配双引号内的字符串、单引号内的字符串、块注释、单行注释以及尾部多余的逗号。在这里我们详细拆解分析了整个正则的细节和作用。
虽然JSON不支持注释,但是可以使用JSON5这种扩展格式来支持注释。不过目前很多解析器都不支持JSON5,因此在实际开发中,还是需要使用类似VSCode这种minify的方式来去除注释,以保证更高效简洁的性能。