正则表达式

正则表达式(Regular Expression),在代码中常简写为 regex、regexp或RE。使用单个字符串来描述、匹配一系列符合某个句法规则的字符串搜索模式。

搜索是可用于文本搜索和文本替换。

语法:

/正则表达式主体/修饰符(可选)

在 javascript 中, 正则表达式通常用于两个字符串方法:search()replace()

search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置。

replace()方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。

  • search() 方法的使用
const str = 'hello world'
// 正则表达式
console.log(str.search(/ello/)) // 1
console.log(str.search(/lls/)) // -1
// 字符串:字符串参数会转换为正则表达式
console.log(str.search('ello')) // 1
console.log(str.search('lls')) // -1
  • replace() 方法的使用
const str = 'hello world'
// 正则表达式
console.log(str.replace(/ello/, 'ey')) // hey world
console.log(str.replace(/elll/, 'ey')) // hello world
// 字符串:字符串参数会转换为正则表达式
console.log(str.replace('ello', 'ey')) // hey world
console.log(str.replace('elll', 'ey')) // hello world

正则表达式修饰符

修饰符可以在全局搜索中不区分大小写

修饰符

描述

i

执行对大小写不敏感的匹配

g

执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)

m

执行多行匹配

const str = 'hElLo world'
console.log(str.replace(/ello/, 'ey')) // hElLo world
console.log(str.replace(/ello/i, 'ey')) // hey world

const str2 = 'hello world\nRegExp'
console.log(str2.replace(/^Reg/, 'reg')) // hello world\nRegExp 
console.log(str2.replace(/^Reg/m, 'reg')) // hello world\nregExp

const str3 = 'hello world hello RegExp'
console.log(str3.replace(/ello/, 'ey')) // hey world hello RegExp
console.log(str3.replace(/ello/g, 'ey')) // hey world hey RegExp

正则表达式模式

方括号

方括号用于查找某个范围内的字符

表达式

描述

[abc]

查找方括号之间的任何字符

[^abc]

查找任何不在方括号之间的字符

[0-9]

查找任何从0至9的数字

[a-z]

查找任何从小写a到小写z的字符

[A-Z]

查找任何从大写A到大写Z的字符

[A-z]

查找任何从大写A到小写z的字符

元字符

元字符是拥有特殊含义的字符

元字符

描述

.

查找单个字符,除了换行和行结束符

\w

查找单词字符 [a-zA-Z_0-9]

\W

查找非单词字符

\d

查找数字

\D

查找非数字字符

\s

查找空白字符

\S

查找非空白字符

\b

匹配单词边界

\B

匹配非单词边界

\0

查找NULL字符

\n

查找换行符

\f

查找换页符

\r

查找回车符

\t

查找制表符

\v

查找垂直制表符

\xxx

查找以八进制数xxx规定的字符

\xdd

查找以十六进制数 dd 规定的字符

\uxxxx

查找以十六进制数 xxxx 规定的 Unicode 字符

量词

量词

描述

n+

匹配任何包含至少一个 n 的字符串

n*

匹配任何包含零个或多个 n 的字符串

n?

匹配任何包含零个或一个 n 的字符串

n{X}

匹配包含 X 个 n 的序列的字符串

n{X, Y}

匹配包含 X 至 Y 个 n 的序列的字符串

n{X, }

匹配包含至少 X 个 n 的序列的字符串

n$

匹配任何结尾为 n 的字符串

^n

匹配任何开头为 n 的字符串

?=n

匹配任何其后紧接指定字符串 n 的字符串

?!n

匹配任何其后没有紧接指定字符串 n 的字符串

RegExp 对象方法

方法

描述

compile

编译正则表达式

exec

检索字符串中指定的值,返回找到的值,并确定其位置

test

检索字符串中指定的值,返回 true 或 false

const str = 'hello world hello RegExp'
console.log(/ello/.compile(/ello/)) // /ello/
console.log(/ello/.exec(str)) // [ 'ello', index: 1, input: 'hello world hello RegExp', groups: undefined ]
console.log(/ello/.test(str)) // true

test方法的坑

test 方法用于测试字符串参数中是否存在匹配正则表达式模式的字符串。

lastIndex:是当前表达式匹配内容的最后一个字符的后一位,用于规定下一次匹配的起始位置。

当正则表达式使用了全局匹配时,test方法会出现奇怪现象:

let reg = /\w/g

console.log(reg.test('ab')) // true
console.log(reg.test('ab')) // true
console.log(reg.test('ab')) // false
console.log(reg.test('ab')) // true

正常情况,ab符合 test方法,都应该返回 true才对,原因就在于 lastIndex 属性。

