Skip to content
On this page

关于 JS 的正则表达式

正则表达式用处

寻找符合匹配规则(模式)的内容。

js
// 书写规则如下
/原子(原子表){长度}/模式修饰符

转义符

符号意义
\转义符,可以让特殊的字符失去原有的特殊意义
\n转义字符,换行(LF),将当前位置移到下一行开头
\r转义字符,回车(CR),将当前位置移到本行开头
\t转义字符,水平制表(HT),(跳到下一个TAB位置)

问题:

js
let price = 22.34
console.log(/\d+\.\d+/.test(price)) // true
let reg = new RegExp("\d+\.\d+")
console.log(reg.test(price)) // false
// 为什么上面的结果不一致?
console.log('\d' === 'd') // true
reg = new RegExp("\\d+\\.\\d+")
console.log(reg.test(price)) // true

原子或原子表

一个原子(原子表)表示该位置上的匹配规则,是正则表达式的最基本单位,每个模式至少要有一个原子,任何一种字符都可以作为原子,原子我们可以分为以下几类:

  1. 普通字符: 常见的字符,如大小写字母、数字等;

  2. 特殊字符或者不可打印的字符: 普通字符可以直接做为原子,但是对于特殊字符,比如: "'\ 等,这些字符作为原子的时候,需要使用转义字符 “\” 来取消他的特殊意义;

  3. 原子表: 原子表是指使用 [] 将一组彼此平等的原子包在一起组成的集合,原子前面可以加 ^,表示除这几个原子之外的字符,原子之间可以用 - 连接,表示连接一组ASCII码顺序排列的原子;

[asdf]表示对应位置上的字符可以是 asdf 中的任何一个字符;

[asdf]表示对应位置上的字符可以是除 asdf 这四个字符以外的所有字符;

[0-9A-D] 表示对应位置上的字符可以是 0,1,2,3,4,5,6,7,8,9,A,B,C,D 中的任何一个字符。

js
let preg = /1[^124][0-9]{9}/
let result = preg.exec('abc13699909900def')
console.log(result)
  1. 范围字符: 系统内置的一些范围字符,用来表示某一类字符。
原子意义
0-9按照ascii表中的编码字符,相当于[0123456789]
a-z按照ascii表中的编码字符
A-Z按照ascii表中的编码字符
\d相当于[0-9]
\D表示除了0-9之外的所有字符,相当于[^0-9]
\w相当于 [0-9a-zA-Z_]
\W相当于 [^0-9a-zA-Z_]
\s匹配空字符,包含空格、换行、制表符等
\S匹配非空字符
.除换行符和其他 unicode 行终止符之外的任意字符
js
var preq =/1[^124]\d\d\d/
var result = preq.execl('abc13699909900def') 
consolelog(result)

元字符

元字符不能单独存在,它一般被用来修饰原子。元字符根据其功能可以分为以下几类:

  1. 数量限定符

    限定符含义例子
    *匹配它前面原子0次或多次(贪婪匹配)ab*,可以匹配 aababbbbbb
    +匹配它前面原子1次或多次(贪婪匹配)
    ?匹配它前面原子0次或1次
    {m}匹配它前面的原子m次
    {m,}匹配它前面原子m次或多次
    {m,n}匹配它前面原子最少m次,最多n次
    *?+???{m,}?{m,n}?使 *、+、?、{m,}、{m,n} 变成非贪婪模式,也就是使这些匹配次数不定的表达式尽可能少的匹配
js
var preg =/1[^124]\d{9}/
var result = preg.execl('abc13699909900def') 
consolelog(result)
   
// 默认情况下,正则表达式是贪婪的,会尽可能的匹配更多 var preg = /1[^124]\d{5,9}/
var result =preg.exec('abc13699909900def') 
console.log(result)
   
// 非贪婪匹配,在满足条件下,尽可能少的匹配 
var preg = /1[^124]\d{5,9}?/
var result =preg.exec('abc13699909900def') 
console.log(result)
  1. 边界限制符

    边界限制符含义
    ^从字符串开始位置匹配
    $匹配到字符串的尾部
js
var preg = /^1[^124]\d{5,9}?/
// 字符串要从开头位置开始匹配,也就是说从开头就要符合正则规则
var result =preg.exec('13699909900def') 
console.log(result)

// 一直匹配到结尾,它会使非贪婪模式失效
var preg = /^1[^124]\d{5,9}$/
var result =preg.exec('13699909900') 
console.log(result)
  1. 模式选择符:|| 分割多个匹配模式,符合其中一个或多个都会被匹配到。
