
复杂字符串分割的挑战
在前端或后端开发中,我们经常需要对字符串进行解析,将其拆分为多个有意义的片段。一个常见的挑战是,某些特定的模式既作为“分隔符”又需要作为结果的一部分被保留。例如,给定一个字符串 {{ text1 }} 123 {{text1}}{{text1}} {{ text1}}134,我们希望将其分割成一个数组,其中包含所有被 {{...}} 包裹的标签以及标签之间的普通文本,并且要精确保留原始的空白字符。此外,标签内部的变量名(如 text1)是动态的,并且其周围可能存在不确定的空白字符。
传统的 String.prototype.split() 方法通常会将分隔符从结果中移除,这不符合我们的需求。因此,我们需要一种更强大的工具——正则表达式结合 String.prototype.matchAll() 方法。
核心解决方案:正则表达式与 matchAll
解决此类问题的关键在于构建一个能够同时匹配两种类型内容的正则表达式:
- 被 {{ 和 }} 包裹的标签(包含其内部的变量和空白)。
- 不包含 {{ 或 }} 的普通文本。
我们将使用以下正则表达式:
立即学习“Java免费学习笔记(深入)”;
const regex = /\{\{\s*([^}]+)\s*\}\}|([^{}]+)/g;接下来,我们将逐一解析这个正则表达式的各个组成部分。
正则表达式解析
-
\{\{ 和 \}\}:
- \{ 和 \} 是正则表达式中的特殊字符,需要通过反斜杠 \ 进行转义,以匹配字面意义上的大括号。它们匹配标签的起始和结束标记。
-
*`\s`**:
- \s 匹配任何空白字符(包括空格、制表符、换行符等)。
- * 表示匹配前一个字符零次或多次。
- 因此,\s* 用于匹配标签内部变量名周围可能存在的任意数量的空白字符,使其具有良好的健壮性。
-
([^}]+) (捕获组 1):
- [^}] 匹配任何不是右大括号 } 的字符。
- + 表示匹配前一个字符一次或多次。
- 这个捕获组 ((...)) 的作用是提取 {{ 和 }} 之间实际的变量内容(例如 text1)。它确保了我们能获取到标签的“核心”部分,即使其周围有空白。
-
| (或运算符):
- 这个竖线表示“或”逻辑。它允许正则表达式匹配左侧的模式或者右侧的模式。
-
([^{}]+) (捕获组 2):
- [^}{] 匹配任何既不是左大括号 { 也不是右大括号 } 的字符。
- + 表示匹配前一个字符一次或多次。
- 这个捕获组用于匹配标签之间的所有普通文本内容,确保这些非标签部分也能被捕获。
-
g (全局标志):
- g 是全局标志(Global flag)。它指示正则表达式引擎查找字符串中所有可能的匹配项,而不是在找到第一个匹配项后就停止。这对于 matchAll() 方法至关重要。
使用 String.prototype.matchAll()
String.prototype.matchAll() 方法返回一个迭代器,其中包含所有匹配正则表达式的结果。每个结果都是一个数组,类似于 RegExp.prototype.exec() 的结果,其中 [0] 元素是整个匹配的字符串,后续元素是各个捕获组的内容。
为了获得我们所需的分割结果,我们需要遍历 matchAll 返回的迭代器,并根据捕获组来处理每个匹配项。
const input = `{{ text1 }} 123 {{text1}}{{text1}} {{ text1}}134`;
const regex = /\{\{\s*([^}]+)\s*\}\}|([^{}]+)/g;
const matches = [...input.matchAll(regex)].map(match => {
// 如果捕获组1有值,说明匹配到的是 {{...}} 标签
if (match[1] !== undefined) {
// 对标签内部内容进行trim(),然后重新包裹成 {{...}} 形式
return `{{${match[1].trim()}}}`;
}
// 否则,匹配到的是非标签文本
else if (match[2] !== undefined) {
// 直接返回捕获组2的内容,保留其原始空白
return match[2];
}
// 理论上不会出现,但作为兜底
return match[0];
});
console.log(matches);运行效果:
对于给定的输入字符串: {{ text1 }} 123 {{text1}}{{text1}} {{ text1}}134
上述代码将输出: ["{{text1}}", " 123 ", "{{text1}}", "{{text1}}", " ", "{{text1}}", "134"]
这个结果精确地满足了最初的需求:
- {{ text1 }} 被规范化为 {{text1}},移除了内部多余的空白。
- 普通文本 123 和 ` ` 的空白被完整保留。
注意事项与进阶考量
trim() 的作用与选择: 在上述解决方案中,我们对 match[1](即 {{...}} 内部的内容)使用了 .trim() 方法。这使得 {{ text1 }} 和 {{ text1}} 都能被统一处理为 {{text1}}。如果你的需求是完全保留 {{...}} 内部的所有原始字符(包括空白),则可以移除 trim(),直接使用 match[0] 或根据具体需求调整逻辑。但通常情况下,对标签内容进行标准化处理是更常见的做法。
动态变量名的兼容性: 此正则表达式 ([^}]+) 的设计使其能够捕获 {{ 和 }} 之间 任何非 } 的字符。这意味着它天然支持动态的变量名。无论 text1 变为 userName 还是 productID,正则表达式都能正确匹配并提取其内容,无需通过 new RegExp() 动态构建。只有当你需要严格匹配 特定 变量名时,才需要动态构建正则表达式。
性能考量: 对于非常大的字符串,matchAll 结合 map 的操作可能会有一定的性能开销。然而,对于大多数常见的字符串处理场景,这种方法是高效且可读性强的。如果遇到极端性能瓶颈,可能需要考虑更底层的字符串遍历或状态机解析。
-
边缘情况处理:
- 不完整的标签: 例如 {{abc 或 abc}}。当前的正则表达式会将它们视为普通文本的一部分,因为它们不完全符合 \{\{\s*([^}]+)\s*\}\} 的模式。如果需要对这些情况进行特殊处理或报错,需要在后续的逻辑中添加检查。
- 空标签: {{}}。根据 ([^}]+) 的定义(至少一个非 } 字符),它










