正则表达式小记

在开发的过程中,我们不使用正则表达式也可以正常工作,但是,如果有了正则表达式(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构造函数的时候,是将正则以字符串的形式或则正则字面量传入。
那么什么时候该使用哪种方式呢?如果正则是已知的话,则优先选择字面量语法,而构造器方式则是用于在运行时,通过动态构建字符串来构建正则表达式。
除了正则表达本身,还有一些标志可以与正则表达式进行关联(主要使用到的是igm):

  • i——ignore case的简写,让正则不区分大小写,例如/test/i不仅可以匹配test,还可以匹配TesttEstTEsT等。
  • 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个:

这些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 个方法参数都支持正则和字符串,但 searchmatch方法会把字符串转换为正则的。
    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
  • execmatch更强大
    当正则没有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,对exextest的影响
    上面提到了正则实例的 lastIndex属性,表示尝试匹配时,从字符串的 lastIndex位开始去匹配。字符串的四个方法,每次匹配时,都是从 0 开始的,即 lastIndex属性始终不变。
    而正则实例的两个方法 exectest,当正则是全局匹配时,每一次匹配完成后,都会修改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:5901:0019: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.pdfD:\study\javascript\RegExp\D:\study\javascriptD:\
思路

  • 匹配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>') ); // '&lt;div&gt;Blah blah blah&lt;/div&gt;'

逆过程:

// 实体字符转换为等值的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('&lt;div&gt;Blah blah blah&lt;/div&gt;') ); // '<div>Blah blah blah</div>'

通过 key 获取相应的分组引用,然后作为对象的键。

写在最后:正则虽然很强大,但也没必要事事都用正则,有时字符串自带的API使用起来会更为简单且易于理解。



参考文献

  • 《JavaScript忍者秘籍》一书
  • 《JavaScript正则表达式迷你书(1.1版)》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码飞_CC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值