我们可以试试每次都运行 test 方法打印出 lastIndex的值:

let reg = /\w/g
console.log(reg.lastIndex) // 0

console.log(reg.test('ab')) // true
console.log(reg.lastIndex) // 1
console.log(reg.test('ab')) // true
console.log(reg.lastIndex) // 2
console.log(reg.test('ab')) // false
console.log(reg.lastIndex) // 0
console.log(reg.test('ab')) // true
console.log(reg.lastIndex) // 1

lastIndex属性是当前表达式匹配内容的最后一个字符的后一位,用于规定下一次匹配的起始位置。

当进入正则表达式全局模式时,每次使用 test 方法都会从 lastIndex 开始,匹配从 lastIndex 开始的子字符串。比如例子中,第二次执行 test 方法时,此时,lastIndex 已经变为2,子字符串为空,所以 reg 不可能匹配上它,由于子字符串匹配失败,test 方法返回 false,并将 lastIndex 属性置为0,重新开始一轮循环。

避免 test 中的坑的方法

  • test方法本身就是用来测试是否存在匹配正则的字符串,不使用全局模式一样可以实现目的,所以第一种方法就是不适用全局模式;
  • 不将正则对象实例存在变量中,每次直接用正则对象实例调用 test方法,不过这种方法对内存有所损耗,理论上不建议。

支持正则表达式的 String 对象的方法

方法

描述

search

检索与正则表达式相匹配的值

match

找到一个或多个正则表达式的匹配

replace

替换与正则表达式匹配的子串

split

把字符串分割为字符串数组

const str = 'hello world hello RegExp'
console.log(str.search(/ello/)) // 1
console.log(str.search(/Ello/)) // -1

console.log(str.match(/ello/)) // [ 'ello', index: 1, input: 'hello world hello RegExp', groups: undefined ]

console.log(str.replace(/ello/, 'ey')) // hey world hello RegExp
console.log(str.replace(/Ello/, 'ey')) // hello world hello RegExp

console.log(str.split(/ello/)) // [ 'h', ' world h', ' RegExp' ]
console.log(str.split(/ello/, '1')) // [ 'h' ]
console.log(str.split(/ello/, 2)) // [ 'h', ' world h' ]
console.log(str.split(/ello/, 3)) // [ 'h', ' world h', ' RegExp' ]

search 方法

String.prototype.search(reg)

search方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置。方法返回第一个匹配结果的 index。查找不到则返回 -1

  • search方法不执行全局匹配,它将忽略修饰符 g,并且总是从字符串的开始进行检索,因此,它不会产生类似于 test 方法的问题。
  • 不输入正则表达式则 search方法将会自动将其转为正则表达式。

match 方法

String.prototype.match(reg)

match 方法将检索字符串,以找到一个或多个与 reg 匹配的文本,reg是否具有修饰符 g对结果影响很大。

非全局调用

如果 reg 没有修饰符 g,那 match 方法就只能在字符串中执行一次匹配,如果没有找到任何匹配的文本,将返回 null。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。

返回的数组的第一个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。

除了常规的数组元素外,返回的数组还含有2个对象属性:

  • index:声明匹配文本的起始字符在字符串的位置
  • input:声明对 stringObject的引用
let str = 'af12131ds22'
console.log(str.match('v')) // null
console.log(str.match('s')) // [ 's', index: 8, input: 'af12131ds22', groups: undefined ]

全局调用:

如果 reg具有修饰符 g,则 match 方法将执行全局检索,找到字符串中的所有匹配子字符串。如果没有找到任何匹配的子串,将返回 null。否则,返回一个数组。数组元素中存放的是字符串中所有匹配到的子串,而且也没有 index 属性或 input 属性。

let str = 'af12131ds22'
console.log(str.match(/[^\w]/g)) // null
console.log(str.match(/\d+/g)) // [ '12131', '22' ]

split 方法

String.prototype.split(reg)

把字符串分割为字符数组

let str = 'af12131ds22'
console.log(str.split('')) // [ 'a', 'f', '1', '2', '1', '3', '1', 'd', 's', '2', '2' ]
console.log(str.split('1')) // [ 'af', '2', '3', 'ds22' ]

一些复杂情况可以使用正则表达式解决

let str = 'af12131ds22'
console.log(str.split(/\d/)) // [ 'af', '', '', '',   '', 'ds', '',   '' ]
console.log(str.split(/\d+/)) // [ 'af', 'ds', '' ]

replace 方法

String.prototype.replace()

replace 方法有三种形态:

  • String.prototype.replace(str, replaceStr)
  • String.prototype.replace(reg, replaceStr)
  • String.prototype.replace(reg, function)
