正则表达式

正则表达式是一种描述字符模式(特征)的工具,是字符的高度抽象,被用于字符的搜索、匹配、替换、验证等多种场景。ESMAScript 中用 RegExp 类表示正则表达式。

1
2
3
4
5
// RegExp 构造函数
var reg = new RegExp('pattern', 'flags'); // console.log(reg) 会打印输出字面量 /pattern/flags

// 字面量
var reg = /pattern/flags;

RegExp 构造函数下 pattern 以字符串存在,跟字面量模式不同的是,字符串下 / 需要做两层转义,字符串中转义一次,正则中转义一次。以 '\\.' 为例,首先 \\. 在字符串中被转义 \. ,然后作为正则表达式字面量,\. 又被转义为 .,即字符串 .,如果在字符串中只写 \.,字符串转义为 .,作为正则表达式被解释时就变成匹配任意字符了。

1
2
3
var reg1 = /^\w+$/;
var reg2 = new RegExp('^\\w+$');
console.log(reg2); // 输出为字面量 /^\w+$/

基本语法

正则表达式由两种基本字符类型组成,普通文本字符和元字符(Metacharacter)。元字符是有特殊含义的字符,转义字符、字符类、量词、定位符…都是元字符,元字符使正则表达式具有处理能力。反斜杠转义后的元字符将失去特殊含义。

元字符 含义
\ 转移字符
[] 字符类
{}?*+ 量词
^$\b\B 定位符
| 可选项
. 除新行(newline)外的任一字符(s 修饰符将使 . 匹配新行字符)
() 分组

转义字符

反斜杠 \ 通常用作转义字符,用于改变其后紧跟的字符的含义,使其表示本身,而不是它们的特殊含义,或者反之,使其表示特殊含义。

比如,带上转移字符的 \\\.\[\] 表示 \.[] 字符本身,而不是它们的特殊含义。\b\B 表示单词边界和非单词边界,\n\r\t 表示换行符,回车字符和制表符,\0 表示 null 字符。

1
2
3
var reg = /\?/   
// 如果通过 RegExp 构造函数来使用它们 , 则都必须进行双重转义
var reg = new RegExp('\\?');

字符类(Character classes)

将直接量字符放进 [] 方括号内就组成了字符类,一个字符类可以匹配它所包含的任意字符。由于某些字符类非常常用,因此使用了些特殊字符的转义来表示它们,例如 \s,方括号内也可以放这些简写的特殊转义字符,例如 [\s\d]

字符类 匹配情况
[abc] 简单类:(或) 匹配中括号中的字符集中任意字符
[a-z] 范围类:(到) 匹配中括号中的字符集中任意字符
[^a-z] 负向类:(非) 匹配非中括号中的字符集中任意字符
[^a-z0-9] 组合类:(组合)
. 预定义类:匹配除换行符外的任意字符,[^\n\r]
\d 预定义类:匹配数字 [0-9]
\D 预定义类:匹配非数字 [^0-9]
\s 预定义类:匹配空白字符 [\t\n\x0B\f\r]
\S 预定义类:匹配非空白字符 [^\t\n\x0B\f\r]
\w 预定义类:匹配字母数字下划线 [a-zA-Z_0-9]
\W 预定义类:匹配非字母数字下划线 [^a-zA-Z_0-9]
[\b] 退格直接量(特例)
1
2
3
4
5
// 计算元音的个数
var str = 'There was a long silence after this, and Alice could only hear whispers now and then.';
var reg = /[AEIOUYaeiouy]/g;

console.log('元音数:', str.match(reg).length);

注:[] 字符类中,除了 ^-] 是元字符外,其他都是普通字符,比如,[^.] 中的 . 是字符 .,而不是字符类 .,不要将其与 [\n\r] 等效了。

量词(Quantifiers)

量词用于指定匹配的次数。

贪婪 惰性 支配 描述
? ?? ?+ 零次或一次出现
* *? *+ 零次或多次出现
+ +? ++ 一次或多次出现
{n} {n}? {n}+ 恰好 n 次出现
{n,m} {n,m}? {n,m}+ 至少 n 次至多 m 次出现
{n,} {n,}? {n,}+ 至少 n 次出现

三种不同类型量词的区别:

1
2
3
4
5
6
7
8
var str = 'abbbaabbbaaabbb1234';
var reg1 = /.*bbb/g;
var reg2 = /.*?bbb/g;
var reg3 = /.*+bbb/g;

str.match(reg1); // ["abbbaabbbaaabbb"]
str.match(reg2); // ["abbb", "aabbb", "aaabbb"],reg2 包含一个惰性量词
str.match(reg3); // [],没有返回结果,因为它是支配性的,一次测试失败,就得不到结果
  • 贪婪量词

贪婪量词先看整个的字符串是否匹配。如果没有发现匹配,则去掉该字符串中最后一个字符,并再次尝试。如果还是没有发现匹配,那么再次去掉最后一个字符,这个过程会一直重复直到发现一个匹配或者字符串不剩任何字符。

量词 含义
? {0,1},0 个或 1 个
* {0,}
+ {1,}
{n} n 个
{n,} 最少 n 次,, 后没有空格
{n,m} n 到 m 之间
{,m} 没有这种量词
  • 惰性量词

惰性量词先看字符串中的第一个字母是否匹配。如果单独这一个字符还不够,就读入下一个字符,组成两个字符的字符串。如果还是没有发现匹配,惰性量词继续从字符串中添加字符直到发现匹配或者整个字符都检查过也没有匹配。惰性量词和贪婪量词的工作方式相反。

贪婪量词后接 ? 便是惰性量词。

  • 支配性量词

支配量词只尝试匹配整个字符串,如果整个字符串不能产生匹配,不进行回溯,不做进一步尝试。支配性量词并不是所有正则表达式引擎都支持,比如,JavaScript。

贪婪量词后接 + 便是支配量词。

定位符

定位符是用于指定匹配的位置而不是匹配字符本身的特殊字符。

定位符 含义
^ 行首
$ 行尾
\b 单词边界
\B 非单词边界

使用 $^,查找字行中第一个单词和最后一个单词:

1
2
3
4
var str = 'Important word is the last one.';
var reg = /(\w+)\.$/;
reg.test(str);
console.log(RegExp.$1); // one
1
2
3
4
var str = 'Important word is the last one.';
var reg = /^(\w+)/;
reg.test(str);
console.log(RegExp.$1); // Important

也可以用单词边界实现:

1
2
3
4
5
var str = 'Important word is the last one.';
// 惰性来制定在单词边界之前可以出现任何字符,且可出现一次或多次(如果使用贪婪性量词,表达式就匹配整个字符串)
var reg = /^(.+?)\b/;
reg.test(str);
console.log(RegExp.$1); // Important

使用单词边界可以方便地从字符串中抽取单词:

1
2
3
4
var str = 'First second third fourth fifth sixth';
var reg = /\b(\S+?)\b/g;
var result = str.match(reg);
console.log(result); // ["First", "second", "third", "fourth", "fifth", "sixth"]

修饰符(标记)

修饰符 含义 描述
i ignore 不区分大小写 将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别
g global 全局匹配 查找所有的匹配项
m multi line 多行匹配 使边界字符 ^$ 匹配每一行的开头和结尾,注意是多行,不是整个字符串的开头和结尾
s 特殊字符圆点 . 中包含换行符 \n 默认情况下的圆点 . 是匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n

m 多行匹配,会让 $ 边界匹配换行符 \n 以及字符串真正的结尾。

1
2
3
4
var str = 'First second\nthird fourch\nfifth sixth';
var reg = /(\w+)$/gm
var result = str.match(reg);
console.log(result); // ["second", "fourch", "sixth"],若不指定多行模式则会返回 ['sixth']

多行模式同样会改变 ^ 边界的行为,这时它会匹配换行之后的位置。

1
2
3
4
var str = 'First second\nthird fourch\nfifth sixth';
var reg = /^(\w+)/gm
var result = str.match(reg);
console.log(result); // ["First", "third", "fifth"],若不指定多行模式则会返回 ["First"]。

匹配模式(pattern)

根据元字符的复杂程度,可将正则表达式分为简单模式和复杂模式。复杂模式不仅由字符类和量词组成,还由分组、引用、前瞻等一系列强大的正则表达式功能组成。

分组

分组是通过用一系列括号包围一系列字符、字符类以及量词来使用的。例如,假设想匹配字符串 “dogdog”。使用目前获得的知识,可能估计表达式应该类似:

1
var reg = /dogdog/g;

尽管这是可以的,但有是点儿浪费,如果不知道dogh在字符串中到底出现几次时该怎么办?如果 dog 重复次数过多呢?你可以使用分组来重写这个表达式,如下:

1
var reg = /(dog){2}/g;

表达式中的括号的意思是字符序列“dog”将在一行上连续出现两次。但是不并不限制在分组后使用花托号,可以使用任意量词:

1
2
3
var reg1 = /(dog)?/; // 匹配出现零次或一次 dog
var reg2 = /(dog)*/; // 匹配出现零次或任意次 dog
var reg3 = /(dog)+/; // 匹配出现一次或多次 dog

通过混合使用字符、字符类和量词,甚至可以实现一些相当复杂的分组:

1
var reg = /[(bd)ad?]*/; // 匹配出现零次或多次 ”ba”,”da”,”bad”,”dad”

同时也不介意将分组放在分组中间:

1
var reg = /(mom( and dad)?)/; // 匹配 ”mom” 或者 ”mon and dad”

这个表达式要求 ”mon” 是必要的,但是字符串 ” and dad” 可以出现零次或一次。分组还可以用来弥补一些 JavaScript 所缺乏的一些语言功能。

反向引用

反向引用 含义
(string) 用于反向引用的分组
\1$1 匹配第一个分组中的内容
\2$2 匹配第二个分组中的内容
\3$3 匹配第三个分组中的内容

反向引用是按照从左到右遇到的左括号字符的顺序进行创建和编号的。例如,表达式 (A?(B?(C?))) 将产生编号从 1~3 的三个反向引用:

1
2
3
(A?(B?(C?)))
(B?(C?))
(C?)

反向可以有几种不同的使用方法。

使用正则表达式对象的 test()match()search() 方法后,反向引用的值可以从 RegExp 构造函数中获得。例如:

1
2
3
4
var str = '#123456789';
var reg = /#(\d+)/;
reg.test(str);
console.log(RegExp.$1); // 123456789

test() 方法后,所有的反向引用都被保存在 RegExp 构造函数中,从 RegExp.$1(它保存了第一个反向引用)开始,如果还有第二个反向引用,就是 RegExp.$2,如果第三个反向引用存在,就是 RegExp.$3,依此类推。因为该组匹配了 ”123456789”,所以 RegExp.$1 中就存储了这个字符串。

还可以直接在定义分组的表达式中包含反向引用。这可以通过使用特殊转义字符序列如 \1\2 等等实现。例如:

1
2
3
var str = 'dogdog';
var reg = /(dog)\1/;
console.log(reg.test(str)); // true

正则表达式 reg 首先创建单词 dog 的组,然后又被特殊转义序列 \1 引用,使得这个正则表达式等于 /dogdog/。

第三,反向引用可以用在 replace() 方法中,这通过使用特殊字符序列 $1
$2 等等来实现。描述这种功能的最佳例子是调换字符串中的两个单词的顺序。假设想将字符串 “1234 5678” 变成 “5678 1234”。可以通过下面的代码来实现

1
2
3
4
var str = '1234 5678';
var reg = /(\d{4}) (\d{4})/;
var result = str.replace(reg,'$2 $1')
console.log(result); // 5678 1234

在这个例子中,正则表达式有两个分组,每一个分组有四个数字。在 replace() 方法的第二个参数中,$2 等同于 “5678”,而 $1 等同于 “1234”,对应于它们在表达式中出现的顺序。

候选

如果要对同一个表达式同时匹配 ”red” 和 ”black” 时要怎么做呢?这些单词完全没有相同的字符,这样就要写两个不同的正则表达式,并分别对两个字符串进行匹配,像这样:

1
2
3
4
5
6
var str1 = 'red';
var str2 = 'black';
var reg1 = /red/;
var reg2 = /black/;
console.log(reg1.test(str1) || reg2.test(str1)); // true
console.log(reg2.test(str2) || reg2.test(str2)); // true

这虽然完成了任务,但是十分冗长。还有另一种方式就是使用正则表达式的候选操作符。

候选操作符和 ECMAScript 的二进制异或一样,是一个管道符 |,它放在两个单独的柜式之间。正如下面的例子:

