03月09, 2019

JS基础(6)——对象(6)——正则表达式

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个字符串是否含有某种子串,或者对匹配的子串进行替换、取出等操作。

6-6-1 正则表达式基本介绍

JavaScript 中,正则表达式用 RegExp 对象表示,有 2 种构建方式:字面量以及构造函数

1. 字面量构建正则表达式

正则表达式字面量写法,又叫 Perl 写法,因为 JavaScript 的正则表达式特性借鉴自 Perl。

正则表达式字面量定义为包含在一对斜杠/之间的字符,并且可以有 3 个模式修正符,如下:

let expression = /pattern/flags;

这里的pattern就是指我们的字符串模式,而后面的flags则是指带的模式修正符。JavaScript 中的正则表达式支持下面 3 个模式修正符:

g:表示全局(global)模式,即模式将被应用于所有字符串,而并非在发现第一个匹配项时立即停止。
i:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写。
m:表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。

后面我们会对这些模式修正符做详细的介绍。

2. RegExp 构造函数构建正则表达式

和普通的内置对象一样,RegExp 正则表达式对象也支持new RegExp()构造函数的形式来创建正则。

RegExp 构造函数接收两个参数:要匹配的字符串模式(pattern)可选的模式修正符(flags)。

let reg1 = /at/i;
// 等同于
let reg2 = new RegExp("at","i");

需要注意无论是字面量方式,还是构造函数方式创建的正则表达式,返回的类型都为 object

let reg1 = /at/i;
// 等同于
let reg2 = new RegExp("at","i");
console.log(typeof reg1); // object
console.log(typeof reg2); // object

--

字面量构建的正则表达式,大致可以分为 4 个部分:定界符简单字符元字符模式修正符。构造函数构建的正则表达式,则少了一个定界符,由简单字符元字符以及模式修正符组成。

简单字符在正则表达式中,就是字面的含义,比如/a/匹配 a,/b/匹配 b,这样的字符被称之为简单字符,示例如下:

let reg = /dog/;
let str = "This is a big dog";
console.log(reg.test(str)); // true

这里我们调用了正则表达式的实例方法test(),该方法如果和传入的字符串能够匹配,则返回 true,否则返回 false。

除了简单字符以外,还有一些字符,它们除了字面意思外,还有着特殊的含义,这些字符就是元字符

JavaScript 所支持的元字符如下表:

元字符 名称 匹配对象
. 点号 单个任意字符(除回车\r、换行\n、行分隔符\u2028和段分隔符\u2029外)
[] 字符组 列出的单个任意字符
[^] 排除型字符组 未列出的单个任意字符
? 问号 匹配0次或1次
* 星号 匹配0交或多次
+ 加号 匹配1次或多次
{a,b} 区间量词 匹配至少a次,最多b次
^ 脱字符 行的起始位置
$ 美元符 行的结束位置
` ` 竖线 分隔两边的任意一个表达式
() 括号 限制多选结构的范围,标注量词作用的元素,为反向引用捕获文本
\1,\2... 反向引用 匹配之前的第一、第二...组括号内的表达式匹配的文本

在后面我们会慢慢的来介绍这些元字符。

--

转义字符

转义字符(escape)表示为\字符的形式,共有以下 3 种情况:

1. 匹配元字符本身

因为元字符有特殊的含义,所以无法直接匹配。如果要匹配它们本身,则需要在它们前面加上反斜杠\

console.log(/1+1/.test("1+1")); // false
console.log(/1\+1/.test("1+1")); // true

但实际上,并非 14 个元字符都需要转义。例如,右方括号]和右花括号}可以不用写转义字符,当然写了也不会算你错,如下:

console.log(/]/.test("]")); // true
console.log(/\]/.test("]")); // true

2. 打印特殊字符

\和一些特殊字母组合起来,可以表示一些不能打印的特殊字符,具体如下表:

符号 含义
\0 NUL字符 \u0000
[\b] 匹配退格符 \u0008,不要与 \b 混淆
\t 制表符 \u0009
\n 换行符 \u000A
\v 垂直制表符 \u000B
\f 换页符 \u000C
\r 回车符 \u000D
\xnn 由十六进制数 nn 指定的拉丁字符
\uxxxx 由十六进制数 xxxx 指定的 Unicode 字符(\u4e00 - \u9fa5 代表中文)
\cX 控制字符^X,表示ctrl-[X],其中的 X 是 A - Z 之中任一个英文字母,用来匹配控制字符

这里进行一个简单的示例:

let reg = /\n/; // 匹配的字符串里面要有换行
let str = 'this is a test';
let str2 = `this is
a test,too
`;
console.log(reg.test(str)); // false
console.log(reg.test(str2)); // true

3. 搭配普通字符

\搭配上一些普通的字符,默认情况就是匹配该字符。也就是说,反斜线\被忽略了。

console.log(/\x/.test("x")); // true
console.log(/\yat/.test("yat")); // true

--

6-6-2 字符组相关元字符

