Files
es6tutorial/docs/regex.md
T
2015-08-04 09:19:51 +08:00

342 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 正则的扩展
## RegExp构造函数
在ES5中,RegExp构造函数只能接受字符串作为参数。
```javascript
var regex = new RegExp("xyz", "i");
// 等价于
var regex = /xyz/i;
```
ES6允许RegExp构造函数接受正则表达式作为参数,这时会返回一个原有正则表达式的拷贝。
```javascript
var regex = new RegExp(/xyz/i);
```
如果使用RegExp构造函数的第二个参数指定修饰符,则返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
```javascript
new RegExp(/abc/ig, 'i').flags
// "i"
```
## 字符串的正则方法
字符串对象共有4个方法,可以使用正则表达式:match()、replace()、search()和split()。
ES6将这4个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。
- String.prototype.match 调用 RegExp.prototype[Symbol.match]
- String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
- String.prototype.search 调用 RegExp.prototype[Symbol.search]
- String.prototype.split 调用 RegExp.prototype[Symbol.split]
## u修饰符
ES6对正则表达式添加了u修饰符,含义为“Unicode模式”,用来正确处理大于`\uFFFF`的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。
```javascript
/^\uD83D/u.test('\uD83D\uDC2A')
// false
/^\uD83D/.test('\uD83D\uDC2A')
// true
```
上面代码中,“\uD83D\uDC2A”是一个四个字节的UTF-16编码,代表一个字符。但是,ES5不支持四个字节的UTF-16编码,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6就会识别其为一个字符,所以第一行代码结果为false。
一旦加上u修饰符号,就会修改下面这些正则表达式的行为。
**1)点字符**
点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于`0xFFFF`的Unicode字符,点字符不能识别,必须加上u修饰符。
```javascript
var s = "𠮷";
/^.$/.test(s) // false
/^.$/u.test(s) // true
```
上面代码表示,如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。
**2Unicode字符表示法**
ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上u修饰符,才能识别。
```javascript
/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true
```
上面代码表示,如果不加u修饰符,正则表达式无法识别`\u{61}`这种表示法,只会认为这匹配61个连续的u。
**3)量词**
使用u修饰符后,所有量词都会正确识别大于码点大于`0xFFFF`的Unicode字符。
```javascript
/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/𠮷{2}/.test('𠮷𠮷') // false
/𠮷{2}/u.test('𠮷𠮷') // true
```
另外,只有在使用u修饰符的情况下,Unicode表达式当中的大括号才会被正确解读,否则会被解读为量词。
```javascript
/^\u{3}$/.test('uuu') // true
```
上面代码中,由于正则表达式没有u修饰符,所以大括号被解读为量词。加上u修饰符,就会被解读为Unicode表达式。
**4)预定义模式**
u修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的Unicode字符。
```javascript
/^\S$/.test('𠮷') // false
/^\S$/u.test('𠮷') // true
```
上面代码的`\S`是预定义模式,匹配所有不是空格的字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的Unicode字符。
利用这一点,可以写出一个正确返回字符串长度的函数。
```javascript
function codePointLength(text) {
var result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
var s = "𠮷𠮷";
s.length // 4
codePointLength(s) // 2
```
**5i修饰符**
有些Unicode字符的编码不同,但是字型很相近,比如,\u004B与\u212A都是大写的K。
```javascript
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true
```
上面代码中,不加u修饰符,就无法识别非规范的K字符。
## y修饰符
除了u修饰符,ES6还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。
y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
```javascript
var s = "aaa_aa_a";
var r1 = /a+/g;
var r2 = /a+/y;
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"]
r2.exec(s) // null
```
上面代码有两个正则表达式,一个使用g修饰符,另一个使用y修饰符。这两个正则表达式各执行了两次,第一次执行的时候,两者行为相同,剩余字符串都是“_aa_a”。由于g修饰没有位置要求,所以第二次执行会返回结果,而y修饰符要求匹配必须从头部开始,所以返回null。
如果改一下正则表达式,保证每次都能头部匹配,y修饰符就会返回结果了。
```javascript
var s = "aaa_aa_a";
var r = /a+_/y;
r.exec(s) // ["aaa_"]
r.exec(s) // ["aa_"]
```
上面代码每次匹配,都是从剩余字符串的头部开始。
使用lastIndex属性,可以更好地说明y修饰符。
```javascript
const REGEX = /a/g;
REGEX.lastIndex = 2; // 指定从第三个位置y开始搜索
const match = REGEX.exec('xaya');
match.index
// 3
REGEX.lastIndex
// 4
REGEX.exec('xaxa')
// null
```
上面代码中,lastIndex属性指定每次搜索的开始位置,g修饰符从这个位置开始向后搜索,直到发现匹配为止。
y修饰符同样遵守lastIndex属性,但是要求必须在lastIndex指定的位置发现匹配。
```javascript
const REGEX = /a/y;
// 第三个位置y不匹配
REGEX.lastIndex = 2;
console.log(REGEX.exec('xaya')); // null
// 第四个位置出现匹配
REGEX.lastIndex = 3;
const match = REGEX.exec('xaxa');
match.index // 3
REGEX.lastIndex // 4
```
进一步说,y修饰符号隐含了头部匹配的标志ˆ。
```javascript
/b/y.exec("aba")
// null
```
上面代码由于不能保证头部匹配,所以返回null。y修饰符的设计本意,就是让头部匹配的标志ˆ在全局匹配中都有效。
在split方法中使用y修饰符,原字符串必须以分隔符开头。这也意味着,只要匹配成功,数组的第一个成员肯定是空字符串。
```javascript
// 没有找到匹配
'x##'.split(/#/y)
// [ 'x##' ]
// 找到两个匹配
'##x'.split(/#/y)
// [ '', '', 'x' ]
```
后续的分隔符只有紧跟前面的分隔符,才会被识别。
```javascript
'#x#'.split(/#/y)
// [ '', 'x#' ]
'##'.split(/#/y)
// [ '', '', '' ]
```
下面是字符串对象的replace方法的例子。
```javascript
const REGEX = /a/gy;
'aaxa'.replace(REGEX, '-') // '--xa'
```
上面代码中,最后一个a因为不是出现下一次匹配的头部,所以不会被替换。
如果同时使用g修饰符和y修饰符,则y修饰符覆盖g修饰符。
y修饰符的主要作用,是从字符串提取token(词元),y修饰符确保了匹配之间不会有漏掉的字符。
```javascript
function tokenize(TOKEN_REGEX, str) {
let result = [];
let match;
while (match = TOKEN_REGEX.exec(str)) {
result.push(match[1]);
}
return result;
}
const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;
const TOKEN_G = /\s*(\+|[0-9]+)\s*/g;
tokenize(TOKEN_Y, '3 + 4')
// [ '3', '+', '4' ]
tokenize(TOKEN_G, '3 + 4')
// [ '3', '+', '4' ]
```
上面代码中,如果字符串里面没有非法字符,y修饰符与g修饰符的提取结果是一样的。但是,一旦出现非法字符,两者的行为就不一样了。
```javascript
tokenize(TOKEN_Y, '3x + 4')
// [ '3' ]
tokenize(TOKEN_G, '3x + 4')
// [ '3', '+', '4' ]
```
上面代码中,g修饰符会忽略非法字符,而y修饰符不会,这样就很容易发现错误。
## sticky属性
与y修饰符相匹配,ES6的正则对象多了sticky属性,表示是否设置了y修饰符。
```javascript
var r = /hello\d/y;
r.sticky // true
```
## flags属性
ES6为正则表达式新增了flags属性,会返回正则表达式的修饰符。
```javascript
// ES5的source属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"
// ES6的flags属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'gi'
```
## RegExp.escape()
字符串必须转义,才能作为正则模式。
```javascript
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
let str = '/path/to/resource.html?search=query';
escapeRegExp(str)
// "\/path\/to\/resource\.html\?search=query"
```
上面代码中,str是一个正常字符串,必须使用反斜杠对其中的特殊字符转义,才能用来作为一个正则匹配的模式。
已经有[提议](https://esdiscuss.org/topic/regexp-escape)将这个需求标准化,作为RegExp对象的静态方法[RegExp.escape()](https://github.com/benjamingr/RexExp.escape),放入ES7。2015年7月31日,TC39认为,这个方法有安全风险,又不愿这个方法变得过于复杂,没有同意将其列入ES7,但这不失为一个真实的需求。
```javascript
RegExp.escape("The Quick Brown Fox");
// "The Quick Brown Fox"
RegExp.escape("Buy it. use it. break it. fix it.")
// "Buy it\. use it\. break it\. fix it\."
RegExp.escape("(*.*)");
// "\(\*\.\*\)"
```
字符串转义以后,可以使用RegExp构造函数生成正则模式。
```javascript
var str = 'hello. how are you?';
var regex = new RegExp(RegExp.escape(str), 'g');
assert.equal(String(regex), '/hello\. how are you\?/g');
```
目前,该方法可以用上文的escapeRegExp函数或者垫片模块[regexp.escape](https://github.com/ljharb/regexp.escape)实现。
```javascript
var escape = require('regexp.escape');
escape('hi. how are you?')
"hi\\. how are you\\?"
```