正則手記——方法篇

RainBow發表於2022-11-23

方法篇,JavaScript 中都有哪些操作正則的方法。

RegExp 物件方法

方法描述
exec檢索字串中指定的值。返回找到的值,並確定其位置。
test檢索字串中指定的值。返回 true 或 false。正則.test(字串)

regexp.test(str)

方法 regexp.test(str) 查詢匹配項,然後返回 true/false 表示是否存在。

let str = "I love JavaScript";

// 這兩個測試相同
console.log(/love/i.test(str)); // true
console.log(str.search(/love/i) != -1); // true

regexp.exec(str)

regexp.exec(str) 方法返回字串 str 中的 regexp 匹配項, 可指定從位置進行搜尋。

基於否具有修飾符 g 其有兩種搜尋模式。

(1) 沒有修飾符 g,則 regexp.exec(str) 會返回與 第一個匹配項,就像 str.match(regexp) 那樣。這種行為並沒有帶來任何新的東西。

let str = "More about JavaScript at https://javascript.info";
let regexp = /javascript/i;
console.log(regexp.exec(str));
/* [
0 : "JavaScript"
groups:undefined
index: 11
input :"More about JavaScript at https://javascript.info"
] */

(2) 有修飾符 g,可以基於 regexp.lastIndex 位置迴圈搜尋全部。

詳細步驟:

  • 呼叫 regexp.exec(str) 會返回第一個匹配項,並將緊隨其後的位置儲存在屬性 regexp.lastIndex 中。
  • 下一次這樣的呼叫會從位置 regexp.lastIndex 開始搜尋,返回下一個匹配項,並將其後的位置儲存在 regexp.lastIndex 中。
    ……以此類推。
  • 如果沒有匹配項,則 regexp.exec 返回 null,並將 regexp.lastIndex 重置為 0。
  • 因此,重複呼叫會一個接一個地返回所有匹配項,使用屬性 regexp.lastIndex 來跟蹤當前搜尋位置。

過去,在將 str.matchAll 方法新增到 JavaScript 之前,會在迴圈中呼叫 regexp.exec 來獲取組的所有匹配項:

let str = "More about JavaScript at https://javascript.info";
let regexp = /javascript/gi;

let result;

while ((result = regexp.exec(str))) {
  console.log(`Found ${result[0]} at position ${result.index}`);
  // 在位置 11 找到了 JavaScript,然後
  // 在位置 33 找到了 javascript
}

這現在也有效,儘管對於較新的瀏覽器 str.matchAll 通常更方便。

指定位置搜尋

我們可以透過手動設定 lastIndex,用 regexp.exec 從給定位置進行搜尋。

例如:

let str = "Hello, world!";

let regexp = /\w+/g; // 沒有修飾符 "g",lastIndex 屬性會被忽略
regexp.lastIndex = 5; // 從第 5 個位置搜尋(從逗號開始)

console.log(regexp.exec(str)); // world

如果正規表示式帶有修飾符 y,則搜尋將精確地在 regexp.lastIndex 位置執行,不會再進一步。

讓我們將上面示例中的 g 修飾符替換為 y。現在沒有找到匹配項,因為在位置 5 處沒有單詞:

let str = "Hello, world!";

let regexp = /\w+/y;
regexp.lastIndex = 5; // 在位置 5 精確查詢

console.log(regexp.exec(str)); // null

當我們需要透過正規表示式在確切位置而不是其後的某處從字串中“讀取”某些內容時,這很方便。

String 物件的方法

str.match(regexp)

str.match(regexp) 方法在字串 str 中查詢 regexp 的匹配項。搜尋成功就返回內容,格式為陣列,失敗就返回 null。

它有三種模式:

  1. 非全域性匹配, 不帶 g,返回第一個匹配項,其中包括捕獲組和屬性 index
let str = "2022/10/24";
let result = str.match(/(\d{4})\/(\d{2})\/(\d{2})/);

// 第一份是完全匹配的
console.log(result[0]); // 2022/10/24(完全匹配)
// 捕獲組的結果從第二項開始展示,如果沒有捕獲內容則顯示 undefined
console.log(result[1]); // "2022"(第一個分組)
console.log(result[2]); // "10"(第一個分組)
console.log(result[3]); // "24"(第一個分組)
console.log(result.length); // 4

