JavaScript 现在配备了一种新的正则表达式增强功能,称为“匹配索引”。假设您想在 JavaScript 代码中查找与保留字重叠的无效变量名,并输出一个插入符号和一个变量名下的“下划线”,例如
const function = foo;
^------- Invalid variable name
在上面的示例中,function
是一个保留字,不能用作变量名。为此,我们可以编写以下函数
function displayError(text, message) {
const re = /\b(continue|function|break|for|if)\b/d;
const match = text.match(re);
// Index `1` corresponds to the first capture group.
const [start, end] = match.indices[1];
const error = ' '.repeat(start) + // Adjust the caret position.
'^' +
'-'.repeat(end - start - 1) + // Append the underline.
' ' + message; // Append the message.
console.log(text);
console.log(error);
}
const code = 'const function = foo;'; // faulty code
displayError(code, 'Invalid variable name');
注意:为简单起见,上面的示例只包含一些 JavaScript 保留字。
简而言之,新的 indices
数组存储每个匹配的捕获组的起始和结束位置。当源正则表达式对所有生成正则表达式匹配对象的内置函数使用 /d
标志时,此新数组可用,包括 RegExp#exec
、String#match
和 String#matchAll
。
如果您想更详细地了解其工作原理,请继续阅读。
动机 #
让我们来看一个更复杂的例子,并思考如何解决解析编程语言的任务(例如 TypeScript 编译器 所做的)——首先将输入源代码拆分为标记,然后为这些标记提供语法结构。如果用户编写了一些语法错误的代码,您希望向他们提供有意义的错误,理想情况下,指向第一次遇到问题代码的位置。例如,给定以下代码片段
let foo = 42;
// some other code
let foo = 1337;
我们希望向程序员提供类似的错误
let foo = 1337;
^
SyntaxError: Identifier 'foo' has already been declared
为了实现这一点,我们需要一些构建块,第一个是识别 TypeScript 标识符。然后我们将重点放在查明错误发生的准确位置。让我们考虑以下示例,使用正则表达式来判断字符串是否为有效的标识符
function isIdentifier(name) {
const re = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
return re.exec(name) !== null;
}
注意:一个现实世界的解析器可以使用新引入的 正则表达式中的属性转义,并使用以下正则表达式来匹配所有有效的 ECMAScript 标识符名称
const re = /^[$_\p{ID_Start}][$_\u200C\u200D\p{ID_Continue}]*$/u;
为简单起见,让我们坚持使用我们之前的正则表达式,它只匹配拉丁字符、数字和下划线。
如果我们遇到像上面这样的变量声明错误,并且想要向用户打印确切的位置,我们可能想要扩展上面的正则表达式,并使用类似的函数
function getDeclarationPosition(source) {
const re = /(let|const|var)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/;
const match = re.exec(source);
if (!match) return -1;
return match.index;
}
可以使用 RegExp.prototype.exec
返回的匹配对象上的 index
属性,它返回整个匹配的起始位置。但是,对于上面描述的用例,您通常希望使用(可能多个)捕获组。直到最近,JavaScript 还没有公开捕获组匹配的子字符串的起始和结束位置的索引。
正则表达式匹配索引解释 #
理想情况下,我们希望在变量名的位置打印错误,而不是在 let
/const
关键字的位置(如上面的示例所示)。但为此,我们需要找到索引为 2
的捕获组的位置。(索引 1
指的是 (let|const|var)
捕获组,0
指的是整个匹配。)
如上所述,新的 JavaScript 功能 在 RegExp.prototype.exec()
的结果(子字符串数组)上添加了一个 indices
属性。让我们增强上面的示例以利用此新属性
function getVariablePosition(source) {
// Notice the `d` flag, which enables `match.indices`
const re = /(let|const|var)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/d;
const match = re.exec(source);
if (!match) return undefined;
return match.indices[2];
}
getVariablePosition('let foo');
// → [4, 7]
此示例返回数组 [4, 7]
,这是索引为 2
的组中匹配的子字符串的 [start, end)
位置。根据此信息,我们的编译器现在可以打印所需的错误。
附加功能 #
indices
对象还包含一个 groups
属性,它可以通过 命名捕获组 的名称进行索引。使用它,上面的函数可以改写为
function getVariablePosition(source) {
const re = /(?<keyword>let|const|var)\s+(?<id>[a-zA-Z_$][0-9a-zA-Z_$]*)/d;
const match = re.exec(source);
if (!match) return -1;
return match.indices.groups.id;
}
getVariablePosition('let foo');
对正则表达式匹配索引的支持 #
- Chrome: 从版本 90 开始支持
- Firefox: 不支持
- Safari: 不支持
- Node.js: 从版本 16 开始支持
- Babel: 不支持