1
2
3
4
5
var str1 = 'red';
var str2 = 'black';
var reg = /(red|black)/;
console.log(reg.test(str1)); // true
console.log(reg.test(str2)); // true

在这里,reg 匹配 ”red” 或者 ”black”,同时测试每个字符串都返回 true。因为两个备选项存放在一个分组中,不管哪个被匹配了,都会存在 RegExp.$1 中以备将来使用(同时也可以在表达式中使用 \1)。在第一个测试中,RegExp.$1 等于 ”red”,在第二个中,它等于 ”blue”。

OR 模式在实践中以一种通常的用法是从用户输入中删除不合适的单词,这对于在线论坛来说是非常重要的。通过针对这些敏感单词使用 OR 模式和 replace() 方法,则可以很方便地在帖子发布之前去掉敏感内容。

1
2
3
4
var reg = /badword|anotherbadword/gi;
var str = 'This is a string using badword and anotherbadword';
var result = str.replace(reg, '****');
console.log(result); // This is a string using **** and ****

也可以用星号替换敏感词中每一个字母,也就是说最后出现的文本中星号的数量和敏感词中的字符数量是一样的,使用函数来作为 replace() 方法的第二个参数,就可以达到这个目的:

1
2
3
4
5
6
var reg = /badword|anotherbadword/gi;
var str = 'This is a string using badword and anotherbadword';
var result = str.replace(reg,function(item){
return item.replace(/./g, '*');
});
console.log(result); // This is a string using ******* and **************

非捕获性分组

非捕获性分组即不保存反向引用的分组。在较长的正则表达式中,存储反向引用会降低匹配速度。通过使用非捕获性分组,仍然可以拥有与匹配字符串序列同样的能力,而无需存储结果的开销。创建一个非捕获性分组,只要在左括号的后面加上 ?:

1
2
3
4
var str = '#123456789';
var reg = /#(?:\d+)/;
reg.test(str);
console.log(RegExp.$1); // ''

这个例子的最后一行代码输出一个空字符,因为该分组是非捕获性的。正因如此,replace()方法就不能通过 RegExp.$x 变量为使用任何反向引用,或在正则表达式中使用它。看看运行下面的代码会怎样:

1
2
3
var str = '#123456789';
var reg =/#(?:\d+)/;
console.log(str.replace(reg, 'abcd$1')); // 'abcd$1'

这段代码输出 abcd$1 而不是 abcd123456789,因为 $1 在这里并不被看成是一个反向引用,而被直接翻译成字符。

正则表达式有一个十分常用的方式是去掉文本中所有的 HTML 标签,尤其是在论坛和BBS上,这可以防止游客在他们的发帖中插入恶意或是无意错误的 HTML。删除HTML标记的正则表达式很简单,只要用一个简单的表达式:

1
var reg = /<(?:.|\s)*?>/g;

这个表达式匹配一个小括号 < 后面跟着任何文本,然后跟着一个大于号 >,这有效地匹配了所有的 HTML 标签,这里使用非捕获性分组是因为在小于号和大于号之间出现的内容并不重要(这些都是要删除的)。

1
2
3
4
5
6
String.prototype.innerHTML = function() {
var reg = /<(?:.|\s)*?>/g;
return this.replace(reg, '');
}
var str = '<b>This would be bold</b>';
console.log(str.innerHTML()); // 'This would be bold';

前瞻

有时候,可能希望,当某个特定的字符分组出现在另一个字符串之前时,才去捕获它。

前瞻就是告诉正则表达式运算器向前看一些字符而不移动其位置。有正向前瞻和负向前瞻。正向前瞻检查的是接下来的出现的是不是某个特定字符集。而负向前瞻则是检查接下来的不应该出现的特定字符集。

创建正向前瞻是要将模式放在 (?=) 之间。注意这不是分组,虽然它也用到括号。事实上,分组不会考虑前瞻的存在(无论是正向的还是负向的)。

1
2
3
4
5
6
var str1 = 'bedroom';
var str2 = 'bedding';
var reg = /(bed(?=room))/;
console.log(reg.test(str1)); // true
console.log(RegExp.$1); // bed
console.log(reg.test(str2)); // false