从这一小节开始我们来介绍一下正则表达式中的元字符。

元字符可以分为好几个大类,例如字符组,量词等内容。其实可以这么说,学习正则表达式,主要就是学习元字符。所以,接下来我们就来一起认识一下这些在正则表达式中拥有特殊含义的符号。

1. 字符组介绍

字符组(Character Class),有的翻译成字符类或字符集。简单而言,就是指用方括号表示的一组字符,它匹配若干字符之一。

// 匹配 0 - 9 这 10 个数字之一
let reg = /[0123456789]/;
console.log(reg.test("1")); // true
console.log(reg.test("a")); // false

注:字符组中的字符排列顺序并不影响字符组的功能,出现重复字符也不会影响

[0123456789]
// 等价于
[9876543210]
// 等价于
[1234567890123456789]

--

2. 范围

正则表达式通过连字符-提供了范围表示法,可以简化字符组。

[0123456789]
// 等价于
[0-9]
[abcdefghijklmnopqrstuvwxyz]
// 等价于
[a-z]

连字符-表示的范围是根据 ASCII 编码的码值来确定的,码值小的在前,码值大的在后。所以[0-9]是合法的,而[9-0]会报错。

// 匹配 0 - 9 这 10 个数字之一
let reg1 = /[0-9]/;
console.log(reg1.test("1")); // true
let reg2 = /[9-0]/;// 报错
console.log(reg2.test("1"));
// SyntaxError: Invalid regular expression/[9-0]/:
// Range out of order in character class

在字符组中可以同时并列多个-范围。

// [0-9a-zA-Z]:匹配数字、大写字母和小写字母
// [0-9a-fA-F]:匹配数字,大、小写形式的 a - f,用来验证十六进制字符
console.log(/[0-9a-fA-F]/.test('d')); // true
console.log(/[0-9a-fA-F]/.test('x')); // false

只有在字符组内部,连字符-才是元字符,表示一个范围,否则它就只能匹配普通的连字符号。

注:如果连字符出现在字符组的开头或末尾,那么它表示的也是普通的连字符号,而不是一个范围。

// 匹配中划线
console.log(/-/.test('-')); // true
console.log(/[-]/.test('-')); // true
// 匹配 0 - 9 的数字或中划线
console.log(/[0-9]/.test('-')); // false
console.log(/[0-9-]/.test('-')); // true
console.log(/[0-9\-]/.test('-')); // true
console.log(/[-0-9]/.test('-')); // true
console.log(/[\-0-9]/.test('-')); // true

--

3. 排除

字符组的另一个类型是排除型字符组,在左方括号后紧跟一个脱字符^,表示在当前位置匹配一个没有列出的字符。例如,[^0-9]表示匹配 0 - 9 以外的字符。

// 匹配第一个是数字字符,第二个不是数字字符的字符串
console.log(/[0-9][^0-9]/.test('1e')); // true
console.log(/[0-9][^0-9]/.test('q2')); // false

脱字符^也是有位置的限定。在字符组内部的开始位置才表示排除,如果不在字符组内的开始位置,则表示普通字符。此时不用转义也可以。

console.log(/[6^9]/.test('6')); // true
console.log(/[6^9]/.test('^')); // true
console.log(/[6^9]/.test('9')); // true
// 匹配 abc 和 ^ 符号
console.log(/[a-c^]/.test('^')); // true
console.log(/[a-c\^]/.test('^')); // true
console.log(/[\^a-c]/.test('^')); // true

在字符组中,只有^-这 2 个字符可能被当做元字符,其他有元字符功能的字符都只表示其本身。

例如:在[]中的*+?等元字符都是被当作其本身。

// 部分元字符示例
// 这里会被当作是字面意思
console.log(/[1$]/.test('$')); // true
console.log(/[1|2]/.test('|')); // true
console.log(/[1?]/.test('?')); // true
console.log(/[1*]/.test('*')); // true
console.log(/[1+]/.test('+')); // true
console.log(/[1.]/.test('.')); // true

--

4. 简记

[0-9][a-z]等字符组,可以很方便地表示数字字符和小写字母字符。对于这类常用字符组,正则表达式提供了更简单的记法,这就是字符组简记(shorthands)。

常见的字符组简记有\d\w\s。其中 d 表示(digit)数字,w 表示(word)单词,s 表示(space)空白。

正则表达式也提供了对应的排除型字符组的简记法:\D\W\S。字母完全相同,只是改为大写。它们和小写的简记符在含义上刚好相反。