let str = 'af12131ds22'
console.log(str.replace('1', 7)) // af72131ds22
console.log(str.replace(/1/g, 7)) // af72737ds22

str.replace(/1/g, function (...args) {
  console.log(args)
})
/**
 * [ '1', 2, 'af12131ds22' ]
 * [ '1', 4, 'af12131ds22' ]
 * [ '1', 6, 'af12131ds22' ]
 */

replace 接受函数参数时,有四个参数:

  • 匹配字符串
  • 正则表达式分组内容,没有分组则没有该参数
  • 匹配项在字符串中的 index
  • 原字符串
// 有分组内容的
let str = 'af12131ds22'
str.replace(/(\d)(\w)/g, function (...args) {
  console.log(args)
})
/**
 * [match, group1, group2, index, origin]
[ '12', '1', '2', 2, 'af12131ds22' ]
[ '13', '1', '3', 4, 'af12131ds22' ]
[ '1d', '1', 'd', 6, 'af12131ds22' ]
[ '22', '2', '2', 9, 'af12131ds22' ]
 */

let str = 'af12131ds22'
str.replace(/(\d)(\w)(\d)/g, function (...args) {
  console.log(args)
})
/**
 * [match, group1, group2, group3, index, origin]
[ '121', '1', '2', '1', 2, 'af12131ds22' ]
 */

后行断言

JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead)。不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。ES2018 引入后行断言

量词

描述

?=n

匹配任何其后紧接指定字符串 n 的字符串

?!n

匹配任何其后没有紧接指定字符串 n 的字符串

先行断言”指的是,x 只有在 y 前面才匹配,必须写成 /x(?=y)/。比如,只匹配百分号之前的数字,要写成 /\d+(?=%)/

先行否定断言”指的是,x 只有不在 y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成 /\d+(?!%)/

let lookbehead = /\d+(?=%)/.exec('100% of US presidents have been male')
console.log(lookbehead)
let negativeLookbehead = /\d+(?!%)/.exec('that is all 44 of them')
console.log(negativeLookbehead)
/**
 * [
    '100',
    index: 0,
    input: '100% of US presidents have been male',
    groups: undefined
  ]
  [ '44', index: 12, input: 'that is all 44 of them', groups: undefined ]
 */

上面两个字符串,如果互换正则表达式,就不会得到相同的结果。另外,还可以看到,“先行断言”括号之中的部分((?=%)),是不计入返回结果的。

后行断言”正好与“先行断言”相反,x只有在 y 后面才匹配,必须写成 /(?<=y)x/。比如,只匹配美元符号后面的数字,要写成/(?<=\$)\d+/

后行否定断言”则与“先行否定断言”相反,x 只有不在y后面才匹配,必须写成 /(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成 /(?<!\$)\d+/

let lookbehind = /(?<=\$)\d+/.exec('$100 of US presidents have been male')
console.log(lookbehind)
let negativeLookbehind = /(?<!\$)\d+/.exec('that is all &44 of them')
console.log(negativeLookbehind)
/**
 * [
      '100',
      index: 1,
      input: '$100 of US presidents have been male',
      groups: undefined
    ]
    [
      '44',
      index: 13,
      input: 'that is all &44 of them',
      groups: undefined
    ]
 */

上面例子中,“后行断言”的括号之中的部分((?<=\$)),也是不计入返回结果。

下面的例子是使用后行断言进行字符串替换。

let reg = /(?<=\$)foo/g
console.log('$foo %foo foo'.replace(reg, 'bar')) // $bar %foo foo

上面代码中,只有在美元符号后面的 foo才会被替换。

“后行断言”的实现,需要先匹配 /(?<=y)x/x,然后再回到左边,匹配 y 的部分。这种“先右后左”的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为。

具名组匹配

正则表达式使用圆括号进行组匹配

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/

上面代码中,正则表达式里面有三组圆括号。使用 exec 方法,就可以将这三组匹配结果提取出来。

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/
const matchObj = RE_DATE.exec('1997-11-26')
console.log(matchObj)
// [ '1997-11-26', '1997', '11', '26', index: 0, input: '1997-11-26', groups: undefined ]
const year = matchObj[1] // 1997
const month = matchObj[2] // 11
const day = matchObj[3] // 26

组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如 matchObj[1])引用,要是组的顺序变了,引用的时候就必须修改序号。

ES2018 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/

const matchObj = RE_DATE.exec('1997-11-26')
console.log(matchObj)
// [ '1997-11-26', '1997', '11', '26', index: 0, input: '1997-11-26', groups: [Object: null prototype] { year: '1997', month: '11', day: '26' } ]

