2020年12月10日

粘性标记 "y",在指定位置搜索

标记 y 允许在源字符串的指定位置执行搜索。

为了理解 y 标记的用例,并更好地理解正则表达式的使用方法,让我们探索一个实际的例子。

正则表达式的一个常见任务是“词法分析”:我们得到一段文本,例如编程语言中的文本,需要找到它的结构元素。例如,HTML 有标签和属性,JavaScript 代码有函数、变量等等。

编写词法分析器是一个特殊的领域,有自己的工具和算法,所以我们不会深入探讨,但有一个常见的任务:在指定位置读取内容。

例如,我们有一个代码字符串 let varName = "value",我们需要从中读取变量名,该变量名从位置 4 开始。

我们将使用正则表达式 \w+ 来查找变量名。实际上,JavaScript 变量名需要更复杂的正则表达式才能进行精确匹配,但这里并不重要。

  • 调用 str.match(/\w+/) 将只找到行中的第一个单词 (let)。这不是我们想要的。
  • 我们可以添加标记 g。但是,然后调用 str.match(/\w+/g) 将查找文本中的所有单词,而我们只需要位置 4 处的单词。同样,这不是我们想要的。

那么,如何精确地在给定位置搜索正则表达式呢?

让我们尝试使用 `regexp.exec(str)` 方法。

对于没有 `g` 和 `y` 标志的 `regexp`,此方法只查找第一个匹配项,它与 `str.match(regexp)` 的工作方式完全相同。

…但是,如果存在 `g` 标志,那么它将在 `str` 中执行搜索,从 `regexp.lastIndex` 属性中存储的位置开始。并且,如果它找到匹配项,则将 `regexp.lastIndex` 设置为匹配项后的索引。

换句话说,`regexp.lastIndex` 作为搜索的起点,每次 `regexp.exec(str)` 调用都会重置为新值(“上次匹配后”)。当然,只有在存在 `g` 标志的情况下才会这样。

因此,对 `regexp.exec(str)` 的连续调用会一个接一个地返回匹配项。

以下是一个此类调用的示例

let str = 'let varName'; // Let's find all words in this string
let regexp = /\w+/g;

alert(regexp.lastIndex); // 0 (initially lastIndex=0)

let word1 = regexp.exec(str);
alert(word1[0]); // let (1st word)
alert(regexp.lastIndex); // 3 (position after the match)

let word2 = regexp.exec(str);
alert(word2[0]); // varName (2nd word)
alert(regexp.lastIndex); // 11 (position after the match)

let word3 = regexp.exec(str);
alert(word3); // null (no more matches)
alert(regexp.lastIndex); // 0 (resets at search end)

我们可以在循环中获取所有匹配项

let str = 'let varName';
let regexp = /\w+/g;

let result;

while (result = regexp.exec(str)) {
  alert( `Found ${result[0]} at position ${result.index}` );
  // Found let at position 0, then
  // Found varName at position 4
}

这种 `regexp.exec` 的用法是 `str.matchAll` 方法的替代方法,对过程有更多控制。

让我们回到我们的任务。

我们可以手动将 `lastIndex` 设置为 `4`,以从给定位置开始搜索!

像这样

let str = 'let varName = "value"';

let regexp = /\w+/g; // without flag "g", property lastIndex is ignored

regexp.lastIndex = 4;

let word = regexp.exec(str);
alert(word); // varName

万岁!问题解决!

我们执行了 `\w+` 的搜索,从 `regexp.lastIndex = 4` 的位置开始。

结果是正确的。

…但是等等,别高兴得太早。

请注意:`regexp.exec` 调用从 `lastIndex` 位置开始搜索,然后继续。如果 `lastIndex` 位置没有单词,但它在之后的位置,那么它将被找到

let str = 'let varName = "value"';

let regexp = /\w+/g;

// start the search from position 3
regexp.lastIndex = 3;

let word = regexp.exec(str);
// found the match at position 4
alert(word[0]); // varName
alert(word.index); // 4

对于某些任务,包括词法分析,这完全是错误的。我们需要在文本中找到与给定位置完全匹配的匹配项,而不是在它之后的位置。这就是 `y` 标志的作用。

`y` 标志使 `regexp.exec` 在 `lastIndex` 位置精确搜索,而不是“从”它开始搜索。

以下是用 `y` 标志进行的相同搜索

let str = 'let varName = "value"';

let regexp = /\w+/y;

regexp.lastIndex = 3;
alert( regexp.exec(str) ); // null (there's a space at position 3, not a word)

regexp.lastIndex = 4;
alert( regexp.exec(str) ); // varName (word at position 4)

正如我们所见,`/\w+/y` 正则表达式在 `3` 位置不匹配(与 `g` 标志不同),但在 `4` 位置匹配。

这不仅是我们需要的,而且在使用 `y` 标志时,性能会得到显著提升。

想象一下,我们有一个很长的文本,而且它根本没有匹配项。然后,使用 `g` 标志进行搜索将一直进行到文本的末尾,什么也找不到,这将比使用 `y` 标志进行搜索花费更多时间,因为 `y` 标志只检查确切的位置。

在词法分析等任务中,通常需要在特定位置进行多次搜索,以检查该位置的内容。使用标志 y 是实现正确且高效的代码的关键。

教程地图

评论

在评论之前请阅读…
  • 如果您有改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入少量代码,请使用<code> 标签,对于多行代码,请使用<pre> 标签,对于超过 10 行的代码,请使用沙箱(plnkrjsbincodepen…)