正则可视化工具
正则可视化工具-regexper
正则在线测试工具-regex101

勘误:

2.4.2.5. 格式化
$ 1888.00 -> $ 1,888.00

3.3.1. 括号嵌套怎么办?
最后的是 \4,找到第3个开括号 -> 最后的是 \4,找到第4个开括号

3.5.6. 匹配成对标签
其中开标签 <[\^>]+> 改成 <([^>]+)> -> 其中开标签<[^>]+>改成<([^>]+)>

5.1. 结构和操作符
在 (c|de*) 中,注意其中的量词 ,因此 e 是一个整体结构—>按照原文:这里的因此应该不标红。

6.3.2. 匹配浮点数
因此整个正则是这三者的或的关系,提取公众部分后是:—->公共部分

[《JavaScript 正则表达式迷你书》问世了!](https://zhuanlan.zhihu.com/p/29707385?utm_source=com.daimajia.gold&utm_medium=social)

记录一些学到的

1. 第一章 正则表达式字符匹配攻略

1.2.1. 范围表示法

因为连字符有特殊用途,那么要匹配 “a”、”-“、”z” 这三者中任意一个字符,该怎么做呢?
不能写成 [a-z],因为其表示小写字符中的任何一个字符。
可以写成如下的方式:[-az] 或 [az-] 或 [a-z]。
即要么放在开头,要么放在结尾,要么转义。总之不会让引擎认为是范围表示法就行了。

1.2.3. 常见的简写形式

如果要匹配任意字符怎么办?可以使用 [\d\D]、[\w\W]、[\s\S] 和 [^] 中任何的一个。

1.3.2. 贪婪匹配与惰性匹配

其中 /\d{2,5}?/ 表示,虽然 2 到 5 次都行,当 2 个就够的时候,就不再往下尝试了。

1.4. 多选分支

但有个事实我们应该注意,比如我用 /good|goodbye/,去匹配 “goodbye” 字符串时,结果是 “good”:

1
2
3
4
var regex = /good|goodbye/g;
var string = "goodbye";
console.log( string.match(regex) );
// => ["good"]
1
2
3
4
var regex = /goodbye|good/g;
var string = "goodbye";
console.log( string.match(regex) );
// => ["goodbye"]

也就是说,分支结构也是惰性的,即当前面的匹配上了,后面的就不再尝试了。

1.5.1. 匹配 16 进制颜色值

1
2
3
4
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
var string = "#ffbbad #Fc01DF #FFF #ffE";
console.log( string.match(regex) );
// => ["#ffbbad", "#Fc01DF", "#FFF", "#ffE"]

1.5.2. 匹配时间

1
2
3
4
5
var regex = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/;
console.log( regex.test("23:59") );
console.log( regex.test("02:07") );
// => true
// => true

如果也要求匹配 “7:9”,也就是说时分前面的 “0” 可以省略。
此时正则变成:

1
2
3
4
5
6
7
var regex = /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/;
console.log( regex.test("23:59") );
console.log( regex.test("02:07") );
console.log( regex.test("7:9") );
// => true
// => true
// => true

1.5.3. 匹配日期

1
2
3
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
console.log( regex.test("2017-06-10") );
// => true

1.5.4. window 操作系统文件路径

1
2
3
4
5
6
7
8
9
var regex = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/;
console.log( regex.test("F:\\study\\javascript\\regex\\regular expression.pdf") );
console.log( regex.test("F:\\study\\javascript\\regex\\") );
console.log( regex.test("F:\\study\\javascript") );
console.log( regex.test("F:\\") );
// => true
// => true
// => true
// => true

1.5.5. 匹配 id

1
2
3
4
5
// 解决之道,可以使用惰性匹配:
var regex = /id=".*?"/
var string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
// => id="container"

当然,这样也会有个问题。效率比较低,因为其匹配原理会涉及到“回溯”这个概念(这里也只是顺便提一下,第四章会详细说明)。可以优化如下:

1
2
3
4
5
var regex = /id="[^"]*"/
var string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
// => id="container"
// 思考:id里有特殊字符呢。

2. 第二章 正则表达式位置匹配攻略

正则表达式是匹配模式,要么匹配字符,要么匹配位置。请记住这句话。

2.2. 如何匹配位置呢?

在 ES5 中,共有 6 个锚:
^、$、\b、\B、(?=p)、(?!p)

2.2.1. ^ 和 $

^(脱字符)匹配开头,在多行匹配中匹配行开头。
$(美元符号)匹配结尾,在多行匹配中匹配行结尾。

2.2.2. \b 和 \B

\b 是单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置。
比如考察文件名 “[JS] Lesson_01.mp4” 中的 \b,如下:

1
2
3
var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result);
// => "[#JS#] #Lesson_01#.#mp4#"