const year = matchObj.groups.year // 1997
const month = matchObj.groups.month // 11
const day = matchObj.groups.day // 26

上面代码中,“具名组匹配”在圆括号内部,模式的头部添加“问号+尖括号+组名”(?<year>),然后就可以在 exec 方法返回结果的 groups 属性上引用该组名。同时,数字序号 (matchObj[1]) 依然有效。

具名组匹配等于为每一组匹配加上了ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。

如果具名组没有匹配,那么对应的 groups 对象属性会是 undefined

const reg = /^(?<as>a+)?$/
const matchObj = reg.exec('')
console.log(matchObj)
console.log(matchObj.groups.as) // undefined
console.log('as' in matchObj.groups) // true
/**
 * [
  '',
  undefined,
  index: 0,
  input: '',
  groups: [Object: null prototype] { as: undefined }
]
 */

上面代码中,具名组 as 没有找到匹配,那么 matchObj.groups.as 属性值就是 undefined,并且 as 这个键名在 groups 是始终存在的。

解构赋值和替换

有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。

let { groups: { one, two } } = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar')
console.log(one, two) // foo bar

字符串替换时,使用 $<组名>引用具名组。

let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
console.log('2006-06-07'.replace(reg, '$<day>/$<month>/$<year>')) // 07/06/2006

上面代码中,replace 方法的第二个参数是一个字符串,而不是正则表达式。

replace 方法的第二个参数也可以是函数,该函数的参数序列如下。

let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
let str = '2015-01-02'.replace(reg, (
   matched, // 整个匹配结果 2015-01-02
   capture1, // 第一个组匹配 2015
   capture2, // 第二个组匹配 01
   capture3, // 第三个组匹配 02
   position, // 匹配开始的位置 0
   S, // 原字符串 2015-01-02
   groups // 具名组构成的一个对象 {year, month, day}
 ) => {
  let { day, month, year } = groups
  return `${day}/${month}/${year}`
})
console.log(str) // 02/01/2015

具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。

let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u

let str1 = '2015-01-02'.replace(reg, (...args) => {
  console.log(args)
  let { day, month, year } = args[6]
  return `${day}/${month}/${year}`
})
console.log(str1)
/**
 * [
  '2015-01-02',
  '2015',
  '01',
  '02',
  0,
  '2015-01-02',
  [Object: null prototype] { year: '2015', month: '01', day: '02' }
]
 */

引用

如果要在正则表达式内部引用某个“具名组匹配”,可以使用 \k<组名>的写法。

const reg = /^(?<word>[a-z]+)!\k<word>$/
console.log(reg.test('abc!abc')) // true
console.log(reg.test('abc!ab')) // false

数字引用(\1)依然有效。

const reg = /^(?<word>[a-z]+)!\1$/
console.log(reg.test('abc!abc')) // true
console.log(reg.test('abc!ab')) // false

这两种引用方法还可以同时使用。

const reg = /^(?<word>[a-z]+)!\k<word>!\1$/
console.log(reg.test('abc!abc!abc')) // true
console.log(reg.test('abc!ab!ab')) // false

String.prototype.matchAll()

如果一个正则表达式在字符串里面有多个匹配,现在一般使用 g 修饰符或 y 修饰符,在循环里面逐一取出。

let regex = /t(e)(st(\d?))/g
let string = 'test1test2test3'

let matches = []
let match
while (match = regex.exec(string)) {
  matches.push(match)
}

console.log(matches)
// [
//   ["test1", "e", "st1", "1", index: 0, input: "test1test2test3", groups: undefined],
//   ["test2", "e", "st2", "2", index: 5, input: "test1test2test3", groups: undefined],
//   ["test3", "e", "st3", "3", index: 10, input: "test1test2test3", groups: undefined]
// ]

console.log(reg.exec(str))
// [ 'test1', 'e', 'st1', '1', index: 0, input: 'test1test2test3', groups: undefined ]

上面代码中,while 循环取出每一轮的正则匹配,一共三轮。

ES2020 增加了 String.prototype.matchAll() 方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。

let reg = /t(e)(st(\d?))/g
let str = 'test1test2test3'
for (const match of str.matchAll(reg)) {
  console.log(match)
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3", groups: undefined]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3", groups: undefined]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3", groups: undefined]

上面代码中,由于 str.matchAll(reg) 返回的是遍历器,所以可以用 for...of循环取出。相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源。

遍历器转为数组是非常简单的,使用 ... 运算符和 Array.from() 方法就可以了

// 转为数组的方法一
[...str.matchAll(reg)]
// 转为数组的方法二
Array.from(str.matchAll(reg))