JavaScript: 详解正则表达式之二

上一篇文章中我们讲了正则表达式的基本用法,接下来博主想聊聊其中的细节,今天就从正则修饰符开始吧。

正则修饰符又称为正则标记(flags),它会对正则的匹配规则做限定,进而影响匹配的最终结果。在上次的文章中我们也提到过,正则修饰符一共有以下几种,可以单独使用,也可以组合使用:

/\w+/g; // global search
/\w+/i; // ignore case
/\w+/m; // multi-line
/\w+/u; // unicode
/\w+/y; // sticky

/\w+/gi;
new RegExp('\\w+', 'gi');

其中的i好理解,正如上面的注释一样,ignore case或case insensitive,忽略大小写。

下面是一个简单的例子,正则表达式加上了i修饰符之后也可以匹配到大写字母:

'Hello World'.match(/hello/i);  // ["Hello"]

/hello/i.exec('Hello World');   // ["Hello"]

再来看看全局匹配修饰符g,下面是一个全局匹配的例子:

var source = 'hello world hello JS';

source.match(/hello/);      // ["hello"]

source.match(/hello/g);     // ["hello", "hello"]

从上面代码中可以看出,普通正则的匹配结果只有一个,如果想要找出全部的匹配结果,后面则需要加一个g修饰符,使其成为全局匹配模式。

全局修饰符g通常也会和多行匹配修饰符m结合使用,我们将上面例子稍加改动,添加一个换行符,正则也稍加修改:

var source = 'hello world\nhello JS';

source.match(/^hello.+/g);    // ["hello world"]

大家会看到,我们是要在多行文本中匹配以"hello"开头的字符串,但结果只出现了第一个匹配项,后面的"hello JS"并未匹配到,这时我们需要加入多行匹配修饰符m:

var source = 'hello world\nhello JS';

source.match(/^hello.+/gm);   // ["hello world", "hello JS"]

现在,所有的结果都匹配到了。

但需要注意的是,单独使用修饰符m是不起作用的,它必须和g相结合,就像下面例子一样,虽然有m修饰符,但仍旧只匹配到了第一行文字:

var source = 'hello world\nhello JS';

source.match(/^hello.+/m);    // ["hello world"]

另外,还有一个很重要的条件,那就是,只有正则中包含起始标记"^"或结束标记"$"时,修饰符m才会发挥它的作用,否则g不需要m,且看下面例子:

// 只有匹配开始标记^或结束标记$时,g才需要m

var source = 'hello world\nhey world';

// 正则中没有^或$ 只需g即可匹配多行
source.match(/he.+/g);          // ["hello world", "hey world"]

// 正则中含有^或$ g只能匹配第一个结果
source.match(/^he.+/g);         // ["hello world"]
source.match(/.+world$/g);      // ["hey world"]

// 含有^或$的情况下 需要添加m 才可以匹配多行
source.match(/^he.+/gm);         // ["hello world", "hey world"]
source.match(/.+world$/gm);      // ["hello world", "hey world"]

以上介绍的都是正则修饰符在String#match()方法中的表现,我们也知道,RegExp#exec()是与之对应的一个方法,同样可以匹配字符串,返回结果数组,那么这个exec()方法对于含有全局修饰符的正则又会有什么样的表现呢?实际操作发现,RegExp#exec()方法与上面String#match()的规则大致相同,但不同的是,RegExp#exec()方法每次只会匹配一个结果,所以需多次环执行才能获取全部。我们来看下面示例:

var regex = /^hello.+/gm;
var source = 'hello world\nhello JS';

regex.exec(source);   // ["hello world"]
regex.exec(source);   // ["hello JS"]

可以看到每一次执行正则实例的exec()方法都会返回一个结果数组,由于正则中含有起始标记^和gm组合,我们需要执行两次才能获取到全部的结果,这是与String#match()方法不同的地方。一般来说,我们可以使用循环结构调用RegExp#exec()方法来获取所有的结果:

var result = null;
while (result = regex.exec(source)) {
    console.log(result);
}
// output:
// ["hello world"]
// ["hello JS"]

对于RegExp#test()方法,一般是用来检测字符串是否匹配某种模式,如果要在多行中检测任意一行是否匹配时,同样需要gm组合,下面代码先简单检测匹配情况,然后在多行中进行匹配:

var source = 'hello world\nhey JS';

/^hello.+/.test(source);      // true

/^hey.+/.test(source);        // false
/^hey.+/g.test(source);       // false

/^hey.+/gm.test(source);      // true

从结果来看,不加gm修饰符的正则,只能检测一行数据的匹配情况,加入gm后可以对多行进行检测,只要任意一行符合条件,即返回true。

最后再来说说String#replace()方法,同样地,如果正则中出现了^或$,那就需要加上gm组合,下面代码演示了多行替换的操作:

var source = 'hello world\nhello JS';

// 正则中没有^或$,全局g轻松搞定
source.replace(/hello/g, 'hey');    // "hey world\nhey JS"

// 正则中含有^或$,全局g也无能为力,仅能替换第一行
source.replace(/^hello/g, 'hey');   // "hey world\nhello JS"

// 需要使用gm组合
source.replace(/^hello/gm, 'hey');  // "hey world\nhey JS"

上面是全局匹配g和多行匹配m,下面介绍一下u修饰符。