\B 就是 \b 的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉 \b,剩下的都是 \B 的。
具体说来就是 \w 与 \w、 \W 与 \W、^ 与 \W,\W 与 $ 之间的位置。
比如上面的例子,把所有 \B 替换成 “#”:

1
2
3
var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
console.log(result);
// => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"

2.2.3. (?=p) 和 (?!p)

(?=p),其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p。
比如 (?=l),表示 “l” 字符前面的位置,例如:

1
2
3
var result = "hello".replace(/(?=l)/g, '#');
console.log(result);
// => "he#l#lo"

而 (?!p) 就是 (?=p) 的反面意思,比如:

1
2
3
var result = "hello".replace(/(?!l)/g, '#');
console.log(result);
// => "#h#ell#o#"

二者的学名分别是 positive lookahead 和 negative lookahead。
中文翻译分别是正向先行断言和负向先行断言。
ES5 之后的版本,会支持 positive lookbehind 和 negative lookbehind。
具体是 (?<=p) 和 (?<!p)。
也有书上把这四个东西,翻译成环视,即看看右边和看看左边。
但一般书上,没有很好强调这四者是个位置。
比如 (?=p),一般都理解成:要求接下来的字符与 p 匹配,但不能包括 p 匹配的那些字符。
而在本人看来,(?=p) 就与 ^ 一样好理解,就是 p 前面的那个位置。

2.3. 位置的特性

对于位置的理解,我们可以理解成空字符 “”。
也等价于:

1
"hello" == "" + "" + "hello"

因此,把 /^hello$/ 写成 /^^hello$$$/,是没有任何问题的:

1
2
3
var result = /^^hello$$$/.test("hello");
console.log(result);
// => true

甚至可以写成更复杂的:

1
2
3
var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello");
console.log(result);
// => true

也就是说字符之间的位置,可以写成多个。

TIP 把位置理解空字符,是对位置非常有效的理解方式。

2.4. 相关案例

2.4.1. 不匹配任何东西的正则

1
/.^/

2.4.2 数字的千位分隔符表示法

1
2
3
4
5
6
7
8
比如把 "12345678",变成 "12,345,678"。
var regex = /(?!^)(?=(\d{3})+$)/g;
var result = "12345678".replace(regex, ',')
console.log(result);
// => "12,345,678"
result = "123456789".replace(regex, ',');
console.log(result);
// => "123,456,789"

github上 demo 5.2、添加千分位

千位分隔符的完整攻略

2.4.2.4. 支持其他形式

1
2
3
4
5
var string = "12345678 123456789",
regex = /(?!\b)(?=(\d{3})+\b)/g;
var result = string.replace(regex, ',')
console.log(result);
// => "12,345,678 123,456,789"

其中 (?!\b) 怎么理解呢?
要求当前是一个位置,但不是 \b 前面的位置,其实 (?!\b) 说的就是 \B。
因此最终正则变成了:/\B(?=(\d{3})+\b)/g。

2.4.2.5. 格式化

1
2
3
4
5
function format (num) {
return num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g, ",").replace(/^/, "$$ ");
};
console.log( format(1888) );
// => "$ 1,888.00"

2.4.3. 验证密码问题(TODO Read again)

密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符。
此题,如果写成多个正则来判断,比较容易。但要写成一个正则就比较困难。
那么,我们就来挑战一下。看看我们对位置的理解是否深刻。
((?=p) 就与 ^ 一样好理解,就是 p 前面的那个位置。)

// 扩展: 密码中必须包含字母、数字、特称字符,至少8个字符,最多30个字符。

Array.apply(null, {length: 10})和Array(10)有什么区别?

关于apply,Array.apply(null, {length:5})怎么理解

第三章 正则表达式括号的作用

NOTE match 返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的
内容,然后是匹配下标,最后是输入的文本。另外,正则表达式是否有修饰符 g,match
返回的数组格式是不一样的。

3.2.1. 提取数据

1
2
3
4
5
6
// 提取日期
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( string.match(regex) );
console.log( regex.exec(string) ); // 这里的结果一样。
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

同时,也可以使用构造函数的全局属性 $1 至 $9 来获取:

1
2
3
4
5
6
7
8
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
regex.test(string); // 正则操作即可,例如
//regex.exec(string);
//string.match(regex);
console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"

3.2.2. 替换

比如,想把 yyyy-mm-dd 格式,替换成 mm/dd/yyyy 怎么做?

1
2
3
4
5
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result);
// => "06/12/2017"

其中 replace 中的,第二个参数里用 $1、$2、$3 指代相应的分组。等价于如下的形式:

1
2
3
4
var result = string.replace(regex, function () {
return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result);

1
2
3
4
5
// 也等价于
var result = string.replace(regex, function (match, year, month, day) {
return month + "/" + day + "/" + year;
});
console.log(result);

3.3. 反向引用

除了使用相应 API 来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。(\1,\2)这类

1
2
3
4
5
6
7
8
9
var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false

3.3.1. 括号嵌套怎么办?

以左括号(开括号)为准。

1
2
3
4
5
6
7
var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
var string = "1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3

该正则图形化(%5Cd(%5Cd)))%5C1%5C2%5C3%5C4%24)

