字符串的扩展
1.字符的 Unicode 表示法
允许采用\uxxxx的形式表示一个字符,xxxx表示Unicode码点,超出\u0000~\uFFFF之间的字符必须用两个双字节形式表示。但是如果将码点放入大括号,就可正确解读字符。
"\uD842\uDFB7"
// "𠮷"
"\u20BB7"
// " 7"
"\u{20BB7}"
// "𠮷"
JavaScript表示一个字符:
'\z' === 'z' // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true
2.字符串的遍历器接口
字符串可以被for…of循环遍历,且可以识别大于0xFFFF的码点。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
let text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
console.log(text[i]);
}
// " "
// " "
for (let i of text) {
console.log(i);
}
// "𠮷"
text只有一个字符,但是for循环会认为它包含两个字符(都不可打印),而for…of循环可以正确识别出这个字符。
3.直接输入U+2028和U+2029
JavaScript 字符串允许直接输入字符,以及输入字符的转义形式。但是,JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式。
U+005C:反斜杠
U+000D:回车
U+2028:行分隔符
U+2029:段分隔符
U+000A:换行符
JSON 格式允许字符串里面直接使用 U+2028(行分隔符)和 U+2029(段分隔符)
ES2019 允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符)。
4.JSON.stringify() 的改造
为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD800到0xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。
JSON.stringify('\u{D834}') // ""\\uD834""
JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834""
5.模板字符串
模板字符串是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量,模板字符串中嵌入变量,需要将变量名写在${}之中。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义,所有的空格和缩进都会被保留在输出之中。如果你不想要换行,可以使用trim方法消除它。
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`.trim());
如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。
模板字符串的嵌套:
const tmpl = addrs => `
<table>
${addrs.map(addr => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`).join('')}
</table>
`;
const data = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];
console.log(tmpl(data));
// <table>
//
// <tr><td><Jane></td></tr>
// <tr><td>Bond</td></tr>
//
// <tr><td>Lars</td></tr>
// <tr><td><Croft></td></tr>
//
// </table>
6.标签模板
标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。
let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
let a = 5;
let b = 10;
function tag(s, v1, v2) {
console.log(s[0]);
console.log(s[1]);
console.log(s[2]);
console.log(v1);
console.log(v2);
return "OK";
}
tag`Hello ${ a + b } world ${ a * b}`;
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"
一个复杂的例子
let total = 30;
let msg = passthru`The total is ${total} (${total*1.05} with tax)`;
function passthru(literals) {
let result = '';
let i = 0;
while (i < literals.length) { //循环了三遍,因为第一个参数数组中有三个
result += literals[i++];
if (i < arguments.length) { //有两个变量
result += arguments[i];
}
}
return result;
}
msg // "The total is 30 (31.5 with tax)"
“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
标签模板的另一个应用,就是多语言转换(国际化处理)。
在 JavaScript 语言之中嵌入其他语言。
模板处理函数的第一个参数(模板字符串数组),还有一个raw属性,保存的是转义后的原字符串。
tag`First line\nSecond line`
function tag(strings) {
console.log(strings.raw[0]);
// strings.raw[0] 为 "First line\\nSecond line"
// 打印输出 "First line\nSecond line"
}
正则的扩展
1.RegExp 构造函数
如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
new RegExp(/abc/ig, 'i').flags
// "i"
//原有正则对象的修饰符是ig,它会被第二个参数i覆盖
2.字符串的正则方法
字符串对象共有 4 个方法,可以使用正则表达式:match()、replace()、search()和split()。
3.u修饰符
ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。
(1)点字符
(2)Unicode 字符表示法
(3)量词
(4)预定义模式
(5)i修饰符
(6)转义
4.Unicode 字符表示法
正则实例对象新增unicode属性,表示是否设置了u修饰符
const r1 = /hello/;
const r2 = /hello/u;
r1.unicode // false
r2.unicode // true
5.y修饰符
y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
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
y修饰符的一个应用,是从字符串提取 token(词元),y修饰符确保了匹配之间不会有漏掉的字符
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' ]
function tokenize(TOKEN_REGEX, str) {
let result = [];
let match;
while (match = TOKEN_REGEX.exec(str)) {
result.push(match[1]);
}
return result;
}
6.RegExp.prototype.sticky 属性
表示是否设置了y修饰符
var r = /hello\d/y;
r.sticky // true
7.RegExp.prototype.flags 属性
返回正则表达式的修饰符
// ES5 的 source 属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"
// ES6 的 flags 属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'gi'
8.s 修饰符:dotAll 模式
点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符。
换行符(\n)
回车符(\r)
U+2028 行分隔符
U+2029 段分隔符
/foo.bar/.test('foo\nbar')
// false
//因为点不能匹配\n
引入s修饰符,使得.可以匹配任意单个字符,点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。
const re = /foo.bar/s;
// 另一种写法
// const re = new RegExp('foo.bar', 's');
re.test('foo\nbar') // true
re.dotAll // true
re.flags // 's'
9.后行断言
x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=$)\d+/。“后行否定断言”则与“先行否定断言”相反,x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!$)\d+/。
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90') // ["90"]
“先右后左”与所有其他正则操作相反,导致了一些不符合预期的行为
(1)后行断言的组匹配,与正常情况下结果是不一样的
(2)反斜杠引用,也与通常的顺序相反,必须放在对应的那个括号之前
/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]
/(?<=(o)d\1)r/.exec('hodor') // null
/(?<=\1d(o))r/.exec('hodor') // ["r", "o"]
10.Unicode 属性类
\p{…}和\P{…},允许正则表达式匹配符合 Unicode 某种属性的所有字符,使用的时候一定要加上u修饰符。
Unicode 属性类要指定属性名和属性值
\p{UnicodePropertyName=UnicodePropertyValue}
const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true
\P{…}是\p{…}的反向匹配,即匹配不满足条件的字符
11.具名组匹配
“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(?< year>),然后就可以在exec方法返回结果的groups属性上引用该组名。同时,数字序号(matchObj[1])依然有效。
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31
如果具名组没有匹配,那么对应的groups对象属性会是undefined,但键名在groups是始终存在的。
(1)解构赋值和替换
有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one // foo
two // bar
字符串替换时,使用$<组名>引用具名组
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
// '02/01/2015'
(2)引用
如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法
数字引用(\1)依然有效
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
RE_TWICE.test('abc!abc!abc') // true
RE_TWICE.test('abc!abc!ab') // false
12.正则匹配索引
exec()方法的返回结果加上indices属性,在这个属性上面可以拿到匹配的开始位置和结束位置
const text = 'zabbcdef';
const re = /ab/;
const result = re.exec(text);
result.index // 1
result.indices // [ [1, 3] ]
开始位置包含在匹配结果之中,但是结束位置不包含在匹配结果之中
多组匹配:
const text = 'zabbcdef';
const re = /ab+(cd(ef))/;
const result = re.exec(text);
result.indices // [ [1, 8], [4, 8], [6, 8] ]
如果正则表达式包含具名组匹配,indices属性数组还会有一个groups属性
const text = 'zabbcdef';
const re = /ab+(?<Z>cd)/;
const result = re.exec(text);
result.indices.groups // { Z: [ 4, 6 ] }
如果获取组匹配不成功,indices属性数组的对应成员则为undefined,indices.groups属性对象的对应成员也是undefined。
13.String.prototype.matchAll()
如果一个正则表达式在字符串里面有多个匹配,现在一般使用g修饰符或y修饰符,在循环里面逐一取出,String.prototype.matchAll()方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器,而不是数组。
const string = 'test1test2test3';
const regex = /t(e)(st(\d?))/g;
for (const match of string.matchAll(regex)) {
console.log(match);
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
遍历器转为数组:
(1)使用…运算符
(2)Array.from()方法