在文本处理、模板引擎和高亮渲染的场景里,经常需要用正则表达式把字符串中的花括号、方括号和尖括号结构整体找出来,再替换或包裹成别的形式。这三类符号在正则语法里身份完全不同:花括号是量词语法的一部分,方括号是字符类的界定符,尖括号虽然在正则里是普通字符,但在HTML上下文里却牵扯到转义和安全问题。如果对转义规则理解不到位,写出来的正则要么匹配不到,要么匹配范围失控。本文从转义原理讲到完整实现,帮你把这个问题一次理清。

一、先弄清楚三组括号在正则里的真实身份
很多人写正则失败,根源在于没分清哪些字符需要转义。我们逐个来看这三组符号。
第一组是尖括号<和>。它们在正则表达式语法中属于普通字符,不承担任何特殊语法功能,直接写就能匹配字面上的尖括号。例如/<div>/可以直接匹配字符串中的<div>这段文本。需要注意的是,尖括号的问题不在正则这一层,而在HTML那一层:如果你的代码要插入到HTML文档中,必须先转义,否则可能被浏览器当成标签解析,甚至带来XSS风险。
第二组是方括号[和]。[在正则中是字符类的起始符,比如[a-z]表示小写字母区间。如果你想匹配字面上的方括号本身,必须用反斜杠转义,写成\[和\]。不转义的话,正则引擎会把[后面的内容当成字符类定义,轻则报错,重则匹配出完全无关的结果。这一点是新手最容易踩的坑。
第三组是花括号{和}。花括号用于量词,比如a{2,4}表示字母a出现2到4次。当{后面跟的不是合法量词格式时,不同引擎的行为不一样:有的当作普通字符,有的直接报错。JavaScript引擎在某些情况下能容忍不合法的花括号,但依赖这种宽松行为是不可靠的,规范做法是统一转义为\{和\},这样无论在哪个环境下行为都一致。
二、写出能匹配三类标签结构的正则模式
假设我们的任务是找出文本中形如{name}、[tag]、<tag>的三种结构,并给它们整体包裹上HTML标签。先从最简单的一对一匹配写起,再逐步合并成一个综合模式。
匹配花括号模板变量,可以用/\{([^{}]+)\}/g。这里的\{和\}匹配字面花括号,[^{}]表示不含花括号的任意字符,加号要求至少一个字符,这样能避免匹配空的花括号对{},也能防止跨层嵌套时匹配错乱。捕获组([^{}]+)把花括号内部的内容单独抓出来,方便后续处理。
匹配方括号标签同理,写成/\[([^\[\]]+)\]/g。注意字符类内部也要排除方括号本身,否则遇到[a[b]这种字符串时边界会判断错误。匹配尖括号标签则写成/<([^<>]+)>/g,尖括号本身不需要在正则层转义,但字符类里排除两个尖括号可以保证匹配不会越界。
三个模式可以用分支语法合并成一个:
// 综合匹配三种标签结构
const tagPattern = /\{([^{}]+)\}|\[([^\[\]]+)\]|<([^<>]+)>/g;
const text = "用户 {name} 的角色是 [admin],头像标记为 <avatar>";
let match;
while ((match = tagPattern.exec(text)) !== null) {
console.log("匹配到整体:", match[0]);
console.log("花括号内容:", match[1]);
console.log("方括号内容:", match[2]);
console.log("尖括号内容:", match[3]);
}使用exec配合全局标志g循环时要注意,正则对象的lastIndex属性会记录上次匹配位置,同一个正则对象在多次调用之间共享这个状态。如果中途调用了test方法,lastIndex会被改动,可能造成循环漏匹配。稳妥的做法是把正则定义在循环外部但避免混用,或者每次用String.prototype.matchAll来获取全部匹配,它返回一个可迭代对象,写法更干净。
三、用replace配合回调函数实现自动包裹
匹配只是第一步,实际需求往往是把匹配到的结构包裹起来,比如加高亮效果或者替换成模板值。这时replace的第二个参数传函数是最灵活的方案,函数的返回值就是替换后的文本。
// 给三种标签分别包裹不同颜色的 span
function wrapTags(source) {
return source.replace(
/\{([^{}]+)\}|\[([^\[\]]+)\]|<([^<>]+)>/g,
function (whole, curly, square, angle) {
if (curry !== undefined) {
return '<span class="curly">{' + curly + '}</span>';
}
if (square !== undefined) {
return '<span class="square">[' + square + ']</span>';
}
return '<span class="angle"><' + angle + '></span>';
}
);
}
const result = wrapTags("配置项 {host},开关 [debug],节点 <main>");
console.log(result);
// 输出:配置项 <span class="curly">{host}</span>,开关 <span class="square">[debug]</span>,节点 <span class="angle"><main></span>回调函数的第一个参数是整体匹配到的字符串,后面的参数依次对应各个捕获组。因为三个分支只会命中其中一个,所以用undefined判断来分辨来源,这是处理分支捕获组的标准手法。如果分支较多,可以考虑具名捕获组让代码更易读:
// 使用具名捕获组提升可读性
const namedPattern = /\{(?<curly>[^{}]+)\}|\[(?<square>[^\[\]]+)\]|<(?<angle>[^<>]+)>/g;
function wrapWithNames(source) {
return source.replace(namedPattern, function (whole, ...groups) {
const g = groups[groups.length - 1]; // 最后一个参数是具名组对象
if (g.curly) return '<b>{' + g.curly + '}</b>';
if (g.square) return '<b>[' + g.square + ']</b>';
return '<b><' + g.angle + '></b>';
});
}有一点必须强调:如果处理的文本来自用户输入,包裹后要输出到HTML页面,尖括号内容本身可能藏有恶意代码。比如用户输入<script>,你的正则会老老实实把它匹配出来再包进span里,浏览器解析时就会执行。正确顺序是先对原文做HTML转义,再做正则替换,或者替换时只信任白名单内的标签名。安全永远排在功能前面。
四、嵌套结构与边界情况的应对思路
上面的模式都刻意排除了嵌套,因为正则表达式本质上不适合处理任意深度的嵌套结构。如果你的数据里可能出现{a{b}c}这种嵌套,简单模式只会匹配到{b},外层花括号被截断。应对方式有两种:一是限制嵌套层数,用固定深度的展开写法,比如内层允许一层嵌套写成\{([^{}]|\{[^{}]*\})*\};二是放弃正则,改用逐字符扫描的状态机,遇到开括号压栈、闭括号弹栈,层数归零时截取完整结构。
状态机方式代码量稍大,但表达力完整,任何深度都能处理,而且可以顺手校验括号是否配对。对于模板引擎这类核心逻辑,建议直接采用状态机;对于日志高亮、临时文本处理这类轻量需求,限制层数的正则方案足够用,没必要过度设计。
另外提醒几个边界点:全角括号{}和半角{}是不同字符,正则里的\{匹配不了全角符号,如果数据来源混杂,需要在字符类里一并加入;空结构是否要匹配、未闭合的括号如何处理,都应该在写正则前明确成规则,否则线上数据一变,匹配结果就会出乎意料。把这些细节提前想清楚,正则才能真正稳定可靠地为业务服务。
JavaScript正则表达式标签匹配replace替换修改时间:2026-09-04 03:24:49