在线正则测试

1
2
^((\d)(\d([a-z])))\1\2\3\4$
12a12a12aa

3.3.2. \10 表示什么呢?

另外一个疑问可能是,即 \10 是表示第 10 个分组,还是 \1 和 0 呢?
答案是前者,虽然一个正则里出现 \10 比较罕见。测试如下:

1
2
3
4
var regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
var string = "123456789# ######"
console.log( regex.test(string) );
// => true

TIP 如果真要匹配 \1 和 0 的话,请使用 (?:\1)0 或者 \1(?:0)。

3.3.3. 引用不存在的分组会怎样?

因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配
反向引用的字符本身。例如 \2,就匹配 “\2”。注意 “\2” 表示对 “2” 进行了转义。

1
2
3
4
var regex = /\1\2\3\4\5\6\7\8\9/;
console.log( regex.test("\1\2\3\4\5\6\7\8\9") );
console.log( "\1\2\3\4\5\6\7\8\9".split("") );
// Chrome 浏览器打印的结果(不同的浏览器和版本,打印的结果可能不一样)

3.3.4. 分组后面有量词会怎样?

分组后面有量词的话,分组最终捕获到的数据是最后一次的匹配。

1
2
3
4
var regex = /(\d)+/;
var string = "12345";
console.log( string.match(regex) );
// => ["12345", "5", index: 0, input: "12345"]

同理对于反向引用,也是这样的。测试如下:

1
2
3
4
5
var regex = /(\d)+ \1/;
console.log( regex.test("12345 1") );
// => false
console.log( regex.test("12345 5") );
// => true

3.4. 非捕获括号

之前文中出现的括号,都会捕获它们匹配到的数据,以便后续引用,因此也称它们是捕获型分组和捕获型分
支。
如果只想要括号最原始的功能,但不会引用它,即,既不在 API 里引用,也不在正则里反向引用。
此时可以使用非捕获括号 (?:p) 和 (?:p1|p2|p3)。

3.5. 相关案例

3.5.1. 字符串 trim 方法模拟

第一种,匹配到开头和结尾的空白符,然后替换成空字符。

1
2
3
4
5
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}
console.log( trim(" foobar ") );
// => "foobar"

第二种,匹配整个字符串,然后用引用来提取出相应的数据:

1
2
3
4
5
function trim (str) {
return str.replace(/^\s*(.*?)\s*$/g, "$1");
}
console.log( trim(" foobar ") );
// => "foobar

当然,前者效率高。

3.5.2. 将每个单词的首字母转换为大写

1
2
3
4
5
6
7
unction titleize (str) {
return str.toLowerCase().replace(/(?:^|\s)\w/g, function (c) {
return c.toUpperCase();
});
}
console.log( titleize('my name is epeli') );
// => "My Name Is Epeli"

3.5.3. 驼峰化

1
2
3
4
5
6
7
8
function camelize (str) {
return str.replace(/[-_\s]+(.)?/g, function (match, c) {
return c ? c.toUpperCase() : '';
});
}
console.log( camelize('-moz-transform') );
// => "MozTransform"
// 这里c不需要判断。''.toUpperCase() === ''; // true

3.5.4. 中划线化

1
2
3
4
5
function dasherize (str) {
return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
}
console.log( dasherize('MozTransform') );
// => "-moz-transform"

3.5.5. HTML 转义和反转义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 将HTML特殊字符转换成等值的实体
function escapeHTML (str) {
var escapeChars = {
'<' : 'lt',
'>' : 'gt',
'"' : 'quot',
'&' : 'amp',
'\'' : '#39'
};
return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'),
function (match) {
return '&' + escapeChars[match] + ';';
});
}
console.log( escapeHTML('<div>Blah blah blah</div>') );
// => "&lt;div&gt;Blah blah blah&lt;/div&gt";

其中使用了用构造函数生成的正则,然后替换相应的格式就行了,这个跟本章没多大关系。
倒是它的逆过程,使用了括号,以便提供引用,也很简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 实体字符转换为等值的HTML。
function unescapeHTML (str) {
var htmlEntities = {
nbsp: ' ',
lt: '<',
gt: '>',
quot: '"',
amp: '&',
apos: '\''
};
return str.replace(/\&([^;]+);/g, function (match, key) {
if (key in htmlEntities) {
return htmlEntities[key];
}
return match;
});
}
console.log( unescapeHTML('&lt;div&gt;Blah blah blah&lt;/div&gt;') );
// => "<div>Blah blah blah</div>"

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

3.5.6. 匹配成对标签

要求匹配

1
2
<title>regular expression</title>
<p>laoyao bye bye</p>