\d:数字,等同于 [0-9]
\D:非数字,等同于 [^0-9]
\s:空白字符,等同于 [\f\n\r\t\u000B\u0020\u00A0\u2028\u2029]
\S:非空白字符,等同于 [^\f\n\r\t\u000B\u0020\u00A0\u2028\u2029]
\w:字母、数字、下划线,等同于 [0-9A-Za-z_](汉字不属于\w\W:非字母、数字、下划线,等同于 [^0-9A-Za-z_]

注意:\w 不仅包括字母、数字,还包括下划线。在进行数字验证时,只允许输入字母和数字时,不可以使用 \w,因为还包含了下划线。所以应该使用[0-9a-zA-Z]

--

5. 任意字符

经常有人存在一个误区,认为点可以代表任意字符,其实并不是。点号.代表除回车(\r),换行(\n),行分隔符(\u2028)和段分隔符(\u2029)以外的任意字符。

如果要匹配任意字符,可以妥善的利用互补属性来得到这一效果。比如,[\s\S][\w\W][\d\D]都可以表示任意字符。

// 匹配任意字符
console.log(/./.test('\r')); // false
console.log(/[\s\S]/.test('\r')); // true
console.log(/[\d\D]/.test('\r')); // true

--

6-6-3 量词相关元字符

根据字符组的介绍,可以用字符组[0-9]\d来匹配单个数字字符,但是如果要匹配多个数字字符,则显得有点不太方便,如下:

// 表示邮政编码 6 位数字
/[0-9][0-9][0-9][0-9][0-9][0-9]/;
// 等价于
/\d\d\d\d\d\d/;

正则表达式提供了量词,用来设定某个模式出现的次数

{n}: 匹配 n 次。
{n,m}: 匹配至少 n 次,最多 m 次。
{n,}: 匹配至少 n 次。
?: 相当于 {0,1}
*: 相当于 {0,}
+: 相当于 {1,}

举个例子,在美国英语和英国英语里面有些单词的写法不一样,例如 traveler 和 traveller,favor 和 favour,color 和 colour,写出来的正则表达式如下:

// 同时匹配美国英语和英国英语单词
/travell?er/;
/favou?r/;
/colou?r/;

超文本传输协议名有 http 和 https 两种,其正则表达式可写作:

/https?/;

--

贪婪模式(扩展)

默认情况下,量词采用的是贪婪模式(greedy quantifier),即尽可能多的进行匹配,如下:

let reg = /a+/;
let str = "aaabbcc";
console.log(reg.exec(str));
// [ 'aaa', index: 0, input: 'aaabbcc' ]

这里我们使用了正则表达式的另外一个常用的实例方法exec()。该方法会返回一个数组,数组里面有匹配上的字符匹配项开始的索引值以及原始的字符串等更加详细的信息。

懒惰模式(扩展)

懒惰模式(lazy quantifier)和贪婪模式相对应,在量词后添加问号?,表示尽可能少的匹配,一旦条件满足就不再往下匹配。

{n}?: 匹配 n 次。
{n,m}?: 匹配至少 n 次,最多 m 次。
{n,}?: 匹配至少 n 次。
??: 相当于 {0,1}
*?: 相当于 {0,}
+?: 相当于 {1,}

示例如下:

let reg = /a+?/;
let str = "aaabbcc";
console.log(reg.exec(str));
// [ 'a', index: 0, input: 'aaabbcc' ]

--

6-6-4 括号相关元字符

括号有两个功能,分别是分组引用。具体而言,用于限定量词或选择项的作用范围,也可以用于捕获文本并进行引用或反向引用。

1. 分组

量词控制之前元素的出现次数,而这个元素可以是一个字符,也可以是一个字符组,或者是一个表达式。如果把一个表达式用括号包围起来,这个元素就是括号里的表达式,被称为子表达式。

例如:如果希望字符串ab重复出现 2 次,应该写为(ab){2},而如果写为ab{2},则{2}只限定 b,如下:

console.log(/ab{2}/.test("abab")); // false
console.log(/(ab){2}/.test("abab")); // true

身份证长度有 15 位和 18 位两种,如果只匹配长度,可能会想当然地写成\d{15,18},实际上这是错误的,因为它包括 15、16、17、18 这 4 种长度。因此,正确的写法如下:

/\d{15}(\d{3})?/

--

2. 捕获

括号不仅可以对元素进行分组,还会保存每个分组匹配的文本,等到匹配完成后,引用捕获的内容。因为捕获了文本,这种功能叫捕获分组。

比如,要匹配诸如 2016 - 06 - 23 这样的日期字符串:

/(\d{4})-(\d{2})-(\d{2})/

与以往不同的是,年、月、日这三个数值被括号括起来了,从左到右的第 1 个、第 2 个、第 3 个括号,分别代表第 1、2、3 个捕获组。

ECMAScript 有 9 个用于存储捕获组的构造函数属性,如果使用的是test()方法,那么通过正则对象的$1-$9属性来进行访问到。

// RegExp.$1\RegExp.$2\RegExp.$3…… 到 RegExp.$9
// 分别用于存储第一、第二……第九个匹配的捕获组。
// 在调用 exec() 或 test() 方法时,这些属性会被自动填充
console.log(/(\d{4})-(\d{2})-(\d{2})/.test('2016-06-23'));//true
console.log(RegExp.$1);//2016
console.log(RegExp.$2);//06
console.log(RegExp.$3);//23
console.log(RegExp.$4);//""

再例如:

let reg = /(a+)(b*)xj/;
let str = "aabbbxj";
console.log(reg.test(str));//true
console.log("$1的值:",RegExp.$1);//$1的值: aa
console.log("$2的值:",RegExp.$2);//$2的值: bbb
console.log("$3的值:",RegExp.$3);//$3的值:

exec()方法是专门为捕获组而设计的,返回的数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串,如果要获取,那么可以通过指定数组的下标来进行获取,如下:

let reg = /(\d{4})-(\d{2})-(\d{2})/;
let str = "2017-03-21";
let i = reg.exec(str);
console.log(i);
/*
[ '2017-03-21',
  '2017',
  '03',
  '21',
  index: 0,
  input: '2017-03-21',
  groups: undefined ]
*/
console.log(i[1]); // 2017
console.log(i[2]); // 03
console.log(i[3]); // 21

捕获分组捕获的文本,不仅可以用于数据提取,也可以用于替换。字符串的replace()方法就是用于进行数据替换的,该方法接收两个参数,第一个参数为待查找的内容,而第二个参数为替换的内容。

let str = "2017-12-12";
console.log(str.replace(/-/g,"."));
// 2017.12.12

注:这里书写正则表达式的时候必须要写上模式修正符g,这样才能够进行全局匹配。

replace()方法中也可以引用分组,形式还是用$num,其中 num 是对应分组的编号。

//把2017-03-23的形式变成03-23-2017
let reg = /(\d{4})-(\d{2})-(\d{2})/;
let str = "2017-03-23";
console.log(str.replace(reg,"$2-$3-$1"));
//03-23-2017

--

3. 反向引用(扩展)

英文中不少单词都有重叠出现的字母,如 shoot 或 beep。若想检查某个单词是否包含重叠出现的字母,则需要引入反向引用(back-reference)。

反向引用允许在正则表达式内部引用之前捕获分组匹配到的文本,形式为\num,其中 num 表示所引用分组的编号。

// 重复字母
let reg = /([a-z])\1/;
console.log(reg.test('aa')); // true
console.log(reg.test('ab')); // false

接下来我们再来看一个反向引用的例子

let reg = /(ab)(cd)\1xj/;
let str = "abcdabxj";
console.log(reg.test(str)); // true

如果我们要跳过某一个子表达式,那么可以使用?:来跳过(后面会提到的非捕获)。

let reg = /(?:ab)(cd)\1xj/;
let str = "abcdabxj";
let str2 = "abcdcdxj";
console.log(reg.test(str)); // false
console.log(reg.test(str2)); // true

反向引用可以用于建立前后联系。例如 HTML 标签的开始标签和结束标签是对应着的,这个时候我们就可以使用反向引用。

// 开始标签
// <([^>]+)>
// 标签内容
// [\s\S]*?
// 匹配成对的标签
// /<([^>]+)>[\s\S]*?<\/\1>/
console.log(/<([^>]+)>[\s\S]*?<\/\1>/.test('<a>123</a>')); // true
console.log(/<([^>]+)>[\s\S]*?<\/\1>/.test('<a>123</b>')); // false

--

4. 非捕获(扩展)

除了捕获分组,正则表达式还提供了非捕获分组(non-capturing group),以?:的形式表示,它只用于限定作用范围,而不捕获任何文本。

比如,要匹配abcabc这个字符,一般地,可以写为(abc){2},但由于并不需要捕获文本,只是限定了量词的作用范围,所以应该写为(?:abc){2}

// 限定 abc 要出现 2 次 , 并且会对内容进行捕获
console.log(/(abc){2}/.exec('abcabc')); 
// [ 'abcabc', 'abc', index: 0, input: 'abcabc', groups: undefined ]

// 同样是限定 abc 要出现 2 次 , 但是不会对内容进行捕获
console.log(/(?:abc){2}/.exec('abcabc')); 
// [ 'abcabc', index: 0, input: 'abcabc', groups: undefined ]

由于非捕获分组不捕获文本,对应地,也就没有捕获组编号

console.log(/(abc){2}/.test('abcabc')); // true
console.log(RegExp.$1); // abc
console.log(/(?:abc){2}/.test('abcabc')); // true
console.log(RegExp.$1); // ""

非捕获分组也不会被用于反向引用

console.log(/(?:123)\1/.test('123123')); // false
console.log(/(?:123)\1/.test('123\1')); // true
console.log(/(123)\1/.test('123123')); // true

捕获分组和非捕获分组可以在一个正则表达式中同时出现

console.log(/(\d)(\d)(?:\d)(\d)(\d)/.exec('12345'));
// [ '12345', '1', '2', '4', '5', index: 0, input: '12345' ]

--

6-6-5 选择相关元字符

竖线|在正则表达式中表示或(OR)关系的选择,以竖线|分隔开的多个子表达式也叫选择分支或选择项。在一个选择结构中,选择分支的数目没有限制。

在选择结构中,竖线|用来分隔选择项,而括号()用来规定整个选择结构的范围。如果没有出现括号,则将整个表达式视为一个选择结构。

选择项的尝试匹配次序是从左到右,直到发现了匹配项,如果某个选择项匹配就忽略右侧其他选择项,如果所有子选择项都不匹配,则整个选择结构匹配失败。

console.log(/12|23|34/.exec('1')); // null
console.log(/12|23|34/.exec('12')); // [ '12', index: 0, input: '12' ]
console.log(/12|23|34/.exec('23')); // [ '23', index: 0, input: '23' ]
console.log(/12|23|34/.exec('2334')); // [ '23', index: 0, input: '2334' ]

在选择结构中,应该尽量避免选择分支中存在重复匹配,因为这样会大大增加回溯的计算量。

// 不良的选择结构
a|[ab]
[0-9]|\w

--

6-6-6 断言相关元字符(扩展)

在正则表达式中,有些结构并不真正匹配文本,而只负责判断在某个位置左/右侧是否符合要求,这种结构被称为断言(assertion),也称为锚点(anchor),常见的断言有3种:单词边界、行开头结尾、环视。

1. 单词边界

在文本处理中可能会经常进行单词替换,比如把 row 替换成 line。但是,如果直接替换,不仅所有单词 row 都被替换成 line,单词内部的 row 也会被替换成 line。要想解决这个问题,必须有办法确定单词 row,而不是字符串 row。

为了解决这类问题,正则表达式提供了专用的单词边界(word boundary),记为\b,它匹配的是'单词边界'位置,而不是字符。\b匹配的是一边是单词字符\w,一边是非单词字符\W的位置。

\b对应的还有\B,表示非单词边界,但实际上\B很少使用。

let reg = /\bis\b/;
let str = "this is a test";
console.log(reg.exec(str));
// [ 'is', index: 5, input: 'this is a test' ]
console.log(reg.exec("is"));
// [ 'is', index: 0, input: 'is' ]

--

2. 起始结束

常见的断言还有^$,它们分别匹配字符串的开始位置和结束位置,所以可以用来判断整个字符串能否由表达式匹配。

let reg = /^\d\w*/;
let str1 = "1asd";
let str2 = "qwe2";
console.log(reg.test(str1)); // true
console.log(reg.test(str2)); // false

^$的常用功能是删除字符串首尾多余的空白,类似于字符串对象的trim()方法。

let fnTrim = function(str){
    return str.replace(/^\s+|\s+$/,'')
}  
console.log(fnTrim('      hello world   ')); // 'hello world'

--

3. 环视

环视,在不同的地方又称之为零宽断言,简称断言。环视强调的是它所在的位置,前面或者后面,必须满足环视表达式中的匹配情况,才能匹配成功。

环视可以认为是虚拟加入到它所在位置的附加判断条件,并不消耗正则的匹配字符。

环视可分为正序环视逆序环视,而 JavaScript 只支持正序环视,相当于只支持向前看,不支持往回看。而正序环视又分为肯定正序环视否定正序环视

肯定正序环视的记法为?=正则表达式,表示所在的位置右侧能够匹配到该正则。否定正序环视的记忆法是?!正则表达式,表示所在的位置右侧位置不能匹配到该正则。

举一个例子:

let reg = /(?=A)[A-Z]/;
/*
匹配以下的字符串:
    1. (?=A) 所在的位置,后面是 A 
    2. 表达式 [A-Z] 匹配 A-Z 中任意一个字母
*/
console.log(reg.test('ABC')); // true
console.log(reg.test('BBC')); // fasle

从例子可以看出,从左到右,正则分别匹配了环视(?=A)[A-Z],由于环视不消耗正则的匹配字符,因此,[A-Z]还能对 A 进行匹配,并得到结果。

再例如,我要匹配文件名以.js结束的文件:

let reg = /\w+(?=\.js)/;
let str = "test.js";
console.log(reg.test(str)); // true

如果在?=后面想要继续书写字符,那么必须先写一个?=后面的字符,如下:

let reg = /a(?=b)bc/; // 正则 a 后面必须书写 b
let str1 = "abc";
let str2 = "acc";
let str3 = "abb";
let str4 = "abbc";
let str5 = "abcc";
console.log(reg.test(str1)); // true
console.log(reg.test(str2)); // false
console.log(reg.test(str3)); // false
console.log(reg.test(str4)); // false
console.log(reg.test(str5)); // true

注意括号后面的地方第一个字符必须写成 b,因为?=位置后面就写的 b,这样才能继续在后面书写字符。

如果像下面这样写,那么是不可能匹配上的。

let reg = /a(?=b)c/; 
let str1 = "abc";
let str2 = "acc";
let str3 = "abb";
let str4 = "abbc";
let str5 = "abcc";
console.log(reg.test(str1)); // false
console.log(reg.test(str2)); // false
console.log(reg.test(str3)); // false
console.log(reg.test(str4)); // false
console.log(reg.test(str5)); // false

接下来我们来看?!,其实就和?=刚好相反,后面的字符不能是某一个字符,如下:

let reg = /a(?!b)c/;
let str = "ac";
console.log(reg.test(str)); // true

同样需要注意的是,既然指定了后面不能是某一个字符,那么如果想要往后面继续书写字符,首先需要写一个不是?!后面的,如下:

let reg = /a(?!b)b/;
let str1 = "axb";
let str2 = "abb";
let str3 = "acb";
console.log(reg.test(str1)); // false
console.log(reg.test(str2)); // false
console.log(reg.test(str3)); // false

注意:环视虽然也用到括号,却与捕获型分组编号无关;但如果环视结构出现捕获型括号,则会影响分组

console.log(/ab(?=cd)/.exec("abcd"));
// [ 'ab', index: 0, input: 'abcd' ]
console.log(/ab(?=(cd))/.exec("abcd"));
// [ 'ab', 'cd', index: 0, input: 'abcd' ]

关于环视更多的介绍,可以参照:https://www.cnblogs.com/tsql/p/5860889.html

--

6-6-7 模式修正符

匹配模式(match mode)又被称之为模式修正符。指的是匹配时使用的规则。设置特定的模式,可能会改变对正则表达式的识别。

前面已经介绍过创建正则表达式对象时,可以设置mig这三个标志,分别对应多行模式、不区分大小模式和全局模式三种。

i:默认地,正则表达式是区分大小写的,通过设置标志i,可以忽略大小写(ignore case)。

console.log(/ab/.test("aB")); // false
console.log(/ab/i.test("aB")); // true

--

m:默认地,正则表达式中的^$匹配的是整个字符串的起始位置和结束位置,而通过设置标志m,开启多行模式,它们也能匹配字符串内部某一行文本的起始位置和结束位置。

console.log(/^b/.test('a\nb')); // false
console.log(/^b/m.test('a\nb')); // true

--

g:默认地,第一次匹配成功后,正则对象就停止向下匹配了。g修饰符表示全局匹配(global),设置g标志后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。

console.log('1a,2a,3a'.replace(/a/,'b')); // 1b,2a,3a
console.log('1a,2a,3a'.replace(/a/g,'b')); // 1b,2b,3b

--

6-6-8 优先级

正则表达式千变万化,但是大多都是由之前介绍过的字符组、括号、量词等基本结构组合而成的。这些元字符,和运算符一样拥有一个优先级关系,如下:

// 从上到下,优先级逐渐降低
\                            转义符
() (?!) (?=) []              括号、字符组、环视
* + ? {n} {n,} {n,m}         量词
^ $                          起始结束位置
|                            选择

由于括号的用途之一就是为量词限定作用范围,所以优先级比量词高

console.log(/ab{2}/.test('abab')); // false
console.log(/(ab){2}/.test('abab')); // true

注意:选择符|的优先级最低,比起始和结束位置都要低

console.log(/^ab|cd$/.test('abc')); // true
console.log(/^(ab|cd)$/.test('abc')); // false
console.log(/^(ab|cd)$/.test('ab')); // true
console.log(/^(ab|cd)$/.test('cd')); // true

--

6-6-9 局限性

   尽管 JavaScript 中的正则表达式功能比较完备,但与其他语言相比,缺少某些特性。下面列出了 JavaScript 的正则表达式所不支持的特性。

  • POSIX 字符组(只支持普通字符组和排除型字符组)
  • Unicode支持(只支持单个Unicode字符)
  • 匹配字符串开始和结尾的\A和\Z锚(只支持^和$)
  • 逆序环视(只支持顺序环视)
  • 命名分组(只支持0-9编号的捕获组)
  • 单行模式和注释模式(只支持m、i、g)
  • 模式作用范围
  • 纯文本模式

--

6-6-10 正则表达式属性和方法

前面有提到,当我们使用typeof运算符来打印正则表达式的类型时,返回的是object,这说明正则表达式在 JavaScript 中也是一种对象。那么既然是对象,就应该有相应的属性和方法。

1. 实例属性    每个 RegExp 实例对象都包含如下 5 个属性:

global:布尔值,表示是否设置了 g 标志。
ignoreCase:布尔值,表示是否设置了 i 标志。
lastIndex:整数,表示开始搜索下一个匹配项的字符位置,从 0 算起。
multiline:布尔值,表示是否设置了标志 m。
source:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回。

示例如下:

let reg = /test/gi;
console.log(reg.global); // true
console.log(reg.ignoreCase); // true
console.log(reg.multiline); // false
console.log(reg.lastIndex); // 0
console.log(reg.source); // test

RegExp 的exec()test()函数,如果设定了全局模式g,正则表达式的匹配就会从lastIndex的位置开始,并且在每次匹配成功之后重新设定lastIndex,继续往后匹配。这样,就可以在字符串中重复迭代,依次寻找各个匹配结果。

但是,如果需要对不同字符串调用同一个RegExp的exec()test()方法,这个变量也可能会带来意料之外的匹配结果,所以在更换字符串时,要显式地将RegExp的lastIndex置为 0。

// exec()方法以数组形式返回匹配项
let reg = /\w/g;
let str = "abcd";
console.log(reg.lastIndex); // 0
console.log(reg.exec(str)); // [ 'a', index: 0, input: 'abcd' ]
console.log(reg.lastIndex); // 1
console.log(reg.exec(str)); // [ 'b', index: 1, input: 'abcd' ]
console.log(reg.lastIndex); // 2
console.log(reg.exec(str)); // [ 'c', index: 2, input: 'abcd' ]
console.log(reg.lastIndex); // 3
console.log(reg.exec(str)); // [ 'd', index: 3, input: 'abcd' ]
console.log(reg.lastIndex); // 4
console.log(reg.exec(str)); // null
console.log(reg.lastIndex); // 0
console.log(reg.exec(str)); // [ 'a', index: 0, input: 'abcd' ]

--

2. 构造函数属性(扩展)

RegExp 构造函数属性被看成静态属性,这些属性基于所执行的最近一次正则表达式操作而变化。

有 2 种方式访问它们,即长属性名和短属性名。短属性名大都不是有效的 ECMAScript 标识符,所以必须通过方括号语法来访问它们。

长属性名         短属性名           说明
input             $_                最近一次要匹配的字符串
lastMatch         $&                最近一次的匹配项
lastParen         $+                最近一次匹配的捕获组
leftContext       $`                input字符串中lastMatch之前的文本
multiline         $*                布尔值,表示是否所有表达式都使用多行模式
rightContext      $'                input字符串中lastMatch之后的文本

使用这些属性,可以从exec()方法或test()方法执行的操作中提取出更具体的信息

// test()用于测试一个字符串是否匹配某个正则表达式,并返回一个布尔值
let text = 'this has been a short summer';
let pattern = /(.)hort/g;
if(pattern.test(text)){
    console.log(RegExp.input); // 'this has been a short summer'
    console.log(RegExp.leftContext); // 'this has been a '
    console.log(RegExp.rightContext); // ' summer'
    console.log(RegExp.lastMatch); // 'short'
    console.log(RegExp.lastParen); // 's'
    console.log(RegExp.multiline); // undefined
    console.log(RegExp['$_']); // 'this has been a short summer'
    console.log(RegExp['$`']); // 'this has been a '
    console.log(RegExp["$'"]); // ' summer'
    console.log(RegExp['$&']); // 'short'
    console.log(RegExp['$+']); // 's'
    console.log(RegExp['$*']); // undefined        
}