js
// | 可以连接多个正则表达式规则,表示符合任何一个规则即可
var preg = /^1[3589]\d{9}$|^16[56]\d{8}$|^17[678]\d{8}$/
var result =preg.exec('17799909900') 
console.log(result)
  1. 分组(子模式)相关元字符: 分组是指使用()包裹正则表达式中的某一个片段,将这一段正则表达式当成一个组来看待,分组不会对全局表达式结果产生影响,不过却能额外或者该分组的执行结果,分组主要用于提取数据。
符号含义
()分组符(子模式符),会捕获分组的内容
``
\n(n 是整数)回溯引用,调用第n个分组的结果
?: 表达式分组符内使用,非捕获分组,不再捕获对应分组的内容
js
var preg = /\w+@(\w+.[a-z]{2,10})/
var result = preg.exec('test@qq.com')
// 分组内容会被提取出来,一并放到结果中
console.log(result)

// 结尾是 com cn net 任意一个都行
var preg = /^\w+@(\w+.(com|cn|net))$/
var result = preg.exec('test@qq.com')
console.log(result)

// 使用 \1 \2 等可以引用之前已捕获的分组的结果
var preg = /<(\w+)>(\S*)<\/\1>/
var str = '<div>我很好</div><span>你很好</span>'
var result = preg.exec(str)
console.log(result)

// 非捕获组表示改组内容不再被捕获,也不会在结果中显示了
var preg = /<(?:[a-z]{1,4})>.+<\/(\w+)>/
var str = '<div>我很好</div><span>你很好</span>'
var result = preg.exec(str)
console.log(result)
  1. 原子表符号:

    符号含义
    []原子表的边界
    放到原子表中的 ^原子表内的元素取反操作
  2. 环视(断言) 如同 ^ 代表开头,$ 代表结尾,\b 代表单词边界一样,断言也有类似的作用,它们只匹配某些位置,在匹配过程中,不占用字符,所以被称为“零宽”。在处理匹配的时候,也是对当前位周围的情况进行观察,所以也叫“环视”。

    符号含义
    (?=表达式)正向先行断言,在某个位置向右看(写在右面),紧挨着该位置的右侧必须能匹配表达式
    (?!表达式)反向先行断言,在某个位置向右看(写在右面),紧挨着该位置的右侧必须能匹配表达式
    (?<=表达式)正向后行断言,在某个位置向左看(写在左面),紧挨着该位置的左侧必须能匹配表达式
    (?<!表达式)反向后行断言,在某个位置向左看(写在左面),紧挨着该位置的左侧必须能匹配表达式
    js
    // 正向先行断言
    // 匹配含有字符o的单词,但要求o的右面必须是个a,b,u 中的一个
    var preg = /\w*o(?=[abu])\w*/
    var str = 'hello world, I am your hero'
    var result = preg.exec(str)
    console.log(result) // your
    
    // 反向先行断言
    // 匹配含有字符o的单词,但要求o的右面不能是u
    var preg = /\w*o(?!u)\w*/
    var str = 'hello world, I am your brother'
    var result = preg.exec(str)
    console.log(result) // brother
    
    // 正向后行断言
    // 匹配含有字符o的单词,但要求o的左面必须是 r
    var preg = /\w*(?<=r)o\w*/
    var str = 'hello world, I am your hero'
    var result = preg.exec(str)
    console.log(result) // hero
    
    // 反向后行断言
    // 匹配含有字符o的单词,但要求o的左面不是 l
    var preg = /\w*(?<!r)o\w*/
    var str = 'hello world, I am your hero'
    var result = preg.exec(str)
    console.log(result) // world
    

    关于先行(lookahead)和后行(lookbehind): 正则表达式引擎在执行字符串和表达式匹配时,会从头到尾(从前到后)连续扫描字符串中的字符,设想有一个扫描指针指向字符边界处并随匹配过程移动。先行断言,是当扫描指针位于某处时,引擎会尝试匹配指针还未扫过的字符,先于指针到达该字符,故称为先行。后行断言,引警会尝试匹配指针已扫过的字符,后于指针到达该字符,故称为后行。

问题:

js
//字符串中必须含有至少一个大写字母、至少一个小写字母、至少一个数字,同时字符串的长度不能低于6位
var preg = /(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{6,}/
var str = '1A11a132'
var result = preg.exec(str)
console.log(result)

模式修饰符

对整个匹配规则(模式)进行微调。

修饰符意义例子
i匹配时不区分大小写/^[0-9a-z]+@[a-z0-9-]{1,64}\.[a-z]{2,12}$/i
g全局匹配(如果使用 exec(),需要多次调用)/^[0-9a-z]+@[a-z0-9-]{1,64}\.[a-z]{2,12}$/ig
m多行匹配,如果被匹配的字符串中有换行,则每行的开头都可以被^符号识别/^[0-9a-z]+@[a-z0-9-]{1,64}\.[a-z]{2,12}/m

y 修饰符 (ES6)

es5 修饰符:i (忽略大小写) m (多行匹配) g (全局匹配) . 只能匹配换行符以外的任意单个字符 y 叫做粘连(sticky)修饰符,文档描述

js
const str = 'aaa_aa_a'
const reg1 = /a+/g // 每次匹配剩余的
const reg2 = /a+/y // 每次从剩余的第一个开始匹配
console.log(reg1.exec(str)) 
// ['aaa', index: 0, input: 'aaa_aa_a', groups: undefined]
console.log(reg1.exec(str)) 
// ['aa', index: 4, input: 'aaa_aa_a', groups: undefined]
console.log(reg1.exec(str)) 
// ['a', index: 7, input: 'aaa_aa_a', groups: undefined]

console.log(reg2.exec(str)) 
// ['aaa', index: 0, input: 'aaa_aa_a', groups: undefined]
console.log(reg2.exec(str)) 
// null 为什么?每次从剩余的第一个开始匹配,_aa_a 不满足 reg2
console.log(reg2.exec(str)) 
// ['aaa', index: 0, input: 'aaa_aa_a', groups: undefined] , 上一次结果为 null ,所以从头开始匹配

u 修饰符 (ES6)

文档描述

js
const regex = new RegExp('\u{61}', 'u');
console.log(regex.unicode); // true

const str = '\uD842\uDFB7' // '𠮷' 表示4个字节的 UTF16 的编码,表示一个字符,超出 unicode 范围
console.log(/^\uD842/.test(str)) // true , es5 用法,其实是不对的,因为上面表示一个字符(这是一个整体,不能拆开),这个正则处理当做两个字符来处理了
console.log(/^\uD842/u.test(str)) // false

当码点超出 Unicode 范围,. 匹配符就不能识别了(在 Unicode 范围内,只能匹配换行符以外的任意单个字符), 使用 u 修饰符就可以识别了。

js
const str = '\uD842\uDFB7'
console.log(/^.$/.test(str)) // false
console.log(/^.$/u.test(str)) // true

console.log(/\u{61}/.test('a')) // false
console.log(/\u{61}/u.test('a')) // true

console.log(/𠮷{2}/.test('𠮷𠮷')) // false ,当前 𠮷 码点超出 unicode 范围了
console.log(/𠮷{2}/u.test('𠮷𠮷')) // false

dotAll (ES9/ES2018)

. 只能匹配换行符以外的任意单个字符(这些字符属于 Unicode 范围内s 修饰符可以开启 dotAll 模式

js
console.log(/./.test('\n')) // false 
console.log(/./.test('\r')) // false 
console.log(/./.test('\u{2028}')) // false 行分隔符
console.log(/./.test('\u{2029}')) // false 段分隔符
console.log(/./.test('\uD842\uDFB7')) // true
console.log(/./.test('𠮷')) // true
// 使用 dotAll
console.log(/./s.test('\n')) // true 
console.log(/./s.test('\r')) // true 
console.log(/./s.test('\u{2028}')) // true
console.log(/./s.test('\u{2029}')) // true

具名组匹配 (ES9/ES2018)

js
/(\d{4})-(\d{2})-(\d{2})/.exec('2021-08-12')
// ['2021-08-12', '2021', '08', '12', index: 0, input: '2021-08-12', groups: undefined]

const regExp = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
console.log(regExp.exec('2021-08-12'))
// groups: {year: '2021', month: '08', day: '12'}
let groups = regExp.exec('2021-08-12').groups
const { year, month, day } = groups ?? {}

后行断言 (ES9/ES2018)

对应的还有先行断言

js
// 先行断言 es5 语法 ?=
const str = 'ecmascript'
console.log(str.match(/ecma(?=script)/)) 
// ['ecma', index: 0, input: 'ecmascript', groups: undefined]
// 上面的意思:想匹配后面跟着 script 的 'ecma' 字符串
const str1 = 'ecmaxxscript'
console.log(str1.match(/ecma(?=script)/)) // null

// 后行断言 ?<= ?<!
console.log(str.match(/(?<=ecma)script/))
// ['script', index: 4, input: 'ecmascript', groups: undefined]
const str2 = 'ecmajjscript'
console.log(str.match(/(?<=ecma)script/)) // null
console.log(str.match(/(?<!ecma)script/))
// ['script', index: 6, input: 'ecmajjscript', groups: undefined]

Released under the MIT License.