匹配一个开标签,可以使用正则 <[^>]+>,
匹配一个闭标签,可以使用 <\/[^>]+>,
但是要求匹配成对标签,那就需要使用反向引用,如:

1
2
3
4
5
6
7
var regex = /<([^>]+)>[\d\D]*<\/\1>/;
var string1 = "<title>regular expression</title>";
var string2 = "<p>laoyao bye bye</p>";
var string3 = "<title>wrong!</p>";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // false

疑惑:为什么这里没有用.匹配呢,或者说为啥没用.?阻止贪婪匹配模式(惰性模式)呢

4. 第四章 正则表达式回溯法原理

学习正则表达式,是需要懂点儿匹配原理的。
而研究匹配原理时,有两个字出现的频率比较高:“回溯”。

4.1. 没有回溯的匹配

假设我们的正则是 /ab{1,3}c/,
而当目标字符串是 “abbbc” 时,就没有所谓的“回溯”。

4.2. 有回溯的匹配

如果目标字符串是”abbc”,中间就有回溯。

图中第 5 步有红颜色,表示匹配不成功。此时 b{1,3} 已经匹配到了 2 个字符 “b”,准备尝试第三个时,
结果发现接下来的字符是 “c”。那么就认为 b{1,3} 就已经匹配完毕。然后状态又回到之前的状态(即
第 6 步与第 4 步一样),最后再用子表达式 c,去匹配字符 “c”。当然,此时整个表达式匹配成功了。
图中的第 6 步,就是“回溯”。

再举一个例子:
/ab{1,3}bbc/

目标字符串是”abbbc”,匹配过程是:
/“.*”/
目标字符串是:”abc”de,

图中省略了尝试匹配双引号失败的过程。可以看出 . 是非常影响效率的。
为了减少一些不必要的回溯,可以把正则修改为 /“[^”]
“/。

4.3. 常见的回溯形式

正则表达式匹配字符串的这种方式,有个学名,叫回溯法

本质上就是深度优先搜索算法。其中退到之前的某一步这一过程,我们称为“回溯”。从上面的描述过程中
,可以看出,路走不通时,就会发生“回溯”。即,尝试匹配失败时,接下来的一步通常就是回溯。
道理,我们是懂了。那么 JavaScript 中正则表达式会产生回溯的地方都有哪些呢?

4.3.1 贪婪量词

之前的例子都是贪婪量词相关的。

此时我们不禁会问,如果当多个贪婪量词挨着存在,并相互有冲突时,此时会是怎样?
答案是,先下手为强!因为深度优先搜索。测试如下:

1
2
3
4
var string = "12345";
var regex = /(\d{1,3})(\d{1,3})/;
console.log( string.match(regex) );
// => ["12345", "123", "45", index: 0, input: "12345"]

4.3.2 惰性量词

虽然惰性量词不贪,但也会有回溯的现象。
比如正则式:/^\d{1,3}?\d{1,3}$/
目标字符串是 “12345”。

知道你不贪、很知足,但是为了整体匹配成,没办法,也只能给你多塞点了。因此最后 \d{1,3}? 匹配的字
符是 “12”,是两个数字,而不是一个。

4.3.3 分支结构

我们知道分支也是惰性的,比如 /can|candy/,去匹配字符串 “candy”,得到的结果是 “can”,因为分支会
一个一个尝试,如果前面的满足了,后面就不会再试验了。
分支结构,可能前面的子模式会形成了局部匹配,如果接下来表达式整体不匹配时,仍会继续尝试剩下的分
支。这种尝试也可以看成一种回溯。

比如:/^(?:can|candy)$/ 目标字符串是’candy’

上面第 5 步,虽然没有回到之前的状态,但仍然回到了分支结构,尝试下一种可能。所以,可以认为它是
一种回溯的。

4.4. 本章小结

其实回溯法,很容易掌握的。
简单总结就是,正因为有多种可能,所以要一个一个试。直到,要么到某一步时,整体匹配成功了;要么最
后都试完后,发现整体匹配不成功。

贪婪量词“试”的策略是:买衣服砍价。价钱太高了,便宜点,不行,再便宜点。
• 惰性量词“试”的策略是:卖东西加价。给少了,再多给点行不,还有点少啊,再给点。
• 分支结构“试”的策略是:货比三家。这家不行,换一家吧,还不行,再换。
既然有回溯的过程,那么匹配效率肯定低一些。相对谁呢?相对那些 DFA 引擎, DFA 是“确定型有限自动
机”的简写。
而 JavaScript 的正则引擎是 NFA,NFA 是“非确定型有限自动机”的简写。
大部分语言中的正则都是 NFA,为啥它这么流行呢?
答:你别看我匹配慢,但是我编译快啊,而且我还有趣哦。

5. 第五章 正则表达式的拆分

5.1. 结构和操作符

JavaScript 正则表达式中,都有哪些结构呢?
字符字面量、字符组、量词、锚、分组、选择分支、反向引用。

