JavaScript-正则表达式小记

什么是正则表达式

正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript中,正则表达式也是对象。
这些模式被用于 RegExpexectest 方法, 以及 Stringmatchreplacesearchsplit 方法。

正则表达式存在于大部分的编程语言,就算是在写shell时也会不经意的用到正则。
比如大家最喜欢的rm -rf ./*,这里边的*就是正则的通配符,匹配任意字符。

JavaScript也有正则表达式的实现,差不多就长这个样子:/\d/(匹配一个数字)。
个人认为正则所用到的地方还是很多的,比如模版字符的替换、解析URL,表单验证 等等一系列。
如果在Node.js中用处就更为多,比如请求头的解析、文件内容的批量替换以及写爬虫时候一定会遇到的解析HTML标签。

image

正则表达式在JavaScript中的实现

JavaScript中的语法

赘述那些特殊字符的作用并没有什么意义,浪费时间。
推荐MDN的文档:基础的正则表达式特殊字符

关于正则表达式,个人认为以下几个比较重要:

贪婪模式与非贪婪模式

P.S. 关于贪婪模式非贪婪模式,发现有些地方会拿这样的例子:

/.+/ // 贪婪模式
/.+?/ // 非贪婪模式

仅仅拿这样简单的例子来说的话,有点儿扯淡

// 假设有这样的一个字符串
let html = '<p><span>text1</span><span>text2</span></p>'

// 现在我们要取出第一个`span`中的文本,于是我们写了这样的正则
html.match(/<span>(.+)<\/span>/)
// 却发现匹配到的竟然是 text1</span><span>text2
// 这是因为 我们括号中写的是 `(.+)` .为匹配任意字符, +则表示匹配一次以上。
// 当规则匹配到了`text1`的时候,还会继续查找下一个,发现`<`也命中了`.`这个规则
// 于是就持续的往后找,知道找到最后一个span,结束本次匹配。

// 但是当我们把正则修改成这样以后:
html.match(/<span>(.+?)<\/span>/)
// 这次就能匹配到我们想要的结果了
// `?`的作为是,匹配`0~1`次规则
// 但是如果跟在`*`、`+`之类的表示数量的特殊字符后,含义就会变为匹配尽量少的字符。
// 当正则匹配到了text1后,判断后边的</span>命中了规则,就直接返回结果,不会往后继续匹配。

简单来说就是:

  1. 贪婪模式,能拿多少拿多少
  2. 非贪婪模式,能拿多拿多少

捕获组

/123(\d+)0/ 括号中的被称之为捕获组。

捕获组有很多的作用,比如处理一些日期格式的转换。

let date = '2017-11-21'

date.replace(/^(\d{4})-(\d{2})-(\d{2})$/, '$2/$3/$1')

又或者可以直接写在正则表达式中作为前边重复项的匹配。

let template = 'hello helloworl'
template.match(/(\w+) \1/) // => hello hello

// 我们可以用它来匹配出month和day数字相同的数据
let dateList = `
2017-10-10
2017-11-12
2017-12-12
`

dateList.match(/^\d{4}-(\d{2})-(\1)/gm) // => ["2017-10-10", "2017-12-12"]

非捕获组

我们读取了一个文本文件,里边是一个名单列表
我们想要取出所有Stark的名字(但是并不想要姓氏,因为都叫Stark),我们就可以写这样的正则:

let nameList = `
Brandon Stark
Sansa Stark
John Snow
`

nameList.match(/^\w+(?=\s?Stark)/gm) // => ["Brandon", "Sansa"]

上边的(?=)就是非捕获组,意思就是规则会被命中,但是在结果中不会包含它。

比如我们想实现一个比较常用的功能,给数组添加千分位:

function numberWithCommas (x = 0) {
  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

numberWithCommas(123) // => 123
numberWithCommas(1234) // => 1,234

\B代表匹配一个非单词边界,也就是说,实际他并不会替换掉任何的元素。
其次,后边的非捕获组这么定义:存在三的倍数个数字(3、6、9),并且这些数字后边没有再跟着其他的数字。
因为在非捕获组中使用的是(\d{3})+,贪婪模式,所以就会尽可能多的去匹配。
如果传入字符串1234567,则第一次匹配的位置在12之间,第二次匹配的位置在45之间。
获得的最终字符串就是1,234,567

如何使用正则表达式

RegExp对象

创建RegExp对象有两种方式:

  1. 直接字面量的声明:/\d/g
  2. 通过构造函数进行创建:new RegExp('\d', 'g')

RegExp对象提供了两个方法:

exec

方法执行传入一个字符串,然后对该字符串进行匹配,如果匹配失败则直接返回null
如果匹配成功则会返回一个数组:

let reg = /([a-z])\d+/
let str = 'a233'
let result = reg.exec(str) // => ['a233', 'a', ...]

P.S. 如果正则表达式有g标识,在每次执行完exec后,该正则对象的lastIndex值就会被改变,该值表示下次匹配的开始下标

let reg = /([a-z])\d+/g
let str = 'a233'
reg.exec(str) // => ['a233', 'a', ...]
// reg.lastIndex = 4
reg.exec(str) // => null
test

方法用来检查正则是否能成功匹配该字符串

let reg = /^Hello/

reg.test('Hello World') // => true
reg.test('Say Hello') // => false

test方法一般来说多用在检索或者过滤的地方。
比如我们做一些筛选filter的操作,用test就是一个很好的选择。

// 筛选出所有名字为 Niko的数据
let data = [{ name: 'Niko Bellic' }, { name: 'Roman Bellic'}]

data.filter(({name}) => /^Niko/.test(name)) // => [{ name: 'Niko Bellic' }]

String对象

除了RegExp对象实现的一些方法外,String同样提供了一套方法供大家来使用。

search

传入一个正则表达式,并使用该表达式进行匹配;
如果匹配失败,则会返回-1
如果匹配成功,则会返回匹配开始的下标。
可以理解为是一个正则版的indexOf

'Hi Niko'.search(/Niko/) // => 3
'Hi Niko'.search(/Roman/) // => -1

// 如果传入的参数为一个字符串,则会将其转换为`RegExp`对象
'Hello'.search('llo') // => 2
split

split方法应该是比较常用的,用得最多的估计就是[].split(',')了。。

然而这个参数也是可以塞进去一个正则表达式的。

'1,2|3'.split(/,|\|/) // => [1, 2, 3]

// 比如我们要将一个日期时间字符串进行分割
let date = '2017-11-21 23:40:56'

date.split(/-|\s|:/)

// 又或者我们有这么一个字符串,要将它正确的分割
let arr = '1,2,3,4,[5,6,7]'

arr.split(',') // => ["1", "2", "3", "4", "[5", "6", "7]"] 这个结果肯定是不对的。

// 所以我们可以这么写
arr.split(/,(?![,\d]+])/) // => ["1", "2", "3", "4", "[5,6,7]"]

该条规则会匹配,,但是,后边还有一个限定条件,那就是绝对不能出现数字+,的组合并且以一个]结尾。
这样就会使[4,5,6]里边的,不被匹配到。

match

match方法用来检索字符串,并返回匹配的结果。

如果正则没有添加g标识的话,返回值与exec类似。
但是如果添加了g标识,则会返回一个数组,数组的item为满足匹配条件的子串。
这将会无视掉所有的捕获组。
拿上边的那个解析HTML来说

let html = '<p><span>text1</span><span>text2</span></p>'

html.match(/<span>(.+?)<\/span>/g) // => ["<span>text1</span>", "<span>text2</span>"]
replace

replace应该是与正则有关的应用最多的一个函数。
最简单的模版引擎可以基于replace来做。
日期格式转换也可以通过replace来做。
甚至match的功能也可以通过replace来实现(虽说代码会看起来很丑)

replace接收两个参数
replace(str|regexp, newStr|callback)

第一个参数可以是一个字符串 也可以是一个正则表达式,转换规则同上几个方法。
第二个参数却是可以传入一个字符串,也可以传入一个回调函数。

当传入字符串时,会将正则所匹配到的字串替换为该字符串。
当传入回调函数时,则会在匹配到子串时调用该回调,回调函数的返回值会替换被匹配到的子串。

'Hi: Jhon'.replace(/Hi:\s(\w+)/g, 'Hi: $1 Snow') // => Hi: Jhon Snow

'price: 1'.replace(/price:\s(\d)/g, (/* 匹配的完整串 */str, /* 捕获组 */ $1) => `price: ${$1 *= 10}`) // => price: 10