JavaScript 有 9 个用于存储捕获组的构造函数属性,在调用exec()test()方法时,这些属性会被自动填充。

注:理论上,应该保存整个表达式匹配文本的 RegExp.$0 并不存在,值为 undefined。

// RegExp.$1,RegExp.$2,RegExp.$3...到 RegExp.$9 分别用于存储第一、第二……第九个匹配的捕获组
let text = 'this has been a short summer';
let pattern = /(..)or(.)/g;
if(pattern.test(text)){
    console.log(RegExp.$1);//sh
    console.log(RegExp.$2);//t
}

--

3. 实例方法    RegExp 对象的实例方法共 5 个,分为两类。包括toString()toLocalString()valueOf()这3种对象通用方法和test()exec()这2种正则匹配方法

对象通用方法(扩展)

RegExp 对象继承了 Object 对象的toString()toLocaleString()valueOf()这 3 个通用方法。

toString():toString()方法返回正则表达式的字面量。
toLocaleString():toLocaleString()方法返回正则表达式的字面量。
valueOf():valueOf()方法返回返回正则表达式对象本身。

注意:不论正则表达式的创建方式是哪种,这三个方法都只返回其字面量形式

let pattern1 = new RegExp('[bc]at','gi');
console.log(pattern1.toString()); // '/[bc]at/gi'
console.log(pattern1.toLocaleString()); // '/[bc]at/gi'
console.log(pattern1.valueOf()); // /[bc]at/gi