其中涉及到的操作符有:
操作符描述 操作符 优先级
转义符 \ 1
括号和方括号 (…)、(?:…)、(?=…)、(?!…)、[…] 2
量词限定符 {m}、{m,n}、{m,}、?、*、+ 3
位置和序列 ^、$、\元字符、一般字符 4
管道符(竖杠) | 5

5.2. 注意要点

5.2.1 匹配字符串整体问题

比如要匹配目标字符串 “abc” 或者 “bcd” 时,如果一不小心,就会写成 /^abc|bcd$/。
而位置字符和字符序列优先级要比竖杠高,故其匹配的结构是。

开始-abc
bcd-结束
应该是:
/^(abc|bcd)$/

5.2.2 量词连缀问题

  1. 每个字符为 “a、”b”、”c” 任选其一,
  2. 字符串的长度是 3 的倍数。
    /^[abc]{3}+$/,这样会报错,说 + 前面没什么可重复的。
    应该为:
    /([abc]{3})+/

5.2.3 元字符转义问题

所谓元字符,就是正则中有特殊含义的字符。
所有结构里,用到的元字符总结如下:
^、$、.、*、+、?、|、\、/、(、)、[、]、{、}、=、!、:、- ,
当匹配上面的字符本身时,可以一律转义:

1
2
3
4
var string = "^$.*+?|\\/[]{}=!:-,";
var regex = /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/;
console.log( regex.test(string) );
// => true

其中 string 中的 \ 字符也要转义的。
另外,在 string 中,也可以把每个字符转义,当然,转义后的结果仍是本身:

1
2
3
4
var string = "^$.*+?|\\/[]{}=!:-,";
var string2 = "\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,";
console.log( string == string2 );
// => true

现在的问题是,是不是每个字符都需要转义呢?否,看情况。

5.2.3.1. 字符组中的元字符

跟字符组相关的元字符有 [、]、^、-。因此在会引起歧义的地方进行转义。例如开头的 ^ 必须转义,不然
会把整个字符组,看成反义字符组。

1
2
3
4
5
var string = "^$.*+?|\\/[]{}=!:-,";
var regex = /[\^$.*+?|\\/\[\]{}=!:\-,]/g;
console.log( string.match(regex) );
// => ["^", "$", ".", "*", "+", "?", "|", "\", "/", "[", "]", "{", "}", "=", "!", ":",
"-", ","]

5.2.3.2. 匹配 “[abc]” 和 “{3,5}”

我们知道 [abc],是个字符组。如果要匹配字符串 “[abc]” 时,该怎么办?
可以写成 /[abc]/,也可以写成 /[abc]/
只需要在第一个方括号转义即可,因为后面的方括号构不成字符组,正则不会引发歧义,自然不需要转义。

同理,要匹配字符串 “{3,5}”,只需要把正则写成 /{3,5}/ 即可。
另外,我们知道量词有简写形式 {m,},却没有 {,n} 的情况。虽然后者不构成量词的形式,但此时并不会报
错。当然,匹配的字符串也是 “{,n}”,测试如下:

1
2
3
4
var string = "{,3}";
var regex = /{,3}/g;
console.log( string.match(regex)[0] );
// => "{,3}"

5.2.3.3. 其余情况

比如 =、!、:、-、, 等符号,只要不在特殊结构中,并不需要转义。
但是,括号需要前后都转义的,如 /(123)/。
至于剩下的 ^、$、.、*、+、?、|、\、/ 等字符,只要不在字符组内,都需要转义的。

5.3. 案例分析

5.3.1 身份证

正则表达式是:
/^(\d{15}|\d{17}[\dxX])$/

5.3.2 IPV4 地址

正则表达式是:

1
/^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/

这个正则,看起来非常吓人。但是熟悉优先级后,会立马得出如下的结构:
((…).){3}(…)
其中,两个 (…) 是一样的结构。表示匹配的是 3 位数字。因此整个结构是
3位数.3位数.3位数.3位数
然后再来分析 (…):
(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])

6. 第六章 正则表达式的构建

6.1. 平衡法则

构建正则有一点非常重要,需要做到下面几点的平衡:
1.• 匹配预期的字符串
2.• 不匹配非预期的字符串
3.• 可读性和可维护性
4.• 效率

6.2. 构建正则前提

比如匹配这样的字符串:1010010001…。
虽然很有规律,但是只靠正则就是无能为力。

6.2.2. 是否有必要使用正则?

要认识到正则的局限,不要去研究根本无法完成的任务。同时,也不能走入另一个极端:无所不用正则。能用字符串 API 解决的简单问题,就不该正则出马。

比如,从日期中提取出年月日,虽然可以使用正则:

1
2
3
4
var string = "2017-07-01";
var regex = /^(\d{4})-(\d{2})-(\d{2})/;
console.log( string.match(regex) );
// => ["2017-07-01", "2017", "07", "01", index: 0, input: "2017-07-01"]