u修饰符是ES6新增特性,可以启用Unicode模式对字符串进行正则匹配,能正确处理四个字节的UTF-16字符集。为什么需要这个修饰符呢,我们先来看一个例子:

/^.{3}$/.test('你好啊');    // true
/^.{3}$/.test('𠮷野家');    // false

上面正则用于检测字符串是否由三个字符组成,大家看到结果也许会比较困惑,为什么第二个不匹配呢?原因在于“𠮷野家”中的“𠮷”字,仔细观察,会发现它并不是“吉祥”中的“吉”字,而是它的一个异形字,这个字也属于汉字,不过后来在日文中较为常见,对于这样的字,一般的正则很难匹配到,而在ES6中,我们可以指定一个u修饰符,启用Unicode模式,对大于\uFFFF的Unicode字符进行匹配。下面我们添加了u修饰符之后,正则就匹配成功了:

/^.{3}$/u.test('𠮷野家');   // true

类似地,还有下面两个例子:

// 下面正则中含有Unicode字符和对应的量词

/𠮷{3}/.test('𠮷𠮷𠮷');     // false
/𠮷{3}/u.test('𠮷𠮷𠮷');    // true

// 下面正则用于匹配非空格字符

/^\S$/.test('𠮷');         // false
/^\S$/u.test('𠮷');        // true

另外,ES6新增了大括号Unicode表示法,就像下面这样:

var a = '\u{20BB7}';       // '𠮷'

/\u{20BB7}/u.test('𠮷');   // true

不过在使用这种表达方式时,要特别小心,因为\u在普通的正则中表示匹配字符u,而开启Unicode模式时,就不一定了:

// 检测是否含有3个'u'字符

/\u{3}/.test('uuu');      // true

// 检测是否含有字符'\u{3}'

/\u{3}/u.test('uuu');     // false

/\u{3}/u.test('\u{3}');   // true

从上面结果来看,开启Unicode模式时,\u和量词{3}组成了一个Unicode字符,所以结果也就完全不同了,在实际开发中,要特别注意。

讲完u修饰符之后,最后再来介绍一下ES6新增的y修饰符。

y修饰符对应的英文全称是sticky,“黏连”的意思,表示后一次的匹配目标必须紧随前一次匹配项,也就是说,多次匹配时,每次的匹配目标必须在起始位置,后一次匹配必须在前一次匹配成功的下一个位置进行。y修饰符与全局g修饰符有些相似之处,但又有区别,先来看下面例子:

var source = 'hello-hello-world';
var re1 = /hello/g;
var re2 = /hello/y;

re1.exec(source);           // ["hello"]
re2.exec(source);           // ["hello"]

console.log(re2.lastIndex); // 6

// 新一轮匹配将会在"-hello-world"中进行

re1.exec(source);           // ["hello"]
re2.exec(source);           // null

可以看到,re1是g修饰,re2是y修饰,第一轮都相同,然后我们通过正则实例的lastIndex属性获取下一轮匹配的起始位置,进而得知下一轮将会在"-hello-world"中进行匹配,由于它起始位置并不是"hello",其结果是,g仍旧匹配成功,而y匹配失败,返回null。

我们再来稍微修改一下正则,让第二次匹配时,"hello"处于起始位置,使其成功匹配:

var source = 'hello-hello-world';
var re2 = /hello-/y;

re2.exec(source);           // ["hello-"]

console.log(re2.lastIndex); // 6

// 这时新一轮匹配将会在"hello-world"中进行

re2.exec(source);           // ["hello-"]

我们还可以通过修改正则的lastIndex属性,改变新一轮匹配起始位置的索引,进而使其符合y的规则,同样可以匹配成功:

var source = 'hello-hello-world';
var re2 = /hello/y;

re2.exec(source);           // ["hello"]

// 手动更改lastIndex
re2.lastIndex = 6;

re2.exec(source);           // ["hello"]

从上面几个例子来看,y修饰符要求匹配目标必须出现在起始位置,这一点隐含了起始标记"^"的作用:

/hello/y.test('-hello');    // false

// 相当于下面这样

/^hello/.test('-hello');    // false

当y和全局g同时出现时,全局匹配将会受限,除了上面讲到的Regex#exec()方法,gy组合还会对String的replace和match两个方法起作用,全局匹配g加上y之后,匹配模式更严格了一些,每一次匹配时目标必须出现在首位:

// String#replace()

'hello-hello-world'.replace(/hello/g, 'hey');     // "hey-hey-world"

// 加上y之后,第二个hello不会被匹配和替换

'hello-hello-world'.replace(/hello/gy, 'hey');    // "hey-hello-world"

// 需要改成下面这样

'hello-hello-world'.replace(/hello-/gy, 'hey-');  // "hey-hey-world"


// String#match()

'hello-hello-world'.match(/hello/g);              // ["hello", "hello"]

'hello-hello-world'.match(/hello/gy);             // ["hello"]

'hello-hello-world'.match(/hello-/gy);            // ["hello-", "hello-"]

以上就是正则修饰符的全部内容,在下一篇中博主将会介绍元字符和高级匹配的相关内容,敬请期待。 

 

参考资料:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions

http://es6.ruanyifeng.com/#docs/regex

posted @ 2016-12-21 08:30  liuhe688  阅读(1221)  评论(0编辑  收藏  举报