let pattern2 = /[bc]at/gi;
console.log(pattern2.toString()); // '/[bc]at/gi'
console.log(pattern2.toLocaleString()); // '[bc]at/gi'
console.log(pattern2.valueOf()); // /[bc]at/gi

--

正则匹配方法

正则表达式 RegExp 对象的正则匹配方法只有两个:分别是test()exec()

test()方法用来测试在字符串中是否能够找到符合正则要求的字符。接收一个字符串参数,匹配时返回 true,否则返回 false。

let reg = /test/;
let str = "this is a test";
console.log(reg.test(str)); // true

在调用test()方法时,会造成 RegExp 对象的lastIndex属性的变化。

如果指定了全局模式,每次执行test()方法时,都会从字符串中的lastIndex偏移值开始尝试匹配。所以用同一个 RegExp 多次验证相同字符串时,必须在每次调用之后,将lastIndex值置为 0。

let pattern = /^\d{4}-\d{2}-\d{2}$/g;
console.log(pattern.test('2016-06-23')); // true
console.log(pattern.test('2016-06-23')); // false
// 正确的做法应该是在验证不同字符串前,先将 lastIndex 重置为 0
let pattern = /^\d{4}-\d{2}-\d{2}$/g;
console.log(pattern.test('2016-06-23'));// true
pattern.lastIndex = 0;
console.log(pattern.test('2016-06-23'));// true