其实,可以使用字符串的 split 方法来做,即可:

1
2
3
4
var string = "2017-07-01";
var result = string.split("-");
console.log( result );
// => ["2017", "07", "01"]

比如,判断是否有问号,虽然可以使用:

1
2
3
var string = "?id=xx&act=search";
console.log( string.search(/\?/) );
// => 0

其实,可以使用字符串的 indexOf 方法:

1
2
3
var string = "?id=xx&act=search";
console.log( string.indexOf("?") );
// => 0

比如获取子串,虽然可以使用正则:

1
2
3
var string = "JavaScript";
console.log( string.match(/.{4}(.+)/)[1] );
// => Script

其实,可以直接使用字符串的 substring 或 substr 方法(语言精粹中推荐使用slice,substr是在ES5规范附则里。)来做:

1
2
3
var string = "JavaScript";
console.log( string.substring(4) );
// => Script

6.2.3. 是否有必要构建一个复杂的正则?

6.2.3. 是否有必要构建一个复杂的正则?
比如密码匹配问题,要求密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字
符。
在第2章里,我们写出了正则是

1
/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/

1
2
3
4
5
6
7
8
9
10
11
12
其实可以使用多个小正则来做:
var regex1 = /^[0-9A-Za-z]{6,12}$/;
var regex2 = /^[0-9]{6,12}$/;
var regex3 = /^[A-Z]{6,12}$/;
var regex4 = /^[a-z]{6,12}$/;
function checkPassword (string) {
if (!regex1.test(string)) return false;
if (regex2.test(string)) return false;
if (regex3.test(string)) return false;
if (regex4.test(string)) return false;
return true;
}

6.3. 准确性

所谓准确性,就是能匹配预期的目标,并且不匹配非预期的目标。
这里提到了“预期”二字,那么我们就需要知道目标的组成规则。

6.3.1. 匹配固定电话

比如要匹配如下格式的固定电话号码:

1
2
3
055188888888
0551-88888888
(0551)88888888

1
/^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/

这就是一个平衡取舍问题,一般够用就行。

6.3.2. 匹配浮点数

要求匹配如下的格式:

1
2
3
1.23、+1.23、-1.23
10、+10、-10
.2、+.2、-.2

上述三个部分,并不是全部都出现。如果此时很容易写出如下的正则:
/^[+-]?(\d+)?(.\d+)?$/
此正则看似没问题,但这个正则也会匹配空字符 “”。
因为目标字符串的形式关系不是要求每部分都是可选的。
/^[+-]?(\d+.\d+|\d+|.\d+)$/

6.4. 效率

保证了准确性后,才需要是否要考虑要优化。大多数情形是不需要优化的,除非运行的非常慢。什么情形正
则表达式运行才慢呢?我们需要考察正则表达式的运行过程(原理)。
正则表达式的运行分为如下的阶段:

• 1. 编译;
• 2. 设定起始位置;
• 3. 尝试匹配;
• 4. 匹配失败的话,从下一位开始继续第 3 步;
• 5. 最终结果:匹配成功或失败

当尝试匹配时,需要确定从哪一位置开始匹配。一般情形都是字符串的开头,即第 0 位。
但当使用 test 和 exec 方法,且正则有 g 时,起始位置是从正则对象的 lastIndex 属性开始。

6.4.1. 使用具体型字符组来代替通配符,来消除回溯

而在第三阶段,最大的问题就是回溯。
因为回溯的存在,需要引擎保存多种可能中未尝试过的状态,以便后续回溯时使用。注定要占用一定的内存。

6.4.2. 使用非捕获型分组

因为括号的作用之一是,可以捕获分组和分支里的数据。那么就需要内存来保存它们。
当我们不需要使用分组引用和反向引用时,此时可以使用非捕获分组。

6.4.3. 独立出确定字符

例如,/a+/ 可以修改成 /aa*/。

6.4.4. 提取分支公共部分

比如,/^abc|^def/ 修改成 /^(?:abc|def)/
又比如, /this|that/修改成 /th(?:is|at)/。
这样做,可以减少匹配过程中可消除的重复。

6.4.5. 减少分支的数量,缩小它们的范围

/red|read/ 可以修改成 /rea?d/。
此时分支和量词产生的回溯的成本是不一样的。但这样优化后,可读性会降低的。

7. 第七章 正则表达式编程

7.1. 正则表达式的四种操作

正则表达式是匹配模式,不管如何使用正则表达式,万变不离其宗,都需要先“匹配”。
有了匹配这一基本操作后,才有其他的操作:验证、切分、提取、替换。

7.1.1. 验证

比如,判断一个字符串中是否有数字。
使用 search:

1
2
3
4
5
var regex = /\d/;
var string = "abc123";
console.log( !!~string.search(regex) );
// ~0 === -1
// => true

1
2
3
4
var regex = /\d/;
var string = "abc123";
console.log( regex.test(string) );
// => true

