d标志是ES2022正式引入的一个正则表达式标志,全称是hasIndices,也就是带索引的匹配。它的作用很简单:让正则表达式在执行匹配时,除了返回匹配到的文本内容,还额外记录匹配结果在原始字符串中的起始位置和结束位置,包括各个捕获组的索引信息。在没有这个标志之前,我们只能通过exec方法的返回值拿到整个匹配的起始位置,捕获组的具体位置需要手动计算,非常麻烦。本文将详细介绍d标志的用法、原理以及实际应用场景。

d标志的基本用法
使用d标志非常简单,只需要在创建正则表达式时在标志位加上字母d即可。可以写在字面量形式中,也可以通过RegExp构造函数传入。加上了d标志之后,匹配结果对象上会多出一个indices属性,它是一个数组,结构与传统的匹配数组一一对应,只是每个元素变成了一个包含开始和结束位置的二元素数组。
来看一个最基础的例子,我们匹配字符串中的一个日期,并获取它的位置:
// 使用字面量形式添加d标志
const re = /(\d{4})-(\d{2})/d;
const result = re.exec('发布日期是2023-06-15,共300页');
console.log(result[0]); // '2023-06'
console.log(result.indices[0]); // [10, 17] 整个匹配的开始和结束索引
console.log(result.indices[1]); // [10, 14] 第一个捕获组'2023'的索引
console.log(result.indices[2]); // [15, 17] 第二个捕获组'06'的索引
console.log(result.index); // 10 传统属性,只有整体匹配的起始位置可以看到,indices[0]对应整个匹配,indices[1]对应第一个捕获组,以此类推,下标关系与传统的捕获组数组完全一致。结束索引是不包含在匹配内的,遵循左闭右开区间,这一点与字符串的slice方法保持一致,所以可以直接用slice切出原文。
需要注意一点,如果不加d标志,匹配结果对象上是没有indices属性的,值为undefined。所以在使用前最好判断一下,或者干脆统一都加上d标志。另外,通过正则实例的hasIndices属性可以检测当前正则是否启用了该标志:
const re = /abc/d;
console.log(re.hasIndices); // true
const re2 = /abc/;
console.log(re2.hasIndices); // false
// 在函数中做兼容判断
function safeMatch(re, str) {
const m = re.exec(str);
if (m && m.indices) {
return m.indices[0];
}
return m ? [m.index, m.index + m[0].length] : null;
}命名捕获组与indices的配合使用
d标志与命名捕获组搭配使用时体验最佳。当正则表达式中使用了命名捕获组后,indices属性上会出现一个groups对象,键名就是各命名组的名字,这样就不需要去数下标了,代码可读性大幅提升。这在解析格式化文本时特别实用,比如解析日志行、配置文件或者自定义模板。
const logRe = /(?<time>\d{2}:\d{2}:\d{2})\s+(?<level>\w+):\s+(?<msg>.*)/d;
const line = '10:30:45 ERROR: 数据库连接超时';
const m = logRe.exec(line);
if (m) {
const { time, level, msg } = m.indices.groups;
console.log('时间位置:', time); // 例如 [0, 8]
console.log('级别位置:', level); // 例如 [9, 14]
console.log('信息位置:', msg);
// 用索引反查原文,验证正确性
console.log(line.slice(...time)); // '10:30:45'
console.log(line.slice(...level)); // 'ERROR'
}上面代码中用到了展开语法slice(...time),这是因为indices的每个元素恰好是开始和结束索引组成的数组,直接展开传给slice就能拿到对应原文,写起来非常简洁。命名组的indices在IDE补全和后期维护上都比数字下标更可靠,建议在正式项目中优先使用命名捕获组。
还有一个细节值得了解:未参与匹配的捕获组,其对应的indices元素是undefined,而不是一个无效的位置数组。比如可选分组没有匹配到内容时,传统捕获组数组中该位置是空字符串,而indices中对应位置则是undefined,使用时要做好判空处理:
const re = /(\d+)(?:-(\d+))?/d;
const m1 = re.exec('页码12-30');
console.log(m1.indices[1]); // [2, 4]
console.log(m1.indices[2]); // [5, 7]
const m2 = re.exec('页码12');
console.log(m2.indices[1]); // [2, 4]
console.log(m2.indices[2]); // undefined 可选组未参与匹配在match与matchAll中使用d标志
d标志并不局限于exec方法,所有会返回匹配结果对象的方法都能享受它带来的便利,包括字符串的match方法、matchAll方法以及replace方法的回调参数。其中matchAll与d标志的组合最为实用,因为matchAll返回的是一个迭代器,配合for...of循环可以遍历所有匹配,每次迭代拿到的结果对象上都带有indices属性。
const re = /\b(\w+)@(\w+)\.com/dg;
const text = '联系alice@ippipp.com或bob@test.com';
for (const m of text.matchAll(re)) {
const [start, end] = m.indices[0];
console.log(`在位置${start}-${end}找到: ${m[0]}`);
console.log(`用户名: ${text.slice(...m.indices[1])}`);
}
// 在位置2-18找到: alice@ippipp.com
// 用户名: alice
// 在位置19-31找到: bob@test.com
// 用户名: bob这段代码里同时用了g标志和d标志,matchAll要求正则必须带g标志,否则会抛出TypeError。通过indices我们既能拿到匹配内容,又能知道它出现在原文的哪个位置,这在做语法高亮、关键词定位、文本diff等场景时是刚需。比如实现一个简单的高亮函数,可以把所有匹配位置的字符替换成带标签的形式,因为有精确的起止索引,处理重叠和顺序问题都很方便。
在replace的回调函数中同样可以使用,回调的第三个参数之后的参数是各捕获组的匹配内容,而回调的返回值不直接暴露indices,这一点要留意。如果需要在replace过程中使用索引信息,更推荐先用matchAll收集所有匹配及位置,再自行拼接结果字符串。另外, RegExp构造函数方式创建时,标志作为第二个参数传入即可,例如new RegExp('ab+c', 'dg'),适合在标志需要动态拼装的场景下使用。
兼容性与降级处理
d标志属于较新的语言特性,主流现代浏览器如Chrome、Firefox、Edge以及Node.js较新版本都已支持,但在一些老旧环境比如旧版IE或长期未更新的运行时中不可用。如果在不支持的环境传入d标志,会直接抛出SyntaxError,提示无效的正则标志,所以做兼容处理很有必要。
特性检测有两种思路:一种是通过try...catch包裹正则创建过程,捕获语法错误;另一种是利用RegExp上已知不会报错的标志来判断环境新旧,或者直接检测原型上是否存在hasIndices属性的描述符。示例如下:
// 方式一:try catch检测
function supportsDFlag() {
try {
new RegExp('a', 'd');
return true;
} catch (e) {
return false;
}
}
// 方式二:检测原型属性
const supported = Object.getOwnPropertyDescriptor(
RegExp.prototype, 'hasIndices'
) !== undefined;在不支持d标志的环境下,可以退回到手动计算的方案。整体匹配的起始位置用exec返回的index属性,结束位置用index加上匹配文本长度得到。至于捕获组的索引,则只能通过在捕获组前面加一层定位标记来间接推算,或者改用工具库处理。核心思路是保证功能可用,等环境升级后再切换到原生d标志,享受更直接更可靠的索引获取方式。总体来说,d标志填补了正则匹配中位置信息的空白,写解析类代码时值得优先考虑。