模板字面量

在这一节的最开始,我将不得不呼唤这个ES6特性的极其……误导人的名称,这要看在你的经验中 模板(template) 一词的含义是什么。

许多开发者认为模板是一段可复用的,可重绘的文本,就像大多数模板引擎(Mustache,Handlebars,等等)提供的能力那样。ES6中使用的 模板 一词暗示着相似的东西,就像一种声明可以被重绘的内联模板字面量的方法。然而,这根本不是考虑这个特性的正确方式。

所以,在我们继续之前,我把它重命名为它本应被称呼的名字:插值型字符串字面量(或者略称为 插值型字面量)。

你已经十分清楚地知道了如何使用"'分隔符来声明字符串字面量,而且你还知道它们不是(像有些语言中拥有的)内容将被解析为插值表达式的 智能字符串

但是,ES6引入了一种新型的字符串字面量,使用反引号` 作为分隔符。这些字符串字面量允许嵌入基本的字符串插值表达式,之后这些表达式自动地被解析和求值。

这是老式的前ES6方式:

  1. var name = "Kyle";
  2. var greeting = "Hello " + name + "!";
  3. console.log( greeting ); // "Hello Kyle!"
  4. console.log( typeof greeting ); // "string"

现在,考虑这种新的ES6方式:

  1. var name = "Kyle";
  2. var greeting = `Hello ${name}!`;
  3. console.log( greeting ); // "Hello Kyle!"
  4. console.log( typeof greeting ); // "string"

如你所见,我们在一系列被翻译为字符串字面量的字符周围使用了`..` ,但是${..}形式中的任何表达式都将立即内联地被解析和求值。称呼这样的解析和求值的高大上名词就是 插值(interpolation)(比模板要准确多了)。

被插值的字符串字面量表达式的结果只是一个老式的普通字符串,赋值给变量greeting

警告: typeof greeting == "string"展示了为什么不将这些实体考虑为特殊的模板值很重要,因为你不能将这种字面量的未求值形式赋值给某些东西并复用它。`..` 字符串字面量在某种意义上更像是IIFE,因为它自动内联地被求值。`..` 字符串字面量的结果只不过是一个简单的字符串。

插值型字符串字面量的一个真正的好处是他们允许被分割为多行:

  1. var text =
  2. `Now is the time for all good men
  3. to come to the aid of their
  4. country!`;
  5. console.log( text );
  6. // Now is the time for all good men
  7. // to come to the aid of their
  8. // country!

在插值型字符串字面量中的换行将会被保留在字符串值中。

除非在字面量值中作为明确的转义序列出现,回车字符\r(编码点U+000D)的值或者回车+换行序列\r\n(编码点U+000DU+000A)的值都会被泛化为一个换行字符\n(编码点U+000A)。但不要担心;这种泛化很少见而且很可能仅会在你将文本拷贝粘贴到JS文件中时才会发生。

插值表达式

在一个插值型字符串字面量中,任何合法的表达式都被允许出现在${..}内部,包括函数调用,内联函数表达式调用,甚至是另一个插值型字符串字面量!

考虑如下代码:

  1. function upper(s) {
  2. return s.toUpperCase();
  3. }
  4. var who = "reader";
  5. var text =
  6. `A very ${upper( "warm" )} welcome
  7. to all of you ${upper( `${who}s` )}!`;
  8. console.log( text );
  9. // A very WARM welcome
  10. // to all of you READERS!

当我们组合变量who与字符串s时, 相对于who + "s",这里的内部插值型字符串字面量`${who}s` 更方便一些。有些情况下嵌套的插值型字符串字面量是有用的,但是如果你发现自己做这样的事情太频繁,或者发现你自己嵌套了好几层时,你就要小心一些。

如果确实有这样情况,你的字符串你值生产过程很可能可以从某些抽象中获益。

警告: 作为一个忠告,使用这样的新发现的力量时要非常小心你代码的可读性。就像默认值表达式和解构赋值表达式一样,仅仅因为你 做某些事情,并不意味着你 应该 做这些事情。在使用新的ES6技巧时千万不要做过了头,使你的代码比你或者你的其他队友聪明。

表达式作用域

关于作用域的一个快速提醒是它用于解析表达式中的变量时。我早先提到过一个插值型字符串字面量与IIFE有些相像,事实上这也可以考虑为作用域行为的一种解释。

考虑如下代码:

  1. function foo(str) {
  2. var name = "foo";
  3. console.log( str );
  4. }
  5. function bar() {
  6. var name = "bar";
  7. foo( `Hello from ${name}!` );
  8. }
  9. var name = "global";
  10. bar(); // "Hello from bar!"

在函数bar()内部,字符串字面量`..` 被表达的那一刻,可供它查找的作用域发现变量的name的值为"bar"。既不是全局的name也不是foo(..)name。换句话说,一个插值型字符串字面量在它出现的地方是词法作用域的,而不是任何方式的动态作用域。

标签型模板字面量

再次为了合理性而重命名这个特性:标签型字符串字面量

老实说,这是一个ES6提供的更酷的特性。它可能看起来有点儿奇怪,而且也许一开始看起来一般不那么实用。但一旦你花些时间在它上面,标签型字符串字面量的用处可能会令你惊讶。

例如:

  1. function foo(strings, ...values) {
  2. console.log( strings );
  3. console.log( values );
  4. }
  5. var desc = "awesome";
  6. foo`Everything is ${desc}!`;
  7. // [ "Everything is ", "!"]
  8. // [ "awesome" ]

让我们花点儿时间考虑一下前面的代码段中发生了什么。首先,跳出来的最刺眼的东西就是foo`Everything...`;。它看起来不像是任何我们曾经见过的东西。不是吗?

它实质上是一种不需要( .. )的特殊函数调用。标签 —— 在字符串字面量`..` 之前的foo部分 —— 是一个应当被调用的函数的值。实际上,它可以是返回函数的任何表达式,甚至是一个返回另一个函数的函数调用,就像:

  1. function bar() {
  2. return function foo(strings, ...values) {
  3. console.log( strings );
  4. console.log( values );
  5. }
  6. }
  7. var desc = "awesome";
  8. bar()`Everything is ${desc}!`;
  9. // [ "Everything is ", "!"]
  10. // [ "awesome" ]

但是当作为一个字符串字面量的标签时,函数foo(..)被传入了什么?

第一个参数值 —— 我们称它为strings —— 是一个所有普通字符串的数组(所有被插值的表达式之间的东西)。我们在strings数组中得到两个值:"Everything is ""!"

之后为了我们示例的方便,我们使用...收集/剩余操作符(见本章早先的“扩散/剩余”部分)将所有后续的参数值收集到一个称为values的数组中,虽说你本来当然可以把它们留作参数strings后面单独的命名参数。

被收集进我们的values数组中的参数值,就是在字符串字面量中发现的,已经被求过值的插值表达式的结果。所以在我们的例子中values里唯一的元素显然就是awesome

你可以将这两个数组考虑为:在values中的值原本是你拼接在stings的值之间的分隔符,而且如果你将所有的东西连接在一起,你就会得到完整的插值字符串值。

一个标签型字符串字面量像是一个在插值表达式被求值之后,但是在最终的字符串被编译之前的处理步骤,允许你在从字面量中产生字符串的过程中进行更多的控制。

一般来说,一个字符串字面量标签函数(在前面的代码段中是foo(..))应当计算一个恰当的字符串值并返回它,所以你可以使用标签型字符串字面量作为一个未打标签的字符串字面量来使用:

  1. function tag(strings, ...values) {
  2. return strings.reduce( function(s,v,idx){
  3. return s + (idx > 0 ? values[idx-1] : "") + v;
  4. }, "" );
  5. }
  6. var desc = "awesome";
  7. var text = tag`Everything is ${desc}!`;
  8. console.log( text ); // Everything is awesome!

在这个代码段中,tag(..)是一个直通操作,因为它不实施任何特殊的修改,而只是使用reduce(..)来循环遍历,并像一个未打标签的字符串字面量一样,将stringsvalues拼接/穿插在一起。

那么实际的用法是什么?有许多高级的用法超出了我们要在这里讨论的范围。但这里有一个格式化美元数字的简单想法(有些像基本的本地化):

  1. function dollabillsyall(strings, ...values) {
  2. return strings.reduce( function(s,v,idx){
  3. if (idx > 0) {
  4. if (typeof values[idx-1] == "number") {
  5. // 看,也使用插值性字符串字面量!
  6. s += `$${values[idx-1].toFixed( 2 )}`;
  7. }
  8. else {
  9. s += values[idx-1];
  10. }
  11. }
  12. return s + v;
  13. }, "" );
  14. }
  15. var amt1 = 11.99,
  16. amt2 = amt1 * 1.08,
  17. name = "Kyle";
  18. var text = dollabillsyall
  19. `Thanks for your purchase, ${name}! Your
  20. product cost was ${amt1}, which with tax
  21. comes out to ${amt2}.`
  22. console.log( text );
  23. // Thanks for your purchase, Kyle! Your
  24. // product cost was $11.99, which with tax
  25. // comes out to $12.95.

如果在values数组中遇到一个number值,我们就在它前面放一个"$"并用toFixed(2)将它格式化为小数点后两位有效。否则,我们就不碰这个值而让它直通过去。

原始字符串

在前一个代码段中,我们的标签函数接受的第一个参数值称为strings,是一个数组。但是有一点儿额外的数据被包含了进来:所有字符串的原始未处理版本。你可以使用.raw属性访问这些原始字符串值,就像这样:

  1. function showraw(strings, ...values) {
  2. console.log( strings );
  3. console.log( strings.raw );
  4. }
  5. showraw`Hello\nWorld`;
  6. // [ "Hello
  7. // World" ]
  8. // [ "Hello\nWorld" ]

原始版本的值保留了原始的转义序列\n\n是两个分离的字符),但处理过的版本认为它是一个单独的换行符。但是,早先提到的行终结符泛化操作,是对两个值都实施的。

ES6带来了一个内建函数,它可以用做字符串字面量的标签:String.raw(..)。它简单地直通strings值的原始版本:

  1. console.log( `Hello\nWorld` );
  2. // Hello
  3. // World
  4. console.log( String.raw`Hello\nWorld` );
  5. // Hello\nWorld
  6. String.raw`Hello\nWorld`.length;
  7. // 12

字符串字面量标签的其他用法包括国际化,本地化,和许多其他的特殊处理。