使用 match:

1
2
3
4
var regex = /\d/;
var string = "abc123";
console.log( !!string.match(regex) );
// => true

使用 exec:

1
2
3
4
var regex = /\d/;
var string = "abc123";
console.log( !!regex.exec(string) );
// => true

其中,最常用的是 test。

7.1.2. 切分

匹配上了,我们就可以进行一些操作,比如切分。
所谓“切分”,就是把目标字符串,切成一段一段的。在 JavaScript 中使用的是 split。

7.1.3. 提取

虽然整体匹配上了,但有时需要提取部分匹配的数据。
此时正则通常要使用分组引用(分组捕获)功能,还需要配合使用相关 API。
这里,还是以日期为例,提取出年月日。注意下面正则中的括号:
使用 match:
使用 exec:
使用 test:

1
2
3
4
5
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
regex.test(string);
console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
// => "2017" "06" "26"

使用 search:

1
2
3
4
5
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
string.search(regex);
console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
// => "2017" "06" "26"

使用 replace:

1
2
3
4
5
6
7
8
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
var date = [];
string.replace(regex, function (match, year, month, day) {
date.push(year, month, day);
});
console.log(date);
// => ["2017", "06", "26"]

其中,最常用的是 match。

7.1.4. 替换

找,往往不是目的,通常下一步是为了替换。在 JavaScript 中,使用 replace 进行替换。

7.2. 相关 API 注意要点

从上面可以看出用于正则操作的方法,共有 6 个,字符串实例 4 个,正则实例 2 个:

1
2
3
4
5
6
String#search
String#split
String#match
String#replace
RegExp#test
RegExp#exec

7.2.1. search 和 match 的参数问题

我们知道字符串实例的那 4 个方法参数都支持正则和字符串。
但 search 和 match,会把字符串转换为正则的。
replace和split不会。

7.2.2. match 返回结果的格式问题

match 返回结果的格式,与正则对象是否有修饰符 g 有关。

1
2
3
4
5
6
7
var string = "2017.06.27";
var regex1 = /\b(\d+)\b/;
var regex2 = /\b(\d+)\b/g;
console.log( string.match(regex1) );
console.log( string.match(regex2) );
// => ["2017", "2017", index: 0, input: "2017.06.27"]
// => ["2017", "06", "27"]

没有 g,返回的是标准匹配格式,即,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然
后是整体匹配的第一个下标,最后是输入的目标字符串。
有 g,返回的是所有匹配的内容。
当没有匹配时,不管有无 g,都返回 null。

7.2.3. exec 比 match 更强大

当正则没有 g 时,使用 match 返回的信息比较多。但是有 g 后,就没有关键的信息 index 了。
而 exec 方法就能解决这个问题,它能接着上一次匹配后继续匹配:
其中正则实例 lastIndex 属性,表示下一次匹配开始的位置。
比如第一次匹配了 “2017”,开始下标是 0,共 4 个字符,因此这次匹配结束的位置是 3,下一次开始匹配
的位置是 4。
从上述代码看出,在使用 exec 时,经常需要配合使用 while 循环:

7.2.4. 修饰符 g,对 exex 和 test 的影响

上面提到了正则实例的 lastIndex 属性,表示尝试匹配时,从字符串的 lastIndex 位开始去匹配。
字符串的四个方法,每次匹配时,都是从 0 开始的,即 lastIndex 属性始终不变。
而正则实例的两个方法 exec、test,当正则是全局匹配时,每一次匹配完成后,都会修改 lastIndex。

7.2.5. test 整体匹配时需要使用 ^ 和 $

这个相对容易理解,因为 test 是看目标字符串中是否有子串匹配正则,即有部分匹配即可。
如果,要整体匹配,正则前后需要添加开头和结尾:

1
2
3
4
5
6
console.log( /123/.test("a123b") );
// => true
console.log( /^123$/.test("a123b") );
// => false
console.log( /^123$/.test("123") );
// => true

7.2.6. split 相关注意事项

split 方法看起来不起眼,但要注意的地方有两个的。
第一,它可以有第二个参数,表示结果数组的最大长度:

1
2
3
var string = "html,css,javascript";
console.log( string.split(/,/, 2) );
// =>["html", "css"]

第二,正则使用分组时,结果数组中是包含分隔符的:

1
2
3
var string = "html,css,javascript";
console.log( string.split(/(,)/) );
// =>["html", ",", "css", ",", "javascript"]

7.2.7. replace 是很强大的

《JavaScript 权威指南》认为 exec 是这 6 个 API 中最强大的,而我始终认为 replace 才是最强大的。
因为它也能拿到该拿到的信息,然后可以假借替换之名,做些其他事情。
总体来说 replace 有两种使用形式,这是因为它的第二个参数,可以是字符串,也可以是函数。
当第二个参数是字符串时,如下的字符有特殊的含义:


属性 描述
$1,$2,…,$99 匹配第 1-99 个 分组里捕获的文本
$& 匹配到的子串文本
$` 匹配到的子串的左边文本
$’ 匹配到的子串的右边文本
$$ 美元符号
记忆中语言精粹中有列举更多。

再例如,把 “2+3=5”,变成 “2+3=2+3=5=5”:
1
2
3
var result = "2+3=5".replace(/=/, "$&$`$&$'$&");
console.log(result);
// => "2+3=2+3=5=5

我们对最后这个进行一下说明。要把 “2+3=5”,变成 “2+3=2+3=5=5”,其实就是想办法把 = 替换成
=2+3=5=,其中,$& 匹配的是 =, $匹配的是 2+3,$' 匹配的是 5。因此使用 "$&$$&$’$&” 便达成了
目的。
当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么:

1
2
3
4
5
6
"1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (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"]

此时我们可以看到 replace 拿到的信息,并不比 exec 少。

7.2.8. 使用构造函数需要注意的问题

一般不推荐使用构造函数生成正则,而应该优先使用字面量。因为用构造函数会多写很多 \

7.2.9. 修饰符

ES5 中修饰符,共 3 个:g,i,m
当然正则对象也有相应的只读属性:

1
2
3
4
5
6
7
var regex = /\w/img;
console.log( regex.global );
console.log( regex.ignoreCase );
console.log( regex.multiline );
// => true
// => true
// => true

7.2.10. source 属性

正则实例对象属性,除了 global、ingnoreCase、multiline、lastIndex 属性之外,还有一个 source
属性。
它什么时候有用呢?
比如,在构建动态的正则表达式时,可以通过查看该属性,来确认构建出的正则到底是什么

1
2
3
4
var className = "high";
var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
console.log( regex.source )
// => (^|\s)high(\s|$) 即字符串"(^|\\s)high(\\s|$)"

7.2.11. 构造函数属性

构造函数的静态属性基于所执行的最近一次正则操作而变化。除了是 $1,…,$9 之外,还有几个不太常用的
属性(有兼容性问题):


静态属性 描述 简写形式
RegExp.input 最近一次目标字符串 RegExp[“$_”]
RegExp.lastMatch 最近一次匹配的文本 RegExp[“$&”]
RegExp.lastParen 最近一次捕获的文本 RegExp[“$+”]
RegExp.leftContext 目标字符串中lastMatch之前的文本 RegExp[“$`”]
RegExp.rightContext 目标字符串中lastMatch之后的文本 RegExp[“$’”]

7.3. 真实案例

7.3.1. 使用构造函数生成正则表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<p class="high">1111</p>
<p class="high">2222</p>
<p>3333</p>
<script>
function getElementsByClassName (className) {
var elements = document.getElementsByTagName("*");
var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
var result = [];
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (regex.test(element.className)) {
result.push(element)
}
}
return result;
}
var highs = getElementsByClassName('high');
highs.forEach(function (item) {
item.style.color = 'red';
});
</script>

7.3.2. 使用字符串保存数据

一般情况下,我们都愿意使用数组来保存数据。但我看到有的框架中,使用的却是字符串。
使用时,仍需要把字符串切分成数组。虽然不一定用到正则,但总感觉酷酷的,这里分享如下:

1
2
3
4
5
6
7
8
9
var utils = {};
"Boolean|Number|String|Function|Array|Date|RegExp|Object|Error".split("|").forEach(fun
ction (item) {
utils["is" + item] = function (obj) {
return {}.toString.call(obj) == "[object " + item + "]";
};
});
console.log( utils.isArray([1, 2, 3]) );
// => true

7.3.3. if 语句中使用正则替代 &&

比如,模拟 ready 函数,即加载完毕后再执行回调(不兼容 IE 的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var readyRE = /complete|loaded|interactive/;
function ready (callback) {
if (readyRE.test(document.readyState) && document.body) {
callback()
}
else {
document.addEventListener(
'DOMContentLoaded',
function () {
callback()
},
false
);
}
};
ready(function () {
alert("加载完毕!")
});

7.3.4. 使用强大的 replace

因为 replace 方法比较强大,有时用它根本不是为了替换,只是拿其匹配到的信息来做文章。
这里以查询字符串(querystring)压缩技术为例,注意下面 replace 方法中,回调函数根本没有返回任何
东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
function compress (source) {
var keys = {};
source.replace(/([^=&]+)=([^&]*)/g, function (full, key, value) {
keys[key] = (keys[key] ? keys[key] + ',' : '') + value;
});
var result = [];
for (var key in keys) {
result.push(key + '=' + keys[key]);
}
return result.join('&');
}
console.log( compress("a=1&b=2&a=3&b=4") );
// => "a=1,3&b=2,4"

完。
84/89

本文地址: https://lxchuan12.github.io/2017/10/12/20171012-JavaScript regex mini books reading record/