一些全新的特性

前段时间看了下ECMAScript 2018的一些草案,发现有些Stage 3的草案,其中有提到RegExp相关的,并在chrome上试验了一下,发现已经可以使用了。

Lookbehind assertions(应该可以叫做回溯引用吧)

同样也是一个非捕获组的语法定义

语法定义:

let reg = /(?<=Pre)\w/

reg.test('Prefixer') // => true
reg.test('Prfixer') // => false

设置匹配串前边必须满足的一些条件,与(?=)正好相反,一前一后。
这个结合着(?=)使用简直是神器,还是说解析HTML的那个问题。
现在有了(?<=)以后,我们甚至可以直接通过一个match函数拿到HTML元素中的文本值了。

let html = '<p><span>text1</span><span>text2</span></p>'

html.match(/(?<=<span>)(.+?)(?=<\/span>)/g) // => ["text1", "text2"]

Named capture groups(命名捕获组)

我们知道,()标识这一个捕获组,然后用的时候就是通过\1或者$1来使用。
这次草案中提到的命名捕获组,就是可以让你对()进行命名,在使用时候可以用接近变量的用法来调用。

语法定义:

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

'2017-11-21'.match(reg)

match的返回值中,我们会找到一个groupskey
里边存储着所有的命名捕获组。

image
image
在replace中的用法
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
'2017-11-21'.replace(reg, '$<month>/$<day>/$<year>') // => 21/11/2017
表达式中的反向引用
let reg = /\d{4}-(?<month>\d{2})-\k<month>/
reg.test('2017-11-11') // => true
reg.test('2017-11-21') // => false

参考资料

个人GitHub:https://github.com/jiasm

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容

  • 那个两手撑地倒贴墙上半天的小姑娘不见了,那个一边走路喜欢一边蹦跳的小姑娘不见了,那个能灵活翻空心跟头的小姑娘不见了...
    东方爱_蒲公英阅读 674评论 17 27
  • 人生若只如初见,何事秋风悲画扇。纳兰性德的这首诗当时不知受多少人的喜爱。是啊,若只如初见该多好。 ...
    寒枝旧廊阅读 294评论 3 2