在这个例子中,reg 只匹配后面跟着 “room” 的 “bed”。因此,它能匹配 str1 而不能匹配 str2。在用表达式测试 str1 后,这段代码输出 RegExp.$1 的内容是 “bed”,而不是 “bedroom”。模式的 “room” 的部分是包含在前瞻中的,所以没有作为分组的一部分返回。

创建负向前瞻是要将模式放在 (?!) 之间。

1
2
3
4
5
6
var str1 = 'bedroom';
var str2 = 'bedding';
var reg = /(bed(?=room))/;
console.log(reg.test(str1)); // false
console.log(RegExp.$1); // bed
console.log(reg.test(str2)); // true

这里,表达式变成只匹配后面不跟着 “room” 的 “bed”,所以模式匹配 “bedding” 而不是 “bedroom”。在测试 str2,RegExp.$1 还是包含 “bed”,而不是 “bedding”。
尽管 JavaScript 支持正则表达式前瞻,但它不支持后瞻。后瞻可以匹配这种模式:“匹配 b 当且仅汉它前面没有 a”。

JavaScript 中正则表达式属性和方法

RegExpString 都定义了使用正则表达式进行强大的模式匹配和文本检索、替换的方法。

正则表达式属性和方法

  • test

test() 方法用于检测字符串是否符合正则表达式描述的规则,返回布尔值。用该方法测试字符串时要注意“行首和行尾”,表单在验证时基本会加上行首行尾。

1
/^[\w-]+@[a-zA-Z0-9]+\.[A-Za-z]{2,4}$/.test('xwblearn2008@hotmail.com')
  • exec

exec() 方法也用于在字符串中查找指定正则表达式,如果 exec() 方法执行成功,则返回包含该查找字符串的相关信息数组,如果失败,则返回 null

1
2
3
4
5
var str = 'a bat , a Cat , a fAt ,a baT , a faT cat'; 
var reg = /at/;
var arr = reg.exec(str);
console.log(arr);
console.log( arr.length ); // 1
  • 静态属性
属性 短名 含义
input $_ 当前要匹配的字符串
lastMatch $& 最后一次匹配字符串
lastParen $+ 最后一次匹配的捕获组(圆括号内)
leftContext $" lastMatch 前的子串
rightContext $" lastMatch 后的子串
multiline $* 是否所有的表达式都使用多行模式,是一个布尔值

注:每一个静态属性都对应着一个短名。

1
2
3
4
5
var str = 'this is a google!'; 
var reg = /google/ig;
reg.test(str); // 一定要执行以下,无论是test,还是exec
console.log(RegExp.input); // this is a google!
console.log(RegExp.leftContext); // this is a
  • 实例属性
属性 含义
global Boolean 值,表示 g 是否已设置
ignoreCase Boolean 值,表示i是否已设置
lastIndex 整数,代表下次匹配将从哪里字符位置开始
Multiline Boolean 值,表示 m 是否已设置
Source 正则表达式的源字符串形式
1
2
var reg = /google/;
console.log(reg.global); // false

字符串方法

字符串中支持正则表达式的方法有以下:

1
2
3
4
* match() --- 匹配则放回数组,没有匹配则返回 null。
* replace() ---
* search() --- 返回第一个匹配的位置,没有则返回-1。此方法与 indexOf() 方法类似,但它支持正则。g 全局匹配在此方法不起作用。
* split() --- 切姜片,返回一个数组。

常用正则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 由字母数字下划线组成的字符串
/^\w+$/

// 邮政编码
/[1-9][0-9]{5}/ // 共 6 位数字,第一位不能为 0,比如 224000

// 文件压缩包
/[\w]+\.zip|rar|7zip|tgz/

// 图片
/^.*?\.(jpg|png|bmp|gif)$/

// 所有空格
/\s+/g // 比如删除所有空格 '111 222 333'.replace(reg,'')

// 电子邮件
/^([\w\.\-]+)@([a-zA-Z0-9]+)\.([a-zA-Z]{2,4})$/

// 手机号
/^1[3|4|5|8][0-9]\d{8}$/
/^0?(13|15|18)[0-9]{9}$/
/^1\d{10}$/

// 座机(区号和区号链接 - 可选)
/((^0\d{2,3})-?)?\d{7,8}$/

// 手机号码或座机号
/^((0\d{2,3})-?)?\d{7,8}$|^1\d{10}$/