前面有介绍过,JavaScript有 9 个用于存储捕获组的构造函数属性,在调用exec()test()方法时,这些属性会被自动填充。

注意:理论上,应该保存整个表达式匹配文本的RegExp.$0并不存在,值为undefined

if(/^(\d{4})-(\d{2})-(\d{2})$/.test('2016-06-23')){
    console.log(RegExp.$1); // '2016'
    console.log(RegExp.$2); // '06'
    console.log(RegExp.$3); // '23'
    console.log(RegExp.$0); // undefined
}

exec()方法专门为捕获组而设计,接受一个参数,即要应用模式的字符串。然后返回包含匹配项信息的数组,在没有匹配项的情况下返回null

在匹配项数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串,如果模式中没有捕获组,则该数组只包含一项。

返回的数组包含两个额外的属性:indexinputindex表示匹配项在字符串的位置,input表示应用正则表达式的字符串。

let text = 'mom and dad and baby and others';
let pattern = /mom( and dad( and baby)?)?/gi;
let matches = pattern.exec(text);
console.log(pattern);
console.log(matches);
/*
    /mom( and dad( and baby)?)?/gi
    [ 'mom and dad and baby',
    ' and dad and baby',
    ' and baby',
    index: 0,
    input: 'mom and dad and baby and others',
    groups: undefined ]
 */

