18-函数替换
17.16 函数替换
这是作者最喜欢的正则表达式特性,因为它允许将一个非常复杂的正则表达式拆分成一些简单的正则表达式。
再来看一个实际修改HTML元素的例子。假设我们正在编写一段程序来将所有 链接转换成一个特定的格式:希望保留class、id和href属性,并删除其他内容。问题在于,用户输入的HTML内容可能是散乱的。而且这些属性也可能不存在,即便存在,也无法保证他们有着相同的顺序。所以,必须考虑这样一些不同的输入(这只是众多不同输入中的一部分):
const html =
'<a class="foo" href="/foo" id="foo">Foo</a>\n' +
'<A href='/foo' Class="foo">Foo</a>\n' +
'<a href="/foo">Foo</a>\n' +
'<a onclick="javascript:alert('foo!')" href="/foo">Foo</a>';
至此大家应该意识到,用正则表达式来实现这个功能是一项非常艰巨的任务:因为存在太多可能的变化!不过,可以通过将表达式拆分成两个,从而大大减少变化的数量:一个用于识别标签,而另一个用于将标签替换成你期望的内容。
先来考虑第二个问题。如果只存在一个单独的标签,而希望移除class、id和href之外的所有属性,这就简单了。但即便是这样,如果不能保证属性按特定的顺序出现,还是会有问题。虽然有很多方法可以解决这个问题,但这里会使用String.prototype.split进行切割,这样就可以一次只考虑一个属性:
function sanitizeATag(aTag) {
// 获取标签...
const parts = aTag.match(/<a\s+(.*?)>(.*?)<\/a>/i);
// parts[1] 是<a>标签中的属性
// parts[2] 是<a>和</a>之间的内容
const attributes = parts[1]
// 接下来将其分割成独立的属性
.split(/\s+/);
return '<a ' + attributes
// 过滤掉class、id和href之外属性
.filter(attr => /^(?:class|id|href)[\s=]/i.test(attr))
// 将它们用空格连接起来
.join(' ')
// 关闭<a>标签
+ '>'
// 添加内容
+ parts[2]
// 添加闭合标签
+ '</a>';
}
这个函数比想象中的要长,不过为了更加清晰,可以将它分成不同的部分。注意即使在这个函数中,依旧使用了多个正则表达式:一个用来匹配标签,一个用来切割(使用一个正则表达式来识别一个或多个空格字符)字符串,还有一个用来过滤期望的属性。如果只用一个正则表达式来完成这些工作将会非常复杂。
接下来这部分就有意思了:在一个可能包含很多标签的HTML块中使用 sanitizeATag 函数。编写一个只匹配标签的正则表达式很简单:
html.match(/<a .*?>(.*?)<\/a>/ig);
但是要怎么使用它呢?在匹配时,可以将函数当做一个替换参数传给 String.prototype.replace 。到目前为止,只用过字符串作为替换参数。而使用函数则允许对每一个替换执行一个特定的操作。在完成这个例子之前,用 console.log 来看看它是如何工作的:
html.replace(/<a .*?>(.*?)<\/a>/ig, function(m, g1, offset) {
console.log(` tag found at ${offset}. contents: ${g1}`); });
传给 String.prototype.replace 的函数会按顺序接收以下参数:
- 整个匹配的字符串(等价于$&)。
- 匹配上的组(如果存在)。有多少个组,这种参数就会有多少个 。
- 原始字符串中的匹配偏移量(一个数字)。
- 原始字符串(很少使用)。
该函数的返回值就是用来替换正则表达式的字符串。在刚才的例子中,没有指定返回值,所以默认返回undefined,它会被转换成字符串后当做替换字符串使用。这个例子的重点是强调这种工作机制,而非真实的替换,所以这里并没有返回最终结果。现在来回顾一下这个例子,有了能够清理单个标签的函数,以及在HTML中查找标签的方法,所以可以将它们结合起来使用:
html.replace(/<a .*?<\/a>/ig, function(m) {
return sanitizeATag(m);
});
还可以进一步简化代码,因为 sanitizeATag 函数能够精确匹配 String.prototype.replace 所期望的内容,所以这里可以省去匿名函数,直接使用 sanitizeATag :
html.replace(/<a .*?<\/a>/ig, sanitizeATag);
希望上面的例子已经讲清楚了这个功能的强大之处 。记住一点,不论什么时候,当需要从一个大字符串中匹配小字符串,并且还要对小字符串做额外处理,都可以通过向String.prototype.replace中传入函数来解决这个问题!