|
| 1 | +# 正则的扩展 |
| 2 | + |
| 3 | +## RegExp构造函数 |
| 4 | + |
| 5 | +在ES5中,RegExp构造函数只能接受字符串作为参数。 |
| 6 | + |
| 7 | +```javascript |
| 8 | +var regex = new RegExp("xyz", "i"); |
| 9 | +// 等价于 |
| 10 | +var regex = /xyz/i; |
| 11 | +``` |
| 12 | + |
| 13 | +ES6允许RegExp构造函数接受正则表达式作为参数,这时会返回一个原有正则表达式的拷贝。 |
| 14 | + |
| 15 | +```javascript |
| 16 | +var regex = new RegExp(/xyz/i); |
| 17 | +``` |
| 18 | + |
| 19 | +如果使用RegExp构造函数的第二个参数指定修饰符,则返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。 |
| 20 | + |
| 21 | +```javascript |
| 22 | +new RegExp(/abc/ig, 'i').flags |
| 23 | +// "i" |
| 24 | +``` |
| 25 | + |
| 26 | +## 字符串的正则方法 |
| 27 | + |
| 28 | +字符串对象共有4个方法,可以使用正则表达式。 |
| 29 | + |
| 30 | +ES6将这4个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。 |
| 31 | + |
| 32 | +- String.prototype.match 调用 RegExp.prototype[Symbol.match] |
| 33 | +- String.prototype.replace 调用 RegExp.prototype[Symbol.replace] |
| 34 | +- String.prototype.search 调用 RegExp.prototype[Symbol.search] |
| 35 | +- String.prototype.split 调用 RegExp.prototype[Symbol.split] |
| 36 | + |
| 37 | +## u修饰符 |
| 38 | + |
| 39 | +ES6对正则表达式添加了u修饰符,含义为“Unicode模式”,用来正确处理大于`\uFFFF`的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。 |
| 40 | + |
| 41 | +```javascript |
| 42 | +/^\uD83D/u.test('\uD83D\uDC2A') |
| 43 | +// false |
| 44 | +/^\uD83D/.test('\uD83D\uDC2A') |
| 45 | +// true |
| 46 | +``` |
| 47 | + |
| 48 | +上面代码中,“\uD83D\uDC2A”是一个四个字节的UTF-16编码,代表一个字符。但是,ES5不支持四个字节的UTF-16编码,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6就会识别其为一个字符,所以第一行代码结果为false。 |
| 49 | + |
| 50 | +一旦加上u修饰符号,就会修改下面这些正则表达式的行为。 |
| 51 | + |
| 52 | +**(1)点字符** |
| 53 | + |
| 54 | +点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于`0xFFFF`的Unicode字符,点字符不能识别,必须加上u修饰符。 |
| 55 | + |
| 56 | +```javascript |
| 57 | +var s = "𠮷"; |
| 58 | + |
| 59 | +/^.$/.test(s) // false |
| 60 | +/^.$/u.test(s) // true |
| 61 | +``` |
| 62 | + |
| 63 | +上面代码表示,如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。 |
| 64 | + |
| 65 | +**(2)Unicode字符表示法** |
| 66 | + |
| 67 | +ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上u修饰符,才能识别。 |
| 68 | + |
| 69 | +```javascript |
| 70 | +/\u{61}/.test('a') // false |
| 71 | +/\u{61}/u.test('a') // true |
| 72 | +/\u{20BB7}/u.test('𠮷') // true |
| 73 | +``` |
| 74 | + |
| 75 | +上面代码表示,如果不加u修饰符,正则表达式无法识别`\u{61}`这种表示法,只会认为这匹配61个连续的u。 |
| 76 | + |
| 77 | +**(3)量词** |
| 78 | + |
| 79 | +使用u修饰符后,所有量词都会正确识别大于码点大于`0xFFFF`的Unicode字符。 |
| 80 | + |
| 81 | +```javascript |
| 82 | +/a{2}/.test('aa') // true |
| 83 | +/a{2}/u.test('aa') // true |
| 84 | +/𠮷{2}/.test('𠮷𠮷') // false |
| 85 | +/𠮷{2}/u.test('𠮷𠮷') // true |
| 86 | +``` |
| 87 | + |
| 88 | +另外,只有在使用u修饰符的情况下,Unicode表达式当中的大括号才会被正确解读,否则会被解读为量词。 |
| 89 | + |
| 90 | +```javascript |
| 91 | +/^\u{3}$/.test('uuu') // true |
| 92 | +``` |
| 93 | + |
| 94 | +上面代码中,由于正则表达式没有u修饰符,所以大括号被解读为量词。加上u修饰符,就会被解读为Unicode表达式。 |
| 95 | + |
| 96 | +**(4)预定义模式** |
| 97 | + |
| 98 | +u修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的Unicode字符。 |
| 99 | + |
| 100 | +```javascript |
| 101 | +/^\S$/.test('𠮷') // false |
| 102 | +/^\S$/u.test('𠮷') // true |
| 103 | +``` |
| 104 | + |
| 105 | +上面代码的`\S`是预定义模式,匹配所有不是空格的字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的Unicode字符。 |
| 106 | + |
| 107 | +利用这一点,可以写出一个正确返回字符串长度的函数。 |
| 108 | + |
| 109 | +```javascript |
| 110 | +function codePointLength(text) { |
| 111 | + var result = text.match(/[\s\S]/gu); |
| 112 | + return result ? result.length : 0; |
| 113 | +} |
| 114 | + |
| 115 | +var s = "𠮷𠮷"; |
| 116 | + |
| 117 | +s.length // 4 |
| 118 | +codePointLength(s) // 2 |
| 119 | +``` |
| 120 | + |
| 121 | +**(5)i修饰符** |
| 122 | + |
| 123 | +有些Unicode字符的编码不同,但是字型很相近,比如,\u004B与\u212A都是大写的K。 |
| 124 | + |
| 125 | +```javascript |
| 126 | +/[a-z]/i.test('\u212A') // false |
| 127 | +/[a-z]/iu.test('\u212A') // true |
| 128 | +``` |
| 129 | + |
| 130 | +上面代码中,不加u修饰符,就无法识别非规范的K字符。 |
| 131 | + |
| 132 | +## y修饰符 |
| 133 | + |
| 134 | +除了u修饰符,ES6还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。 |
| 135 | + |
| 136 | +y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。 |
| 137 | + |
| 138 | +```javascript |
| 139 | +var s = "aaa_aa_a"; |
| 140 | +var r1 = /a+/g; |
| 141 | +var r2 = /a+/y; |
| 142 | + |
| 143 | +r1.exec(s) // ["aaa"] |
| 144 | +r2.exec(s) // ["aaa"] |
| 145 | + |
| 146 | +r1.exec(s) // ["aa"] |
| 147 | +r2.exec(s) // null |
| 148 | +``` |
| 149 | + |
| 150 | +上面代码有两个正则表达式,一个使用g修饰符,另一个使用y修饰符。这两个正则表达式各执行了两次,第一次执行的时候,两者行为相同,剩余字符串都是“_aa_a”。由于g修饰没有位置要求,所以第二次执行会返回结果,而y修饰符要求匹配必须从头部开始,所以返回null。 |
| 151 | + |
| 152 | +如果改一下正则表达式,保证每次都能头部匹配,y修饰符就会返回结果了。 |
| 153 | + |
| 154 | +```javascript |
| 155 | +var s = "aaa_aa_a"; |
| 156 | +var r = /a+_/y; |
| 157 | + |
| 158 | +r.exec(s) // ["aaa_"] |
| 159 | +r.exec(s) // ["aa_"] |
| 160 | +``` |
| 161 | + |
| 162 | +上面代码每次匹配,都是从剩余字符串的头部开始。 |
| 163 | + |
| 164 | +使用lastIndex属性,可以更好地说明y修饰符。 |
| 165 | + |
| 166 | +```javascript |
| 167 | +const REGEX = /a/g; |
| 168 | + |
| 169 | +REGEX.lastIndex = 2; // 指定从第三个位置y开始搜索 |
| 170 | +const match = REGEX.exec('xaya'); |
| 171 | + |
| 172 | +match.index |
| 173 | +// 3 |
| 174 | +REGEX.lastIndex |
| 175 | +// 4 |
| 176 | +REGEX.exec('xaxa') |
| 177 | +// null |
| 178 | +``` |
| 179 | + |
| 180 | +上面代码中,lastIndex属性指定每次搜索的开始位置,g修饰符从这个位置开始向后搜索,直到发现匹配为止。 |
| 181 | + |
| 182 | +y修饰符同样遵守lastIndex属性,但是要求必须在lastIndex指定的位置发现匹配。 |
| 183 | + |
| 184 | +```javascript |
| 185 | +const REGEX = /a/y; |
| 186 | + |
| 187 | +// 第三个位置y不匹配 |
| 188 | +REGEX.lastIndex = 2; |
| 189 | +console.log(REGEX.exec('xaya')); // null |
| 190 | + |
| 191 | +// 第四个位置出现匹配 |
| 192 | +REGEX.lastIndex = 3; |
| 193 | +const match = REGEX.exec('xaxa'); |
| 194 | +match.index // 3 |
| 195 | +REGEX.lastIndex // 4 |
| 196 | +``` |
| 197 | + |
| 198 | +进一步说,y修饰符号隐含了头部匹配的标志ˆ。 |
| 199 | + |
| 200 | +```javascript |
| 201 | +/b/y.exec("aba") |
| 202 | +// null |
| 203 | +``` |
| 204 | + |
| 205 | +上面代码由于不能保证头部匹配,所以返回null。y修饰符的设计本意,就是让头部匹配的标志ˆ在全局匹配中都有效。 |
| 206 | + |
| 207 | +在split方法中使用y修饰符,原字符串必须以分隔符开头。这也意味着,只要匹配成功,数组的第一个成员肯定是空字符串。 |
| 208 | + |
| 209 | +```javascript |
| 210 | +// 没有找到匹配 |
| 211 | +'x##'.split(/#/y) |
| 212 | +// [ 'x##' ] |
| 213 | + |
| 214 | +// 找到两个匹配 |
| 215 | +'##x'.split(/#/y) |
| 216 | +// [ '', '', 'x' ] |
| 217 | +``` |
| 218 | + |
| 219 | +后续的分隔符只有紧跟前面的分隔符,才会被识别。 |
| 220 | + |
| 221 | +```javascript |
| 222 | +'#x#'.split(/#/y) |
| 223 | +// [ '', 'x#' ] |
| 224 | + |
| 225 | +'##'.split(/#/y) |
| 226 | +// [ '', '', '' ] |
| 227 | +``` |
| 228 | + |
| 229 | +下面是字符串对象的replace方法的例子。 |
| 230 | + |
| 231 | +```javascript |
| 232 | +const REGEX = /a/gy; |
| 233 | +'aaxa'.replace(REGEX, '-') // '--xa' |
| 234 | +``` |
| 235 | + |
| 236 | +上面代码中,最后一个a因为不是出现下一次匹配的头部,所以不会被替换。 |
| 237 | + |
| 238 | +如果同时使用g修饰符和y修饰符,则y修饰符覆盖g修饰符。 |
| 239 | + |
| 240 | +y修饰符的主要作用,是从字符串提取token(词元),y修饰符确保了匹配之间不会有漏掉的字符。 |
| 241 | + |
| 242 | +```javascript |
| 243 | +function tokenize(TOKEN_REGEX, str) { |
| 244 | + let result = []; |
| 245 | + let match; |
| 246 | + while (match = TOKEN_REGEX.exec(str)) { |
| 247 | + result.push(match[1]); |
| 248 | + } |
| 249 | + return result; |
| 250 | +} |
| 251 | + |
| 252 | +const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y; |
| 253 | +const TOKEN_G = /\s*(\+|[0-9]+)\s*/g; |
| 254 | + |
| 255 | +tokenize(TOKEN_Y, '3 + 4') |
| 256 | +// [ '3', '+', '4' ] |
| 257 | +tokenize(TOKEN_G, '3 + 4') |
| 258 | +// [ '3', '+', '4' ] |
| 259 | +``` |
| 260 | + |
| 261 | +上面代码中,如果字符串里面没有非法字符,y修饰符与g修饰符的提取结果是一样的。但是,一旦出现非法字符,两者的行为就不一样了。 |
| 262 | + |
| 263 | +```javascript |
| 264 | +tokenize(TOKEN_Y, '3x + 4') |
| 265 | +// [ '3' ] |
| 266 | +tokenize(TOKEN_G, '3x + 4') |
| 267 | +// [ '3', '+', '4' ] |
| 268 | +``` |
| 269 | + |
| 270 | +上面代码中,g修饰符会忽略非法字符,而y修饰符不会,这样就很容易发现错误。 |
| 271 | + |
| 272 | +## sticky属性 |
| 273 | + |
| 274 | +与y修饰符相匹配,ES6的正则对象多了sticky属性,表示是否设置了y修饰符。 |
| 275 | + |
| 276 | +```javascript |
| 277 | +var r = /hello\d/y; |
| 278 | +r.sticky // true |
| 279 | +``` |
| 280 | + |
| 281 | +## flags属性 |
| 282 | + |
| 283 | +ES6为正则表达式新增了flags属性,会返回正则表达式的修饰符。 |
| 284 | + |
| 285 | +```javascript |
| 286 | +// ES5的source属性 |
| 287 | +// 返回正则表达式的正文 |
| 288 | +/abc/ig.source |
| 289 | +// "abc" |
| 290 | + |
| 291 | +// ES6的flags属性 |
| 292 | +// 返回正则表达式的修饰符 |
| 293 | +/abc/ig.flags |
| 294 | +// 'gi' |
| 295 | +``` |
| 296 | + |
| 297 | +## RegExp.escape() |
| 298 | + |
| 299 | +字符串必须转义,才能作为正则模式。 |
| 300 | + |
| 301 | +```javascript |
| 302 | +function escapeRegExp(str) { |
| 303 | + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); |
| 304 | +} |
| 305 | + |
| 306 | +let str = '/path/to/resource.html?search=query'; |
| 307 | +escapeRegExp(str) |
| 308 | +// "\/path\/to\/resource\.html\?search=query" |
| 309 | +``` |
| 310 | + |
| 311 | +上面代码中,str是一个正常字符串,必须使用反斜杠对其中的特殊字符转义,才能用来作为一个正则匹配的模式。 |
| 312 | + |
| 313 | +已经有[提议](https://esdiscuss.org/topic/regexp-escape)将这个需求标准化,作为RegExp对象的静态方法[`RegExp.escape()`](https://github.com/benjamingr/RexExp.escape),放入ES7。2015年7月31日,TC39认为,这个方法有安全风险,又不愿这个方法变得过于复杂,没有同意将其列入ES7,但这不失为一个真实的需求。 |
| 314 | + |
| 315 | +```javascript |
| 316 | +RegExp.escape("The Quick Brown Fox"); |
| 317 | +// "The Quick Brown Fox" |
| 318 | + |
| 319 | +RegExp.escape("Buy it. use it. break it. fix it.") |
| 320 | +// "Buy it\. use it\. break it\. fix it\." |
| 321 | + |
| 322 | +RegExp.escape("(*.*)"); |
| 323 | +// "\(\*\.\*\)" |
| 324 | +``` |
| 325 | + |
| 326 | +字符串转义以后,可以使用RegExp构造函数生成正则模式。 |
| 327 | + |
| 328 | +```javascript |
| 329 | +var str = 'hello. how are you?'; |
| 330 | +var regex = new RegExp(RegExp.escape(str), 'g'); |
| 331 | +assert.equal(String(regex), '/hello\. how are you\?/g'); |
| 332 | +``` |
| 333 | + |
| 334 | +目前,该方法可以用上文的escapeRegExp函数或者垫片模块[`regexp.escape`](https://github.com/ljharb/regexp.escape)实现。 |
| 335 | + |
| 336 | +```javascript |
| 337 | +var escape = require('regexp.escape'); |
| 338 | +escape('hi. how are you?') |
| 339 | +"hi\\. how are you\\?" |
| 340 | +``` |
| 341 | + |
0 commit comments