对于exec()方法而言,即使在模式中设置了全局标志g,它每次也只会返回一个匹配项。在不设置全局标志的情况下,在同一个字符串上多次调用exec(),将始终返回第一个匹配项的信息。

let text = 'cat,bat,sat,fat';
let pattern1 = /.at/;
let matches = pattern1.exec(text);
console.log(pattern1,matches);
/* 
 *  /.at/ [ 'cat', index: 0, input: 'cat,bat,sat,fat' ] 
 */

let text = 'cat,bat,sat,fat';
matches = pattern1.exec(text);    
console.log(pattern1,matches);    
/*
 *  /.at/ [ 'cat', index: 0, input: 'cat,bat,sat,fat' ]
 */

而在设置全局标志的情况下,每次调用exec()都会在字符串中继续查找新匹配项。

let text = 'cat,bat,sat,fat';
let pattern2 = /.at/g;
let matches = pattern2.exec(text);
console.log(pattern2,matches);    
/* 
 *  /.at/g [ 'cat', index: 0, input: 'cat,bat,sat,fat' ]
 */  

let text = 'cat,bat,sat,fat';
matches = pattern2.exec(text);
console.log(pattern2,matches);    
/* 
 *  /.at/g [ 'bat', index: 4, input: 'cat,bat,sat,fat' ]
 */

