字符的 Unicode 表示法
概念
ES6允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode码点,其中的\uxxxx取值范围是\u0000到\uFFFF之间的字符;
那如果超出这个范围限制后,则需要2个\uxxxx形式来表示,而到了ES6,为了更加简洁的标识,不采用2个\uxxxx的方式了,直接采用“{码点}”的形式,即:只要将码点放入大括号,就能正确解读该字符;
案例讲解/注意事项
"\u{20BB7}" "\u{41}\u{42}\u{43}" let hello = 123; hell\u{6F} '\u{1F680}' === '\uD83D\uDE80'这里的大括号表示法与四字节的 UTF-16 编码等价 |
字符串的遍历器接口
概念
ES6 为字符串添加了Iterator遍历器接口,这样字符串可以使用for...of循环来遍历字符串;
这样的好处,除了在解构赋值的时候可以赋值给数组外,最大的优点在于可以识别大于0xFFFF的码点,这是传统的for循环无法做到的效果;
案例讲解/注意事项
let text = String.fromCodePoint(0x20BB7); for (let i = 0; i < text.length; i++) { console.log(text[i]); }这里会执行2次循环,每次循环输出一个空格 for (let i of text) { console.log(i); }这里就只输出一个字符 |
直接输入 U+2028 和 U+2029
概念
JS字符串允许直接输入字符,或者字符的转义形式,例如:var str = “中”;也可以是这种形式:var str = “\u4e2d”;
但是,有5个特殊字符,无法直接输入,只能使用转义形式,这5个特殊字符是:
U+005C:反斜杠(reverse solidus)
U+000D:回车(carriage return)
U+2028:行分隔符(line separator)
U+2029:段分隔符(paragraph separator)
U+000A:换行符(line feed)
但是,这里有个问题,就是JSON格式的字符串,这种格式的字符串是允许直接使用U+2028(行分隔符)和U+2029(段分隔符),但这会导致JSON.parse解析报错;
为了解决这个问题,ES6允许字符串直接输入U+2028(行分隔符)和 U+2029(段分隔符)
案例讲解/注意事项
const json = '"\u2028"'; JSON.parse(json); // 可能报错 const PS = eval("'\u2028'"); |
JSON.stringify() 的改造
概念
按照标准,JSON 数据必须是 UTF-8 编码,但JSON.stringify()方法有可能返回不符合 UTF-8 标准的字符串,这是因为UTF-8标准规定,0xD800到0xDFFF之间的码点,不能单独使用,必须配对使用,而JSON.stringify()的问题在于,它可能返回0xD800到0xDFFF之间的单个码点;
既然知道原因,为了确保返回的是合法的UTF-8字符,ES6改变了JSON.stringify()的行为,如果遇到0xD800到0xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用决定下一步的处理。
模板字符串
概念
在JS中拼接HTML输出,是非常复杂的,都是通过单双引号、+来进行拼接字符串,一旦单双引号没有配对,就会报错,再加上HTML标签的属性都是引号包裹,既费时,又费力;
在ES6中引入了“模板字符串”的概念来解决这个问题,个人感觉跟JAVA用到的模板引擎一个思路,都是解析特定的标识符;
模板字符串是增强版的字符串,用反引号(`)标识;
案例讲解/注意事项
可以表示普通字符串 `In JavaScript '\n' is a line-feed.`
可以表示多行字符串 `In JavaScript this is not legal.`
可以在字符串中嵌入变量 let name = "Bob", time = "today"; `Hello ${name}, how are you ${time}?`
可以在模板字符串中使用反引号,则前面要用反斜杠转义 let greeting = `\`Yo\` World!`;
注意:如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中 $('#list').html(` <ul> <li>first</li> <li>second</li> </ul> `);
模板字符串中嵌入变量,需要将变量名写在${}之中 `User ${user.name} is not authorized to do ${action}.`);
大括号内部可以放入任意的JS表达式 `${x} + ${y} = ${x + y}` `${x} + ${y * 2} = ${x + y * 2}` `${obj.x + obj.y}`这里调用的是obj的x属性、y属性
模板字符串之中还能调用函数 `foo ${fn()} bar` 函数名叫做fn()
如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法;如果模板字符串中的变量没有声明,将报错;
模板字符串允许嵌套 const tmpl = addrs => ` <table> ${addrs.map(addr => ` <tr><td>${addr.first}</td></tr> <tr><td>${addr.last}</td></tr> `).join('')} </table> `;
如果需要引用模板字符串本身,在需要时执行,可以写成函数,来达到简介引用的效果 let func = (name) => `Hello ${name}!`; func('Jack') |
标签模板
概念
模板字符串还可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串,这被称为标签模板功能;
从定义上来讲,标签模板是函数调用的一种特殊形式,所谓的“标签”指的是函数,而紧跟在后面的模板字符串就是它的参数;
那问题来了,模板字符串作为参数,该如何拆解呢,这里主要有2种情况:
①模板字符串就是一个纯字符串;
②模板字符串包含变量;
案例讲解/注意事项
模板字符串包含变量时,先把模板字符串拆分成多个参数,再去调用函数: let a = 5; let b = 10; tag`Hello ${ a + b } world ${ a * b }`; 这里的模板字符串Hello ${ a + b } world ${ a * b }被拆分成3部分,分别是['Hello ', ' world ', '']、15、50,因此,传入tag方法的参数也就变成了3个,此刻tag方法为: function tag(stringArr, value1, value2){ ... } // 等同于 function tag(stringArr, ...values){ ... } 然后我们来分析下过程: ①tag函数的第一个参数是一个数组,该数组的成员就是模板字符串中那些没有被变量替换的部分,你可以把变量理解为关键字,然后模板字符串通过split来分割字符串,然后split方法返回一个数组,这样,数组变量之间就肯定隔着一个变量,然后依次轮退,例如:hello与world之前,world与空字符串之间; ②后续的可变参数,就是对应着的用于split分割的变量; ③最后tag的调用形式是:tag(['Hello ',' world ', ''], 15, 50) ④整个表达式的返回值,就是tag函数处理模板字符串后的返回值;
下面有2个经典案例: 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; } // "The total is 30 (31.5 with tax)" function passthru(literals, ...values) { let output = ""; let index; for (index = 0; index < values.length; index++) { output += literals[index] + values[index]; }
output += literals[index] return output; } |
注意:
如果模板字符串是纯字符串,那模板字符串也会作为数组传入参数中,只不过该数组的length=1;
如果模板字符串包含变量,则先把模板字符串用变量分割成数组,然后传入参数中;
重点在于:数组有个raw属性,该raw属性指向的也是一个数组,且raw属性指向的数组保存了转义后的原字符串,raw属性数组与参数数组的区别在于:raw指向的数组会对字符进行转义;
console.log`123` //这里的123,虽然是纯字符串,但其还是作为数组来传入log方法中,所以应该是这样的:["123", raw: Array[1]],raw属性也指向了一个数组,且该数组长度为1;
tag`First line\nSecond line` function tag(strings) { console.log(strings.raw[0]); // strings.raw[0] 为 "First line\\nSecond line" // 打印输出 "First line\nSecond line" } tag函数的第一个参数strings是一个数组,该数组的raw属性也指向了一个数组,且该数组的成员与strings数组完全一致, 比如,strings数组是["First line\nSecond line"],那么strings.raw数组就是["First line\\nSecond line"]。两者唯一的区别,就是字符串里面的斜杠都被转义了。 比如,strings.raw 数组会将\n视为\\和n两个字符,而不是换行符。这是为了方便取得转义之前的原始模板而设计的。 |
扩展
①过滤HTML字符串,防止用户输入恶意内容:
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
function SaferHTML(templateData) { let s = templateData[0]; for (let i = 1; i < arguments.length; i++) { let arg = String(arguments[i]);
s += arg.replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">");
s += templateData[i]; } return s; } 这里的arguments代表的就是function对象的arguments属性,只不过该属性也是对象,然后arguments从1开始遍历,获取用户输入的内容,然后再用正则表达式对关键字符进行转义 |
②多语言转换(国际化处理)
i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!` 这里可以通过i18n来获取地区信息,进而实现国际化效果 |
③可以自定义实现类似于Mustache模板库的if、for功能
let libraryHtml = hashTemplate` <ul> #for book in ${myBooks} <li><i>#{book.title}</i> by #{book.author}</li> #end </ul> `; |
④可以嵌入其他语言:
jsx` <div> <input ref='input' onChange='${this.handleChange}' defaultValue='${this.state.value}' /> ${this.state.value} </div> ` jsx函数,将一个 DOM 字符串转为 React 对象。你可以在 GitHub 找到jsx函数的具体实现; |
模板字符串的限制
概念
可以使用标签模板来在JS中嵌入其他语言,但这里有个细节,就是其他语言的语法,可能与JS的规范发生冲突,特别是与JS的模板字符串的规范发生冲突;
为了解决这个问题,ES2018放松了对标签模板里面的字符串转义的限制,如果遇到不合法的字符串转义,就返回undefined,而不是像之前报错,且从raw属性上面可以得到原始字符串;
本来应该报错的地方,ES6由于放松了对字符串转义的限制,所以引擎无法转义字符时,则返回为undefined,但raw属性依然可以得到原始字符串,进而可以对原字符串进行处理。
注意:
这种放松管制的场景,仅仅发生在标签模板解析字符串时有效,其他场合依然会报错;
案例讲解/注意事项
function latex(strings) { // ... }
let document = latex` \newcommand{\fun}{\textbf{Fun!}} // 正常工作 \newcommand{\unicode}{\textbf{Unicode!}} // 报错 \newcommand{\xerxes}{\textbf{King!}} // 报错
Breve over the h goes \u{h}ere // 报错 ` 这里作者想在标签模板里面可以嵌入LaTEX语言,虽然这些语法符合LaTEX规范,但是不符合JS的模板字符串的规范,在对\unicode进行Unicode转义、对\xerxes按照十六进制字符串转义时抛出异常;
本来在LaTEX正常的语法,却在JS中按照特定编码进行了转义,且转义过程报错,到了ES2018放松了管制,对于这种无法转义的情况,统一返回undefined,而不是报错
再看一个例子: function tag(strs) { strs[0] === undefined strs.raw[0] === "\\unicode and \\u{55}"; } tag`\unicode and \u{55}` 这里的原始数据是\unicode and \u{55},当ES2018进行转义时,会因为\unicode按照Unicode转义,但是转义失败,把整个\unicode and \u{55}转义结果变成了undefined,所以,这才导致了strs[0] = undefined; 而raw只是对某个特定字符转义,所以strs.raw[0] = \\unicode and \\u{55}
let bad = `bad escape sequence: \unicode`; // 报错 只对标签模板发送管制,其他场景无效; |