2022 年 7 月 13 日

RegExp 和字串的方法

在本文中,我們將深入探討各種使用正規表示式的方法。

str.match(regexp)

方法 str.match(regexp) 在字串 str 中尋找與 regexp 相符的項目。

它有 3 種模式

  1. 如果 regexp 沒有旗標 g,則它會傳回第一個相符項目,並以陣列形式顯示,其中包含擷取群組和屬性 index(相符項目的位置)、input(輸入字串,等於 str

    let str = "I love JavaScript";
    
    let result = str.match(/Java(Script)/);
    
    alert( result[0] );     // JavaScript (full match)
    alert( result[1] );     // Script (first capturing group)
    alert( result.length ); // 2
    
    // Additional information:
    alert( result.index );  // 7 (match position)
    alert( result.input );  // I love JavaScript (source string)
  2. 如果 regexp 有旗標 g,它會傳回所有匹配項的陣列,為字串,不包含擷取群組和其他詳細資料。

    let str = "I love JavaScript";
    
    let result = str.match(/Java(Script)/g);
    
    alert( result[0] ); // JavaScript
    alert( result.length ); // 1
  3. 如果沒有匹配項,不論是否有旗標 g,會傳回 null

    這是個重要的細微差別。如果沒有匹配項,我們不會取得一個空陣列,而是 null。很容易忘記這點而犯錯,例如:

    let str = "I love JavaScript";
    
    let result = str.match(/HTML/);
    
    alert(result); // null
    alert(result.length); // Error: Cannot read property 'length' of null

    如果我們希望結果是一個陣列,我們可以這樣寫

    let result = str.match(regexp) || [];

str.matchAll(regexp)

最近新增的功能
這是最近新增到語言中的功能。舊瀏覽器可能需要 polyfill

方法 str.matchAll(regexp)str.match 的「較新、較好的」變體。

它主要用於搜尋所有匹配項和所有群組。

它與 match 有 3 個不同之處

  1. 它傳回一個包含匹配項的可迭代物件,而不是陣列。我們可以使用 Array.from 從中建立一個常規陣列。
  2. 每個匹配項都傳回一個包含擷取群組的陣列(格式與沒有旗標 gstr.match 相同)。
  3. 如果沒有結果,它會傳回一個空的迭代物件,而不是 null

使用範例

let str = '<h1>Hello, world!</h1>';
let regexp = /<(.*?)>/g;

let matchAll = str.matchAll(regexp);

alert(matchAll); // [object RegExp String Iterator], not array, but an iterable

matchAll = Array.from(matchAll); // array now

let firstMatch = matchAll[0];
alert( firstMatch[0] );  // <h1>
alert( firstMatch[1] );  // h1
alert( firstMatch.index );  // 0
alert( firstMatch.input );  // <h1>Hello, world!</h1>

如果我們使用 for..of 來迴圈 matchAll 匹配項,我們就不再需要 Array.from

str.split(regexp|substr, limit)

使用 regexp(或子字串)作為分隔符號來分割字串。

我們可以使用 split 來處理字串,如下所示

alert('12-34-56'.split('-')) // array of ['12', '34', '56']

但是我們也可以使用正規表示式來分割,方法相同

alert('12, 34, 56'.split(/,\s*/)) // array of ['12', '34', '56']

str.search(regexp)

方法 str.search(regexp) 傳回第一個匹配項的位置,如果找不到,則傳回 -1

let str = "A drop of ink may make a million think";

alert( str.search( /ink/i ) ); // 10 (first match position)

重要的限制:search 只會找到第一個匹配項。

如果我們需要後續匹配項的位置,我們應該使用其他方式,例如使用 str.matchAll(regexp) 來找出所有匹配項。

str.replace(str|regexp, str|func)

這是一個用於搜尋和取代的通用方法,是最有用的方法之一。搜尋和取代的瑞士刀。

我們可以在沒有正規表示式的情況下使用它來搜尋和取代子字串

// replace a dash by a colon
alert('12-34-56'.replace("-", ":")) // 12:34-56

不過有一個陷阱。

replace 的第一個引數是字串時,它只會取代第一個匹配項。

您可以在上面的範例中看到:只有第一個 "-"":" 取代。

要找出所有連字號,我們需要使用正規表示式 /-/g,而不是字串 "-",並加上必要的 g 旗標

// replace all dashes by a colon
alert( '12-34-56'.replace( /-/g, ":" ) )  // 12:34:56

第二個引數是一個取代字串。我們可以在其中使用特殊字元

符號 替換字串中的動作
$& 插入整個匹配
$` 插入匹配前的部分字串
$' 插入匹配後的部份字串
$n 如果n是一個 1-2 位數字,插入第 n 個擷取群組的內容,詳細資訊請參閱 擷取群組
$<name> 插入具有給定name的括號內容,詳細資訊請參閱 擷取群組
$$ 插入字元 $

例如

let str = "John Smith";

// swap first and last name
alert(str.replace(/(john) (smith)/i, '$2, $1')) // Smith, John

對於需要「智慧型」替換的情況,第二個引數可以是函式。

它將針對每個匹配呼叫,而傳回值將插入為替換。

函式以引數 func(match, p1, p2, ..., pn, offset, input, groups) 呼叫

  1. match – 匹配
  2. p1, p2, ..., pn – 擷取群組的內容(如果有的話)
  3. offset – 匹配的位置
  4. input – 來源字串
  5. groups – 具有命名群組的物件。

如果正規表示式中沒有括號,則只有 3 個引數:func(str, offset, input)

例如,讓我們將所有匹配轉換為大寫

let str = "html and css";

let result = str.replace(/html|css/gi, str => str.toUpperCase());

alert(result); // HTML and CSS

將每個匹配替換為其在字串中的位置

alert("Ho-Ho-ho".replace(/ho/gi, (match, offset) => offset)); // 0-3-6

在下面的範例中有兩個括號,因此替換函式以 5 個引數呼叫:第一個是完整匹配,然後是 2 個括號,然後是(範例中未使用)匹配位置和來源字串

let str = "John Smith";

let result = str.replace(/(\w+) (\w+)/, (match, name, surname) => `${surname}, ${name}`);

alert(result); // Smith, John

如果有很多群組,可以使用 rest 參數來存取它們

let str = "John Smith";

let result = str.replace(/(\w+) (\w+)/, (...match) => `${match[2]}, ${match[1]}`);

alert(result); // Smith, John

或者,如果我們使用命名群組,則具有它們的groups物件始終是最後一個,因此我們可以像這樣取得它

let str = "John Smith";

let result = str.replace(/(?<name>\w+) (?<surname>\w+)/, (...match) => {
  let groups = match.pop();

  return `${groups.surname}, ${groups.name}`;
});

alert(result); // Smith, John

使用函式可以提供我們最終的替換功能,因為它可以取得有關匹配的所有資訊,可以存取外部變數,並且可以執行所有操作。

str.replaceAll(str|regexp, str|func)

此方法基本上與 str.replace 相同,但有兩個主要差異

  1. 如果第一個引數是字串,它會替換字串的所有出現,而 replace 只會替換第一次出現
  2. 如果第一個參數是一個沒有 g 標記的正規表示式,會出現錯誤。加上 g 標記後,它的作用與 replace 相同。

replaceAll 的主要用途是替換字串中的所有出現。

就像這樣

// replace all dashes by a colon
alert('12-34-56'.replaceAll("-", ":")) // 12:34:56

regexp.exec(str)

regexp.exec(str) 方法會在字串 str 中傳回與 regexp 相符的項目。與前述方法不同,它是針對正規表示式呼叫,而不是針對字串。

它的行為會根據正規表示式是否有 g 標記而有所不同。

如果沒有 g,則 regexp.exec(str) 會傳回第一個相符項目,就像 str.match(regexp) 一樣。這種行為沒有帶來任何新功能。

但如果有 g 標記,則

  • 呼叫 regexp.exec(str) 會傳回第一個相符項目,並在 regexp.lastIndex 屬性中儲存其後面的位置。
  • 下一個這樣的呼叫會從位置 regexp.lastIndex 開始搜尋,傳回下一個相符項目,並在 regexp.lastIndex 中儲存其後面的位置。
  • …以此類推。
  • 如果沒有相符項目,regexp.exec 會傳回 null,並將 regexp.lastIndex 重設為 0

因此,重複呼叫會使用 regexp.lastIndex 屬性來追蹤目前的搜尋位置,一個接一個傳回所有相符項目。

過去,在 JavaScript 加入 str.matchAll 方法之前,會在迴圈中呼叫 regexp.exec 來取得所有包含群組的相符項目

let str = 'More about JavaScript at https://javascriptinfo.dev.org.tw';
let regexp = /javascript/ig;

let result;

while (result = regexp.exec(str)) {
  alert( `Found ${result[0]} at position ${result.index}` );
  // Found JavaScript at position 11, then
  // Found javascript at position 33
}

現在也能這樣做,不過對於較新的瀏覽器,str.matchAll 通常比較方便。

我們可以使用 regexp.exec 手動設定 lastIndex,從特定位置開始搜尋。

例如

let str = 'Hello, world!';

let regexp = /\w+/g; // without flag "g", lastIndex property is ignored
regexp.lastIndex = 5; // search from 5th position (from the comma)

alert( regexp.exec(str) ); // world

如果正規表示式有 y 標記,則搜尋會在位置 regexp.lastIndex 進行,不會再繼續。

讓我們在上面的範例中將 g 標記替換為 y。由於在位置 5 沒有任何字詞,因此不會有任何相符項目

let str = 'Hello, world!';

let regexp = /\w+/y;
regexp.lastIndex = 5; // search exactly at position 5

alert( regexp.exec(str) ); // null

當我們需要使用正規表示式在特定位置「讀取」字串中的內容,而不是在其他位置時,這會很方便。

regexp.test(str)

方法 regexp.test(str) 尋找比對,並回傳 true/false 表示是否存在。

例如

let str = "I love JavaScript";

// these two tests do the same
alert( /love/i.test(str) ); // true
alert( str.search(/love/i) != -1 ); // true

一個有負面答案的範例

let str = "Bla-bla-bla";

alert( /love/i.test(str) ); // false
alert( str.search(/love/i) != -1 ); // false

如果 regexp 有旗標 g,則 regexp.test 會從 regexp.lastIndex 屬性開始尋找,並更新此屬性,就像 regexp.exec 一樣。

因此我們可以使用它從特定位置開始搜尋

let regexp = /love/gi;

let str = "I love JavaScript";

// start the search from position 10:
regexp.lastIndex = 10;
alert( regexp.test(str) ); // false (no match)
在不同來源上重複測試相同的全域 regexp 可能會失敗

如果我們將相同的全域 regexp 套用在不同的輸入上,可能會導致錯誤的結果,因為 regexp.test 的呼叫會推進 regexp.lastIndex 屬性,因此在另一個字串中的搜尋可能會從非零位置開始。

例如,我們在同一段文字上呼叫 regexp.test 兩次,第二次會失敗

let regexp = /javascript/g;  // (regexp just created: regexp.lastIndex=0)

alert( regexp.test("javascript") ); // true (regexp.lastIndex=10 now)
alert( regexp.test("javascript") ); // false

這正是因為在第二次測試中 regexp.lastIndex 為非零。

為了解決這個問題,我們可以在每次搜尋之前設定 regexp.lastIndex = 0。或者,不要在 regexp 上呼叫方法,而是使用字串方法 str.match/search/...,它們不會使用 lastIndex

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改善建議,請 提交 GitHub 議題 或發起 pull request,而不是留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,對於多行,請將它們包在 <pre> 標籤中,對於超過 10 行,請使用沙盒 (plnkrjsbincodepen…)