正则手记——方法篇

方法篇,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 的括号内的内容,详见 捕获组
$$ 插入字符 $

例如:

$&代表匹配结果。

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

参考链接

  • https://zh.javascript.info/regexp-methods
  • https://github.com/veedrin/horseshoe “马蹄铁”专题学习计划
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp
© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容