在开发的过程中,我们不使用正则表达式也可以正常工作,但是,如果有了正则表达式(Regular Expression)的话,那么有些工作任务将事半功倍。使用场景如下:
- 用户输入(手机号、密码、邮箱、邮编等)验证。
- 操作HTML节点中的字符串。
- 使用CSS选择器表达式定位部分选择器。
- 判断一个元素是否含有特定的样式名称。
- 更多…
对正则的感受是好像很强大,但是又害怕学习它,感觉一大堆字符看着很难很复杂的样子,其实了解一下内容(不多)、然后做一些练习后,正则也就那样。正则表达式平时可能用得不多,久了容易遗忘,故在此做个基础的小总结以便日后查看。
1. 使用正则很酷
让我们通过一个例子来查看使用与不使用正则的区别:要验证一个字符串是否符合美国邮编(也叫ZIP代码)格式,即邮编要遵循一个特定的格式:99999-999,该格式前面有5位数字,然后紧跟着连字符-
,连字符后面再跟着4位数字。
1.1 不使用正则
不使用正则,判断的一种方法如下:
function isThisZipCode(candidate) {
if (typeof candidate !== 'string' || candidate.length !== 10) {
return false
}
for (let i = 0; i < candidate.length; i++) {
let c = candidate[i]
switch (i) {
case 0: case 1: case 2: case 3: case 4:
case 6: case 7: case 8: case 9:
if (c < '0' || c > '9') {
return false
}
break
case 5:
if (c !== '-') {
return false
}
break
}
}
return true
}
1.2 使用正则
使用正则的一种写法:
function isThisZipCode(candidate) {
return /^\d{5}-\d{4}$/.test(candidate)
}
看到了吗,使用正则是不是更简洁、更优雅?!
2. JS中正则表达式的创建方式
在JavaScript中,有2种方式可以创建正则表达式:
- 正则表达式字面量
- 通过构造RegExp对象的实例
例如,要创建一个一般的正则表达式(或简称正则(regex)),用于精确匹配字符串'test'
,使用正则字面量:
const pattern = /test/
正则字面量是通过正斜杠(/
)进行界定的,就像字符串是通过引号(""
或''
)进行界定一样。
使用RegExp构造函数:
const pattern = new RegExp('test')
//或者
const pattern = new RegExp(/test/)
使用RegExp构造函数的时候,是将正则以字符串的形式或则正则字面量传入。
那么什么时候该使用哪种方式呢?如果正则是已知的话,则优先选择字面量语法,而构造器方式则是用于在运行时,通过动态构建字符串来构建正则表达式。
除了正则表达本身,还有一些标志可以与正则表达式进行关联(主要使用到的是i
、g
、m
):
i
——ignore case的简写,让正则不区分大小写,例如/test/i
不仅可以匹配test
,还可以匹配Test
、tEst
、TEsT
等。g
——global的简写,匹配模式种的所有实例,而不是默认只匹配第一次出现的结果。m
——multiline的简写,多行匹配,只影响^
和$
,二者变成行的概念,即行开头和行结尾。允许匹配多个行,比如可以匹配文本区元素(textarea)中的值。u
——Unicode的简写,将模式视为Unicode字符码的序列。y
——sticky,粘性匹配;仅匹配目标字符串中此正则表达式的lastIndex属性指示的索引(并且不尝试从任何后续的索引匹配)
这些标志添加的位置:
- 字面量形式:加到第二个正斜杆后面
- 构造函数形式:作为第二个参数。
3. 术语与操作符
正则表达式,由术语和验证这些术语的操作符组成,接下来我们了解一下它们。
3.1 精确匹配
如果一个字符不是特殊字符或操作符,则表示该字符必须在表达式中出现。例如,在/test/
正则中,有4个术语,它们表示这些字符必须在一个字符串中出现才能匹配该模式。
一个接着一个的字符,隐式地表达“后面跟着(followed by)”这样一个操作。所以,/test/
的意思是说,'t'
后面跟着'e'
,'e'
后面跟着's'
,'s'
后面跟着't'
3.2 匹配一类字符:[]
-
匹配一个字符集中的某个字符,可以通过将字符集放到中括号(
[]
)中,来指定该字符集操作符(也称为字符类(character class)操作符)。比如[abc]
就是表示要匹配'a'
、'b'
、'c'
中的任何一个字符。 -
有时,如果要匹配一个字符集以外的字符,可以通过在中括号中的第一个开括号的后面加一个插入符(
^
)来实现,比如:[^abc]
意思为除了'a'
、'b'
、'c'
以外的任何一个字符。 -
字符集操作方面还有一个重要的变异操作:制定一个范围(使用
-
表示连续的范围)。比如要匹配'a'
和'm'
之间的任何一个小写字母,我们可以这样写:[abcdefghijklm]
,但是可以更加简洁:[a-m]
,中横线(-
)表示从'a'
到'm'
之间的所有字符(包括'a'
和'm'
)都在该字符集内。
3.3 转义:\
有一些特殊字符(比如.
,$
,[
等)表示的是它们自身以外的东西,如果我们要匹配这些特殊字符,我们需要通过反斜杆(\
)进行转义,让被转义字符作为字符本身进行匹配。
根据使用情况可能需要转义的有:^
、$
、.
、*
、+
、?
、|
、\
、/
、(
、)
、[
、]
、{
、}
、=
、!
、:
、-
。
3.4 匹配开始与匹配结束:^、$
- 匹配开始:将插入符(
^
)作为正则表达式的第一个字符,则表示要从字符串的开头进行匹配,例如/^test/
只能匹配以'test'
开头的字符串。 - 匹配结束:将美元符号(
$
)作为正则表达式的最后一个字符,则表示该模式必须出现在字符串的结尾,例如/test$/
表示只能匹配以'test'
结尾的字符串。 - 同时使用
^
和$
则表明指定的模式必须包含整个候选字符串。
3.4 重复出现:?、+、*、{n}、{n, m}、{n, }
在一个字符后面添加如下操作符的含义:
?
——表示可选,即可出现一次或者不出现。例如/test?/
可以匹配'test'
或'tes'
。+
——表示出现一次以上。例如/tes+t/
可以匹配'test'
、'tesst'
、'tessssst'
等。*
——表示出现零次或多次。例如/tes*t/
可以匹配'tet'
、'test'
、'tesssst'
等。{n}
——花括号里面指定一个数字来表示重复次数。例如/tes{3}t/
匹配'tessst'
。{n,m}
——花括号里面指定两个数字(用逗号,
分隔)来表示重复次数的区间。例如/a{3,10}/
表示匹配任何含有连续3到10个'a'
字符的字符串。{n,}
——花括号中第二个数省略,但保留逗号,表示重复次数的开区间,例如/a{3,}/
表示匹配任何含有连续3个或3个以上的'a'
字符的字符串。
这些重复操作符可以是贪婪的或非贪婪的。默认情况下,它们是贪婪的:它们匹配所有的字符组合。在操作符后面加一个问号?
字符,如/a+?/
可以让该表达式变成非贪婪的:进行最小限度的匹配。举个例子,若对字符串'aaa'
进行匹配,正则表达式/a+/
将匹配所有着三个字符,而非贪婪的表达式/a+?/
只匹配一个'a'
字符,因为一个'a'
字符就可以满足a+
术语。
3.5 预定义字符类
预定义术语 | 匹配内容 |
---|---|
\t | 水平制表符(tab) |
\b | 空格(blank) |
\v | 垂直制表符(vertical) |
\f | 换页符(form feed) |
\r | 回车(return) |
\n | 换行符 |
\cA:\cZ | 控制符,例如:\cM 匹配一个Control-M |
\x0000:\xFFFF | 十六进制Unicode码 |
\x00:\xFF | 十六进制ASCII码 |
. | 匹配除了换行符、回车符、行分隔符和段分隔符以外的任意字符,等价于[^\n\u\u2028\u2029] |
\d | 匹配任意数字(digital),等价于[0-9] |
\D | 匹配任意非数字,等价于[^0-9] |
\w | 匹配包括下划线的任意单词字符(word),等价于[A-Za-z0-9_] |
\W | 匹配任何非单词字符,等价于[^A-Za-z0-9_] |
\s | 匹配任何空白字符(space),包括空格、水平制表符、垂直制表符、换行符、回车符和换页符。等价于[\t\v\n\r\f] |
\S | 匹配任何非空白字符。等价于[^\t\v\n\r\f] |
\b | 匹配单词边界(boundary) |
\B | 匹配非单词边界 |
由以上可知要想匹配任意字符的话,只要结合它们的对立面即可,可以使用如下任意一个:[\d\D]
、[\w\W]
、[\s\S]
和[^]
。
3.6 分组:()
到目前为止,我们看到的操作符(如+
和*
)只能影响前面的术语,如果将操作符运用于一组术语,可以像数学表达式一样在该组上使用小括号()
,例如/(ab)+/
匹配一个或多个连续出现的子字符串'ab'
。
当正则表达式由一部分是用括号进行分组时,它具有双重责任,同时也创建了所谓的捕获(capture)。
如果只想要括号最原始的功能,但不会引用它,即,既不在 API 里引用,也不在正则里反向引用,此时可以使用非捕获括号(因为捕获分组和分支里的数据需要更多的内存来保存这些信息): (?:p)
和 (?:p1|p2|p3)
,这样就只取到分组的作用。
3.7 或操作符:|
可以使用竖线(|
)表示或者的关系。例如/a|b/
匹配'a'
字符或者'b'
字符,/(ab)+|(cd)+/
表示匹配出现一次或多次的'ab
或'cd'
3.8 反向引用
正则表达式中最复杂的术语是,在正则中所定义的捕获(capture) 的反向引用。这种术语表示法就是在反斜杆后面加一个要引用的捕获数字(捕获所在的索引),该数字从1开始,如\1
、\2
等。
举例来说,/^([dnt])a\1/
可以匹配任意一个以'd'
、'n'
、't'
开头,且后面跟着一个'a'
字符,并且再后面跟着的是和第一个捕获相关字符的字符串(这个很重要!)。比如'dad'
就可以正确匹配,而'dan'
就不行(因为最后一个'n'
字符不是第一个捕获'd'
字符)。
因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。例如\2
,就匹配 '\2'
。注意 '\2'
表示对 '2'
进行了转义。
分组后面有量词的话,分组最终捕获到的数据是最后一次的匹配。例子:
let regex = /(\d)+/
let string = '12345'
console.log( string.match(regex) ) // ["12345", "5", index: 0, input: "12345", groups: undefined]
从上面看出,分组 (\d)
捕获的数据是'5'
。
反向引用在匹配HTML类型标记的时候会很有用,比如这个正则:
/<(\w+)>(.+)<\/\1>/
要匹配像<strong>whatever</strong>
这样的简单元素,不使用反向引用,是无法做到的。因为我们无法知道关闭标签和开始标签是否匹配。
3.9 位置匹配:(?=p)、(?!p)
(?=p)
,其中p
是一个子模式,即p
前面的位置,或者说,该位置后面的字符要匹配p
。例子:
let result = 'hello'.replace(/(?=l)/g, '#')
console.log(result) // 'he#l#lo'
即表示匹配'l'
字符前面的位置,然后将该位子替换为'#'
,result
的结果为:'he#l#lo'
。
(?!p)
就是 (?=p)
的反面意思。
let result = 'hello'.replace(/(?!l)/g, '#')
console.log(result) // '#h#ell#o#'
一般理解(?=p)
为:要求接下来的字符与p
匹配,但不能包括p
匹配的那些字符。
我们也可以理解为p
前面的那个位置。对于位置的理解,我们可以理解为空字符''
,如'hello'
等价于如下的形式:
'hello' == '' + 'h' + '' + 'e' + '' + 'l' + '' + 'l' + '' + 'o' + '';
4. 正则表达式的4种操作
正则表达式主要分4种操作:验证、切分、提取、替换 。
正则操作的方法共有6个,字符串方法4个,正则方法2个:
- String.prototype.match()
- String.prototype.replace()
- String.prototype.search()
- String.prototype.split()
- RegExp.prototype.exec()
- RegExp.prototype.test()
这些API不详细解释,不清楚的可以去学习一下。
4.1 验证
比如表单验证,其实就是字符串的匹配操作,如果匹配上了就是验证通过。例子,判断一个字符串中是否有数字,可以采用的验证方法如下:
- 使用
test
(这个方法最常用)let regex = /\d/ let string = 'abc123' console.log( regex.test(string) ) // true
- 使用
search
let regex = /\d/ let string = 'abc123' // 对search的结果先取反,可以排除search结果为0的情况(如string为'0abc'时) console.log( !!~string.search(regex) ) // true
- 使用
match
let regex = /\d/ let string = 'abc123' console.log( !!string.match(regex) ) // true
- 使用
exec
let regex = /\d/ let string = 'abc123' console.log( !!regex.exec(string) ) // true
4.2 切分
使用字符串的split
方法可以对字符串进行切分。
- 例1,目标字符串是
'html,css,javascript'
,按逗号来切分:let regex = /,/ let string = 'html,css,javascript' console.log( string.split(regex) ) // ["html", "css", "javascript"] // 当然像这种简单的可以直接用字符串来作为参数 console.log( string.split(',') ) // ["html", "css", "javascript"]
- 例2,目标字符串是
'2019/08/13'
、'2019.08.13'
、'2019-08-13'
,切分出年月日:let regex = /\D/ console.log( '2019/08/13'.split(regex) ) // ["2019", "08", "13"] console.log( '2019.08.13'.split(regex) ) // ["2019", "08", "13"] console.log( '2019-08-13'.split(regex) ) // ["2019", "08", "13"]
4.3 提取
有时匹配上了,我们想要提取部分匹配的数据,这时通常要使用分组引用(分组捕获)功能,还要配合使用相关的API。这里,还是以日期为例,提取出年月日。注意下面正则中的括号:
- 使用
match
(这个方式最为常用)let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' console.log( string.match(regex) ) // => ["2019-08-13", "2019", "08", "13", index: 0, input: "2019-08-13", groups: undefined]
- 使用
exec
let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' console.log( regex.exec(string) ) // => ["2019-08-13", "2019", "08", "13", index: 0, input: "2019-08-13", groups: undefined]
- 使用
test
let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' regex.test(string) // 执行完test方法后,RegExp全局属性$1、$2、$3...的值为对应分组的匹配值 console.log( RegExp.$1, RegExp.$2, RegExp.$3 ) // "2019" "08" "13"
- 使用
search
let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' string.search(regex) // 执行完search方法后,RegExp全局属性$1、$2、$3...的值为对应分组的匹配值 console.log( RegExp.$1, RegExp.$2, RegExp.$3 ) // "2019" "08" "13"
- 使用
replace
let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' let date = [] string.replace(regex, (match, year, month, day) => { date.push(year, month, day) }); console.log(date); // ["2019", "08", "13"]
4.4 替换
找,往往不是目的,通常下一步是为了替换。在 JavaScript 中,使用 replace
进行替换。
比如把日期格式,从 yyyy-mm-dd
替换成 yyyy/mm/dd
:
let string = '2019-08-13'
console.log( string.replace(/-/g, '/') ) // "2019/08/13"
replace
方法很强大,需要重点掌握。
4.5 强大的replace
总体来说 replace 有两种使用形式,这是因为它的第二个参数,可以是字符串,也可以是函数。
-
当第二个参数是字符串时,如下的字符有特殊的含义:
属性 描述 $1
、$2
、····,$99
匹配第 1-99 个 分组里捕获的文本 $&
匹配到的子串文本 $`
匹配到的子串的左边文本 $'
匹配到的子串的右边文本 $$
美元符号 例如,把
'2,3,5'
,变成'5=2+3'
:let result = '2,3,5'.replace(/(\d+),(\d+),(\d+)/, '$3=$1+$2') console.log(result) // 5=2+3
又例如,把
'2,3,5'
,变成'222,333,555'
:let result = '2,3,5'.replace(/(\d+)/g, '$&$&$&') console.log(result) // 222,333,555
再例如,把
'2+3=5'
,变成'2+3=2+3=5=5'
:let result = '2+3=5'.replace(/=/, "$&$`$&$'$&") // $&匹配到的是'=',$`这个匹配到的是'2+3',$'匹配到的是'5' console.log(result) // 2+3=2+3=5=5
-
当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么:
'1234 2345 3456'.replace(/(\d)\d{2}(\d)/g, (match, $1, $2, index, input) => { console.log([match, $1, $2, index, input]) }) // ["1234", "1", "4", 0, "1234 2345 3456"] // ["2345", "2", "5", 5, "1234 2345 3456"] // ["3456", "3", "6", 10, "1234 2345 3456"]
4.6 注意点
在使用正则的过程中,以下的一些点需注意:
- 字符串 4 个方法参数都支持正则和字符串,但
search
和match
方法会把字符串转换为正则的。let string = '2019.08.13' console.log( string.search('.') ) // 0 //需要修改成下列形式之一 console.log( string.search('\\.') ) // 4 console.log( string.search(/\./) ) // 4
match
返回结果的格式,与正则对象是否有修饰符g
有关。
没有let string = '2019.08.13' let regex1 = /\b(\d+)\b/ let regex2 = /\b(\d+)\b/g console.log( string.match(regex1) ) // ["2019", "2019", index: 0, input: "2019.08.13", groups: undefined] console.log( string.match(regex2) ) // ["2019", "08", "13"]
g
,返回的是标准匹配格式,即,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然后是整体匹配的第一个下标,最后是输入的目标字符串。有g
,返回的是所有匹配的内容。当没有匹配时,不管有无g
,都返回null
。exec
比match
更强大
当正则没有g
时,使用match
返回的信息比较多。但是有g
后,就没有关键的信息index
了。而exec
方法就能解决这个问题,它能接着上一次匹配后继续匹配:
从上述代码看出,在使用let string = '2019.08.13' let regex2 = /\b(\d+)\b/g console.log( regex2.exec(string) ) // ["2019", "2019", index: 0, input: "2019.08.13", groups: undefined] // lastIndex 属性,表示下一次匹配开始的位置 console.log( regex2.lastIndex) // 4 console.log( regex2.exec(string) ) // ["08", "08", index: 5, input: "2019.08.13", groups: undefined] console.log( regex2.lastIndex) // 7 console.log( regex2.exec(string) ) // ["13", "13", index: 8, input: "2019.08.13", groups: undefined] console.log( regex2.lastIndex) // 10 console.log( regex2.exec(string) ) // null console.log( regex2.lastIndex) // 0
exec
时,经常需要配合使用while
循环:let string = '2019.08.13' let regex2 = /\b(\d+)\b/g let result while ( result = regex2.exec(string) ) { console.log( result, regex2.lastIndex ); } // ["2019", "2019", index: 0, input: "2019.08.13", groups: undefined] 4 // ["08", "08", index: 5, input: "2019.08.13", groups: undefined] 7 // ["13", "13", index: 8, input: "2019.08.13", groups: undefined] 10
- 修饰符
g
,对exex
和test
的影响
上面提到了正则实例的lastIndex
属性,表示尝试匹配时,从字符串的lastIndex
位开始去匹配。字符串的四个方法,每次匹配时,都是从 0 开始的,即lastIndex
属性始终不变。
而正则实例的两个方法exec
、test
,当正则是全局匹配时,每一次匹配完成后,都会修改lastIndex
。下面
让我们以test
为例,看看你是否会迷糊:
如果没有let regex = /a/g // 每次调用都会从 lastIndex 下标处开始查找 console.log( regex.test('a'), regex.lastIndex ) // true 1 console.log( regex.test('aba'), regex.lastIndex ) // true 3 console.log( regex.test('ababc'), regex.lastIndex ) // false 0
g
,自然都是从字符串第 0 个字符处开始尝试匹配:let regex = /a/ console.log( regex.test('a'), regex.lastIndex ) // true 0 console.log( regex.test('aba'), regex.lastIndex ) // true 0 console.log( regex.test('ababc'), regex.lastIndex ) // true 0
test
整体匹配时需要同时使用^
和$
test
方法只是查看目标字符串中是否有字串匹配正则,只要有部分匹配则返回true
。如果需要整体匹配,正则前后需要添加开头和结尾。console.log( /123/.test('a123b') ) // true console.log( /^123$/.test('a123b') ) // false console.log( /^123$/.test('123') ) // true
split
需要注意的两点
第一,它可以有第二个参数,表示结果数组的最大长度:
第二,正则使用分组时,结果数组中是包含分隔符的:let string = 'html,css,javascript' console.log( string.split(/,/, 2) ) // ["html", "css"]
let string = 'html,css,javascript' console.log( string.split(/(,)/) ) // ["html", ",", "css", ",", "javascript"]
5. 练习
5.1 匹配16进制颜色值
像:#AAfa10
、#ABC
、#111
。
思路:
- 16进制每一位的所有可能取值为0~9,A ~ F或a ~ f:
[0-9A-Fa-f]
- 位数可以为3位或者6位:
/#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})/
5.2 匹配数字时钟
像:23:59
、01:00
、19:50
。
思路(一共四位数字,它们之间有约束):
- 第一位数字为0、1或2
- 当第一位为0和1时,第二位可为0~9中的一个:
[01][0-9]
,因为[0-9]
可用\d
表示,那么也可为[01]\d
- 当第一位为2时,第二位可为0~3中的一个:
2[0-3]
- 那么第一位和第二位的组合为:
[01]\d|2[0-3]
- 第三位可为0~5中的一个:
[0-5]
- 第四位为0~9中的一个:
\d
- 最后组合一起:
/([01]\d|2[0-3]):[0-5]\d/
若也要支持时和分前面的0忽略的情况,那么可为:/(0?\d|1\d|2[0-3]):(0?\d|[1-5]\d)/
5.3 日期的匹配
比如:yyyy-mm-dd
的格式。
思路:
- 年份的四位为数字即可:
\d{4}
- 月份分首位为0和1的情况
- 当月份首位为0,那么第二位可为1~9中的一个:
0[1-9]
- 当月份首位为1,那么第二位可为0~2中的一个:
1[0-2]
- 那月份可为:
0[1-9]|1[0-2]
- 日首位有0、1、2、3四种
- 当日首位为0时,第二位可为1~9中的一个:
0[1-9]
- 当日首位为1或2时,第二位可为0~9中的一个:
[12]\d
- 当日首位为3时,第二位只能为0和1:
[01]
- 所以日的组合为:
0[1-9]|[12]\d|3[01]
- 最终组合为:
/\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])/
这里其实有问题,2月份是个特殊的例子,大家自己想想应该如何解决。
5.4 windows操作系统的文件路径匹配
比如:D:\study\javascript\RegExp\regular expression.pdf
、D:\study\javascript\RegExp\
、D:\study\javascript
、D:\
思路:
- 匹配
D:\
,盘符不区分大小写,\
需要转义:[a-zA-Z]:\\
- 文件夹或文件名不能包含特殊字符:
[^\\:*<>|“?\r\n/]
- 并且它们不能为空,因此匹配
文件夹\
为:[^\\:*<>|“?\r\n/]+\\
文件夹\
可以出现任意次:([^\\:*<>|“?\r\n/]+\\)*
- 路径的最后一部分可以是
文件夹
,没有\
,所以要添加:([^\\:*<>|“?\r\n/]+)?
- 最终:
/[a-zA-Z]:\\([^\\:*<>|“?\r\n/]+\\)*([^\\:*<>|“?\r\n/]+)?/
5.5 将数字改成千分位符表示法
比如将12345678
变为12,345,678
。
思路:
- 需要使用位置匹配
(?=p)
,从最末尾开始计算,每三位数字前加一个','
,先弄出最后一个逗号,此时(?=p)
中的p
即为\d{3}$
:/(?=\d{3}$)/g
,此时'12345678'.replace(/(?=\d{3}$)/g, ',')
变为12345,678
- 因为逗号出现的位置,要求后面3个数字为一组,也就是
\d{3}
至少出现一次,此时可以使用量词'+'
,弄出所有逗号:/(?=(\d{3})+$)/g
, 此时'12345678'.replace(/(?=(\d{3})+$)/g, ',')
变为12,345,678
- 不知道你发现没,这里会出现一个问题,匹配的数字串位数如果刚好是3的倍数的话,那么第一个字符就会变为
','
,如'123456'.replace(/(?=(\d{3})+$)/g, ',')
变为,123,456
,这不是我们想要的。所以我们要排除掉开头的位置,可以使用(?!^)
,那么最终的匹配模式为:/(?!^)(?=(\d{3})+$)/g
,'123456'.replace(/(?!^)(?=(\d{3})+$)/g, ',')
变为123,456
应用例子,货币的格式化,比如将1888
转化为$ 1,888.00
,代码如下:
function format (num) {
// $$ 即表示 $ 符号
return num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g, ',').replace(/^/, '$$ ');
};
console.log(format(1888)) // '$ 1,888.00'
console.log(format(1564312.62165)) // '$ 1,564,312.62'
5.6 密码验证
假设密码长度要求为6~12位,由数字、大写字母和小写字母组成,并且必须至少包括两种字符。
思路:
- 首先,不考虑至少包括两种字符的话,那很好写出来:
/^[0-9A-Za-z]{6,12}$/
- 其次,更进一步,假如必须包含数字的话,那么我们可以通过
(?=.*[0-9])
来实现。则正则变为:/(?=.*[0-9])^[0-9A-Za-z]{6,12}$/
,这个正则(?=.*[0-9])^
这部分理解了就搞懂整个了。(?=.*[0-9])^
分开来看就是(?=.*[0-9])
和^
,表示开头前面还有一个位置(当然也是开头,即同一个位置,想想之前的空字符类比)。(?=.*[0-9])
表示该位置后面的字符匹配.*[0-9]
,即,有任何多个任意字符,后面再跟个数字。翻译成大白话,就是接下来的字符,必须包含个数字。 - 需同时包含数字和小写字母的话,可用
(?=.*[0-9])(?=.*[a-z])
来实现,则正则变为:/(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/
- 有了以上,我们只要组合一下即可得出最后的答案:
/((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[ 0-9A-Za-z]{6,12}$/
另一种思路,至少包含两种字符的意思即为:不能全为数字、不能全为小写字母,不能全为大写字母。简单开始,那么不能全为数字该怎么实现?需要这个(?!p)
来匹配了:/(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/
,那么三个都不能的话,最终结果为:/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/
5.7 模拟字符串的trim方法
trim
方法用来去掉字符串首尾的空白符。
思路:找到字符串开头和结尾的所有空白符,将其替换为空字符''
即可。
function trim(str) {
return str.replace(/^\s+|\s+$/g, '')
}
console.log( trim(' javascript ') ) // 'javascript'
5.8 将每个单词的首字母转为大写
比如将'my name is javascript'
转化为'My Name Is Javascript'
。
思路:关键点在于如何找到字符串的每个单词的首字母,使用/(?:^|\s)\w/g
即可,即开头或者空白字符后面的第一个任意单词字符。
则可以通过如下代码实现:
function titleize (str) {
//这里不使用非捕获匹配也是可以的:/(^|\s)\w/g
return str.toLowerCase().replace(/(?:^|\s)\w/g, c => c.toUpperCase())
}
console.log( titleize('my name is javascript') ) // 'My Name Is Javascript'
5.9 驼峰化
比如将'-moz-transform'
转化为'MozTransform'
。
思路:其实主要还是怎么找到单词的首字母,将首字母变为大写,单词之间的内容去除。
function camelize (str) {
return str.replace(/[-_\s]+(.)?/g, (match, c) => c ? c.toUpperCase() : '')
}
console.log( camelize('-moz-transform') ) // 'MozTransform'
上面的代码中,(.)
即为首字母,单词的界定是,前面的字符可以是多个连字符'-'
、下划线'_'
以及空白符。正则后面
的'?'
的目的,是为了应对 str 尾部的字符可能不是单词字符,比如 str 是 '-moz-transform '
。
反之,将驼峰写法中划线化:
function dasherize (str) {
return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
}
console.log( dasherize('MozTransform') ); // '-moz-transform'
5.10 HTML转义与反转义
// 将HTML特殊字符转换成等值的实体
function escapeHTML (str) {
const escapeChars = {
'<' : 'lt',
'>' : 'gt',
'"' : 'quot',
'&' : 'amp',
'\'' : '#39'
}
//'[' + Object.keys(escapeChars).join('') +']', 结果即为:[<>"&']
return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'), match => '&' + escapeChars[match] + ';')
}
console.log( escapeHTML('<div>Blah blah blah</div>') ); // '<div>Blah blah blah</div>'
逆过程:
// 实体字符转换为等值的HTML。
function unescapeHTML (str) {
const htmlEntities = {
nbsp: ' ',
lt: '<',
gt: '>',
quot: '"',
amp: '&',
apos: '\''
};
return str.replace(/\&([^;]+);/g, (match, key) => key in htmlEntities ? htmlEntities[key] : match )
}
console.log( unescapeHTML('<div>Blah blah blah</div>') ); // '<div>Blah blah blah</div>'
通过 key 获取相应的分组引用,然后作为对象的键。
写在最后:正则虽然很强大,但也没必要事事都用正则,有时字符串自带的API使用起来会更为简单且易于理解。
参考文献:
- 《JavaScript忍者秘籍》一书
- 《JavaScript正则表达式迷你书(1.1版)》