// 邮箱
/^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/

// 中文/韩文/日文/全角字符(UTF-8,Unicode 编码范围)
/[\u4e00-\u9fa5]/ // 中文
/[\x3130-\x318F]/ // 韩文
/[\xAC00-\xD7A3]/ // 韩文
/[\u0800-\u4e00]/ // 日文
/[\ufe30-\uffa0]/ // 全角字符

// 密码(字母、数字、@、_、~):
/^[0-9a-zA-Z@_~]{1,}$/

// URL
/(\w+):\/\/([\w.]+)\/(\S*)/

// 颜色
/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/

// 数字和字母
/^\d{1,}$/ // 整数
/^\d+(\.\d+)?$/ // 数值(整数或者小数)
/^[a-zA-Z]{1,}$/ // 字母
/^[0-9a-zA-Z]{1,}$/ // 字母和数字的组合

// 首尾空格
/^\s+/ // 首空格
/\s+$/ // 尾空格
/^\s*(.+?)\s*$/ // 首尾空格,或者 /^\s*(.*?)\s*$/

// HTML 标签
/<[^>]+>/g
/<(?:.|\s)*?>/g // 非捕获性分组提升分组效率,惰性匹配尽可能早的匹配到 >

// HMTL 注释
/<!--(.*?)-->/
1
2
3
4
5
6
7
8
9
// 和谐字符
var blacklistReg = /badword1|badword2|anotherOne/gi;
var userInput = 'This is a string using badword1 and badword2.';

var hamonyInput = userInput.replace(blacklistReg, '*');
// or
var hamonyInput = userInput.replace(blacklistReg, function(matchStr){
return matchStr.replace(/./g,'*');
});
1
2
3
4
5
6
7
8
// 删除首尾空格
var reg = /^\s*(.+?)\s*$/; // 使用非贪婪捕获
var str = ' google ';
console.log('|' + reg.exec(str)[1] + '|');

var reg = /^\s*(.+?)\s*$/;
var str = ' google ';
console.log('|' + str.replace(reg, '$1') + '|'); // 使用分组获取
1
2
3
4
5
6
// 过滤 HTML 标签
var str = '<div>asdf</div>';
String.prototype.innerText = function () {
return this.replace(/<(?:.|\s)*?>/g, '');
};
console.log(str.innerText());

注:也可写为 /<(?:[\s\S])*?>/g/<(?:\S|\s)*?>/g[\s\S](\S|\s) 表示所有字符。

1
2
3
4
5
6
7
8
// trim
String.prototype.trim = function(){
var reExt = /^\s*(.*?)\s*$/ ;
// $1 表示的就是左边表达式中括号内的字符,即第一个子匹配
return this.replace(reExt, '$1');
};

console.log(' fdsa'.trim());
1
2
3
4
5
6
7
8
// 插入千分符
/\B(?=(?:\d{3})+$)/g
/(?=\B(?:\d{3})+$)/g
/(?=(?!\b)(?:\d{3})+$)/g

var str = '1234234';
var reg = /(?=(?!\b)(\d{3})+$)/;
str.replace(reg, ',') // 1,234234
1
2
// 00:00--23:59
/^(([0-1]\d)|(2[0-4])):[0-5]\d--(([0-1]\d)|(2[0-4])):[0-5]\d$/
1
2
3
4
5
6
7
8
// 首字母大写
var str = 'asd asd asd';
var reg = /\b\w+\b/g;
var result = str.replace(reg, function (item) {
return item.substring(0, 1).toUpperCase() + item.substring(1);
});

console.log(result);
1
2
3
// 字符串内部倒置
name = 'Doe, John';
name.replace(/(\w+)\s*, \s*(\w+)/, '$2 $1');
1
2
3
4
5
6
7
8
9
10
11
12
// 连续英文单词去重
var str = 'a mo mo mo fw mo mo'
var str2 = '';

do {
str2 = str;
str = str.replace(/\s(\w+\s)\1/, ' $1');
} while (str.length != str2.length)

str = str.replace(/^(\w+\s)\1/, '$1').replace(/(\s\w+)\1$/, '$1');

console.log(str);

附:正则表达式语法正则表达式快速学习指南regex101 正则表达式在线测试工具。