注意:用exec()方法找出匹配的所有位置和所有值。

let string = 'j1h342jg24g234j 3g24j1';
let pattern = /\d/g;
let valueArray = []; // 值
let indexArray = []; // 位置
let temp;
while ((temp = pattern.exec(string)) != null) {
    valueArray.push(temp[0]);
    indexArray.push(temp.index);
}
console.log(valueArray);
console.log(indexArray);
// [ '1', '3', '4', '2', '2', '4', '2', '3', '4', '3', '2', '4', '1' ]
// [ 1, 3, 4, 5, 8, 9, 11, 12, 13, 16, 18, 19, 21 ]

总结

  1. JavaScript 里面对象是一组键值的集合。

  2. 访问对象的属性的方式有 3 种:点访问法,中括号访问法和 symbol 访问法。

  3. symbol 访问法是 ECMAScript 6 新添加的对象属性访问方式,主要就是为了避免属性名冲突的。

  4. 对象也可以进行嵌套和解构。

  5. 使用对象作为函数参数的技术被称之为命名参数,好处在于传参时参数顺序可以随意。

  6. this 的指向默认可以分为 2 种:指向全局对象和指向当前对象。

  7. 使用对象字面量来为一组函数创建一个命名空间可以解决命名冲突的问题。

  8. JSON 是一种轻量级的数据存储格式,它在人、机可读性方面达到了一个最佳的临界点。

  9. Math 对象是一个数学对象,提供了很多有用的属性和方法供我们使用。

  10. Date 对象主要用于处理和时间相关的操作。

  11. 正则表达式描述了一种字符串匹配的模式,可以用来检查一个字符串是否含有某种子串,或者对匹配的子串进行替换、取出等操作。

本文链接:http://www.yanhongzhi.com/post/js-basis-22.html

-- EOF --

Comments