// 其他屬性:
console.log(result.index); // 0(匹配位置)
console.log(result.input); // '2022/10/24'(源字串)

// groups 屬性它儲存的不是捕獲組的資訊,而是捕獲命名的資訊(自定義捕獲組名時生效)。
console.log(result.groups); // undefined

// 非捕獲組不會捕獲
console.log("2022/10/24".match(/(?:\d{4})\/(\d{2})\/(\d{2})/)); // ['2022/10/24', '10', '24', index: 0, input: '2022/10/24', groups: undefined]
  1. 全域性匹配 帶有 g,則它將返回一個包含所有匹配項的陣列,但不包含捕獲組和其它詳細資訊
"2022/10/24".match(/(?:\d{4})\/(\d{2})\/(\d{2})/g); // ['2022/10/24']

"2022/10/24".match(/(\d{4})\/(\d{2})\/(\d{2})/g); // ['2022/10/24']
  1. 沒有匹配項, 則無論是否帶有修飾符 g,都將返回 null。
"2022/10/24".match(/(\d{6})/); // null
let result = "2022/10/24".match(/(\d{6})/) || []; // 希望結果是陣列
console.log(result); // []

str.matchAll(regexp)

注意: 這是一個最近新增到 JavaScript 的特性。 舊式瀏覽器可能需要 polyfills.

方法 str.matchAll(regexp) 是 str.match 的“更新、改進”的變體。

它主要用來搜尋所有組的所有匹配項。

與 match 相比有 3 個區別:

  1. 它返回一個包含匹配項的可迭代物件,而不是陣列。我們可以用 Array.from 將其轉換為一個常規陣列。
  2. 每個匹配項均以一個包含捕獲組的陣列形式返回(返回格式與不帶修飾符 g 的 str.match 相同)。
  3. 如果沒有結果,則返回的是一個空的可迭代物件而不是 null。
let matchAll = "2022/10/24".matchAll(/(\d{4})\/(\d{2})\/(\d{2})/g);

console.log(matchAll); // [object RegExp String Iterator],不是陣列,而是一個可迭代物件

matchAll = Array.from(matchAll); // 現在是陣列了 // [0: ['2022/10/24', '2022', '10', '24', index: 0, input: '2022/10/24', groups: undefined]]

let firstMatch = matchAll[0];

console.log(firstMatch[0]); // '2022/10/24'
console.log(firstMatch[1]); // 2022
console.log(firstMatch[2]); // 10
console.log(firstMatch[3]); // 24
console.log(firstMatch.index); // 0
console.log(firstMatch.input); // 2022/10/24

如果我們用 for..of 來遍歷 matchAll 的匹配項,那麼我們就不需要 Array.from 了。

str.split(regexp|substr, limit)

使用正規表示式(或子字串)作為分隔符來分割字串。

我們可以用 split 來分割字串,像這樣:

const [y, m, d] = "2022/10/24".split("/");
console.log([m, d, y].join("/")); // 10/24/2022

但同樣,我們也可以用正規表示式:

console.log("2022, 10, 24".split(/,\s*/)); // ['2022', '10', '24']

另外,因為 split 方法中的正則是用來匹配分隔符,所以全域性匹配沒有意義。

str.search(regexp)

方法 str.search(regexp) 返回第一個匹配項的位置索引,如果沒找到,則返回 -1:

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

console.log(str.search(/ink/i)); // 10(第一個匹配位置)

重要限制:search 僅查詢第一個匹配項

如果我們需要其他匹配項的位置,則應使用其他方法,例如用 str.matchAll(regexp) 查詢所有位置。

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

這是用於搜尋和替換的通用方法,是最有用的方法之一。它是搜尋和替換字串的瑞士軍刀。

我們可以在不使用正規表示式的情況下使用它來搜尋和替換子字串。

當 replace 的第一個引數是字串時,它只替換第一個匹配項。

在下面的示例中看到:只有第一個 "/" 被替換為了 "-"。

