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)

對於沒有旗標 gyregexp,此函式只會尋找第一個配對,它與 str.match(regexp) 的運作方式完全相同。

…但如果存在旗標 g,則它會從儲存在 regexp.lastIndex 屬性的位置開始在 str 中執行搜尋。而且,如果它找到配對,則會將 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 是正確實作和良好效能的關鍵。

教學地圖

留言

留言前請先閱讀…
  • 如果您有改進建議,請 提交 GitHub 問題 或發起拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容,請說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,若要插入多行程式碼,請將它們包在 <pre> 標籤中,若要插入超過 10 行程式碼,請使用沙盒(plnkrjsbincodepen…)