// 用-替換/字元
console.log("2022/10/24".replace("/", "-")); // 2022-10/24
// 類似於非全域性模式的正則匹配
console.log("2022/10/24".replace(/\//, "-")); // 2022-10/24

如要找到所有的連字元,我們不應該用字串 "/",而應使用帶 g 修飾符的正規表示式 /\//g

// 將所有用-替換/字元
console.log("2022/10/24".replace(/\//g, "-")); // 2022-10-24

第二個引數是替換字串。我們可以在其中使用特殊字元, 在實際替換時 replace 內部邏輯會自動解析字串,提取出變數。

符號替換字串中的行為
$&插入整個匹配項
$`插入字串中匹配項之前的字串部分
$'插入字串中匹配項之後的字串部分
$n如果 n 是一個 1-2 位的數字,則插入第 n 個分組的內容,詳見 捕獲組
$<name>插入帶有給定 name 的括號內的內容,詳見 捕獲組
$$插入字元 $

例如:

$&代表匹配結果。

console.log(
  "2022/10/24".replace(/(\d{4}\/\d{2}\/\d{2})/g, "今天是$&") // 今天是2022/10/24
);

$`代表匹配結果左邊的文字。

console.log(
  "今天是2022/10/24".replace(/(\d{4}\/\d{2}\/\d{2})/g, "$`") // 今天是今天是
);

$n 代表按序號 n 獲取對應捕獲組的文字。

// 替換字串
console.log(
  "2022/10/24".replace(/(\d{4})\/(\d{2})\/(\d{2})/g, "$2/$3/$1") // 10/24/2022
);

// 命名捕獲組
console.log(
  "2022/10/24".replace(
    /(?<year>\d{4})\/(?<month>\d{2})\/(?<day>\d{2})/g,
    "$<month>/$<day>/$<year>"
  )
); // 10/24/2022

$<name> 代表按 name 為捕獲組命名:

// 命名捕獲組
console.log(
  "2022/10/24".replace(
    /(?<year>\d{4})\/(?<month>\d{2})\/(?<day>\d{2})/g,
    "$<month>/$<day>/$<year>"
  )
); // 10/24/2022

對於需要“智慧”替換的場景,第二個引數可以是一個函式。

每次匹配都會呼叫這個函式,並且返回的值將作為替換字串插入。

該函式 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 result = "2022/10/24".replace(
  /(\d{4})\/(\d{2})\/(\d{2})/g,
  (_, y, m, d) => {
    return [m, d, y].join("/");
  }
);
console.log(result); // 10/24/2022

如果有許多組,用 rest 引數(…)可以很方便的訪問:

let result = "2022/10/24".replace(/(\d{4})\/(\d{2})\/(\d{2})/g, (...match) => {
  return `${match[2]}/${match[3]}/${match[1]}`;
});
console.log(result); // 10/24/2022

或者,如果我們使用的是命名組,則帶有它們的 groups 物件始終是最後一個物件,所以我們可以像這樣獲取它:

let result = "2022/10/24".replace(
  /(?<year>\d{4})\/(?<month>\d{2})\/(?<day>\d{2})/g,
  (...match) => {
    let groups = match.pop();
    return `${groups.month}/${groups.day}/${groups.year}`;
  }
);

console.log(result); // 10/24/2022

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

這個方法與 str.replace 本質上是一樣的,但有兩個主要的區別:

如果第一個引數是一個字串,它會替換 所有出現的 和第一個引數相同的字串 ​,​ 而 replace 只會替換 第一個。
如果第一個引數是一個沒有修飾符 g 的正規表示式,則會報錯。帶有修飾符 g,它的工作方式與 replace 相同。
replaceAll 的主要用途是替換所有出現的字串。

像這樣:

// 使用冒號替換所有破折號
console.log("2022/10/24".replaceAll("/", "-")); // 2022-10-24

RegExp.$1-$9

非標準: 該特性是非標準的,請儘量不要在生產環境中使用它!

非標準$1, $2, $3, $4, $5, $6, $7, $8, $9 屬性是包含括號子串匹配的正規表示式的靜態和只讀屬性。

在指令碼中使用如 replacetestmatch 等方法,如果正則中含有捕獲組,訪問 RegExp 物件的非標準屬性 $1$2 能提取捕獲組裡面的內容。

let str = "I love JavaScript";

console.log(/(love)/i.test(str));

// RegExp.$1 love

參考連結

相關文章