ES6模板字面量与拼接字符串

122

我有以下的代码,用于 ECMAScript 6 中的模板文字(template literals):

let person = {name: 'John Smith'};
let tpl = `My name is ${person.name}.`;
let MyVar = "My name is " + person.name + ".";

console.log("template literal= " + tpl);
console.log("my variable = " + MyVar);

输出结果如下:

template literal= My name is John Smith.
my variable = My name is John Smith.

这里是代码片段。

我尝试搜索了确切的区别,但没有找到,以下两个语句有什么不同?

  let tpl = `My name is ${person.name}.`;

  let MyVar = "My name is "+ person.name+".";
我已经能够在这里将字符串MyVarperson.name连接起来,那么使用模板字面量的情况会是什么?
我已经可以在这里将字符串MyVarperson.name连接起来,那么何时需要使用模板字面量呢?

10
其他语言中这是一个常见功能,关于时间!看起来更干净,而且支持多行。 - elclanrs
5
不确定你所说的“差异”是指 tpl === MyVar 吗?唯一的区别在于它们创建时的语法不同。需要注意的是,与字符串拼接不同,模板提供了标签函数,可用于自动转义等功能。 - Bergi
你基本上在问字符串插值和字符串拼接之间的区别。 - Damjan Pavlica
1
速度差异不值得考虑 - 如果纳秒对您来说是瓶颈,那么您需要解决其他更大的问题,比如另一个服务器等。我更喜欢使用concat,因为主观上反引号看起来太像用在其他地方的撇号。 - James
5个回答

151
如果你只在模板字面量中使用占位符(例如,`Hello ${person.name}`),就像问题的例子一样,那么结果与仅连接字符串相同。主观上看起来更好,更易于阅读,特别是对于包含多行字符串或同时包含 '" 的字符串,因为你不再需要转义这些字符。

可读性是一个很好的特性,但关于模板最有趣的事情是 标记模板字面量

let person = {name: 'John Smith'}; 
let tag = (strArr, name) => strArr[0] + name.toUpperCase() + strArr[1];  
tag `My name is ${person.name}!` // Output: My name is JOHN SMITH!

在这个例子的第三行,调用了一个名为的函数。模板字符串的内容被拆分成多个变量,你可以在函数的参数中访问它们:字面上的部分(在这个例子中,strArr[0]的值是"My name is ",而strArr[1]的值是"!")和替换部分(John Smith)。模板字面量将评估为函数返回的任何内容。 ECMAScript wiki列出了一些可能的用例,如自动转义或编码输入,或本地化。你可以创建一个名为msg的标记函数,查找类似于My name is 的文字部分,并将它们替换为当前语言环境的翻译,例如德语。
console.log(msg`My name is ${person.name}.`) // Output: Mein Name ist John Smith.

标记函数返回的值甚至不必是一个字符串。你可以创建一个名为$的标记函数,评估这个字符串并将其用作查询选择器来返回一组DOM节点,就像在这个example中所示:

$`a.${className}[href=~'//${domain}/']`

2
不错!如果你有另一个模板字面量,比如${person.message},它也会被翻译吗? - Rigotti
1
@Rigotti 这取决于 msg 函数的实现。你当然也可以翻译替换值。 - kapex
@Beat 整个 ecmascript.org 网站似乎都无法访问。我认为他们本来就计划放弃他们的维基,所以我已经用存档版本更新了链接。 - kapex
我尝试在Chrome控制台中运行$a.${className}[href=~'//${domain}/'](并在之前设置className=''domain=''),但我没有得到DOM节点,而是一组字符串:/(另一方面,在jsfiddle中我们在控制台中得到错误:https://jsfiddle.net/d1fkta76/ "Uncaught ReferenceError: $未定义" - 为什么? - Kamil Kiełczewski
@KamilKiełczewski 这只是一个新语法可能性的示例,不是实际功能。我已经编辑了答案以更清楚地表明这一点。 - kapex
2
@AniketSuryavanshi 这里是模板字符串与连接操作性能的比较:https://dev59.com/o14b5IYBdhLWcg3wfhuR#29083467 几年前,模板字符串的速度比较慢,但现在它们似乎比连接操作略快。 - kapex

39

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

let actor = {name: 'RajiniKanth', age: 68};

let oldWayStr = "<p>My name is " + actor.name + ",</p>\n" +
  "<p>I am " + actor.age + " old</p>\n";

let newWayHtmlStr =
 `<p>My name is ${actor.name},</p>
  <p>I am ${actor.age} old</p>`;

console.log(oldWayStr);
console.log(newWayHtmlStr);

正如您所看到的,我们使用了..``将一系列字符括起来,这被解释为字符串字面量,但是任何形式为${..}的表达式都会被解析并立即内联评估。

插入式字符串字面量的一个非常好的好处是它们允许跨越多行:

var Actor = {"name" : "RajiniKanth"};

var text =
`Now is the time for all good men like ${Actor.name}
to come to the aid of their
country!`;
console.log( text );
// Now is the time for all good men
// to come to the aid of their
// country!

插值表达式

任何有效的表达式,包括函数调用、内联函数表达式调用甚至其他的插值字符串字面量,都可以出现在插值字符串字面量的${...}中。

function upper(s) {
  return s.toUpperCase();
}
var who = "reader"
var text =
`A very ${upper( "warm" )} welcome
to all of you ${upper( `${who}s` )}!`;
console.log( text );
// A very WARM welcome
// to all of you READERS!

在这里,内部的 ${who}s 插值字符串字面量对于我们将 who 变量与 "s" 字符串组合而言是一种更好的便利方式,而不是 who + "s"。还有需要注意的是,插值字符串字面量只在其出现的位置 词法作用域 中生效,而不具有任何 动态作用域:

function foo(str) {
  var name = "foo";
  console.log( str );
}
function bar() {
  var name = "bar";
  foo( `Hello from ${name}!` );
}
var name = "global";
bar(); // "Hello from bar!"

使用模板字面量来编写HTML代码,可以通过减少烦琐的操作使代码更易读。

传统方式:

'<div class="' + className + '">' +
  '<p>' + content + '</p>' +
  '<a href="' + link + '">Let\'s go</a>'
'</div>';

使用 ES6:

`<div class="${className}">
  <p>${content}</p>
  <a href="${link}">Let's go</a>
</div>`
  • 字符串可以跨越多行。
  • 您不必转义引号字符。
  • 您可以避免像'">'这样的组合。
  • 您不必使用加号运算符。

标记模板字面量

当一个template 字符串被标记时,它的literals和替换项会被传递给一个函数,该函数返回结果值。

function myTaggedLiteral(strings) {
  console.log(strings);
}

myTaggedLiteral`test`; //["test"]

function myTaggedLiteral(strings,value,value2) {
  console.log(strings,value, value2);
}
let someText = 'Neat';
myTaggedLiteral`test ${someText} ${2 + 3}`;
// ["test ", " ", ""]
// "Neat"
// 5

我们可以在这里使用spread运算符来传递多个值。第一个参数 - 我们称之为strings - 是所有纯字符串的数组(在任何插入表达式之间的内容)。

然后,我们使用...gather/rest操作符将所有后续参数收集到名为values的数组中,尽管您当然可以像上面所做的那样将它们留作单独的命名参数跟在字符串参数后面(value1、value2等)

function myTaggedLiteral(strings,...values) {
  console.log(strings);
  console.log(values);
}

let someText = 'Neat';
myTaggedLiteral`test ${someText} ${2 + 3}`;
// ["test ", " ", ""]
// ["Neat", 5]

收集到值数组中的参数是在字符串字面量中已经计算过的插值表达式的结果。 标记模板字符串类似于插值计算后,但在编译最终字符串值之前的处理步骤,允许您更多地控制从字面量生成字符串。 让我们看一个创建可重复使用的模板的示例。

const Actor = {
  name: "RajiniKanth",
  store: "Landmark"
}

const ActorTemplate = templater`<article>
  <h3>${'name'} is a Actor</h3>
  <p>You can find his movies at ${'store'}.</p>

</article>`;

function templater(strings, ...keys) {
  return function(data) {
  let temp = strings.slice();
  keys.forEach((key, i) => {
  temp[i] = temp[i] + data[key];
  });
  return temp.join('');
  }
};

const myTemplate = ActorTemplate(Actor);
console.log(myTemplate);

原始字符串

我们的标记函数接收一个名为 strings 的第一个参数,它是一个 数组。但还有一个额外的数据包含在内:所有字符串的原始未处理版本。你可以使用 .raw 属性访问这些原始字符串值,像这样:

function showraw(strings, ...values) {
  console.log( strings );
  console.log( strings.raw );
}
showraw`Hello\nWorld`;

正如您所看到的,字符串的raw版本保留了转义的 \n 序列,而处理后的字符串将其视为未转义的真实换行符。ES6带有一个内置函数,可用作字符串字面值标记:String.raw(..)。它只是传递了strings的原始版本:

console.log( `Hello\nWorld` );
/* "Hello
World" */

console.log( String.raw`Hello\nWorld` );
// "Hello\nWorld"

8

这种写法更加简洁清晰,正如评论中所述,在其他语言中也是常见的特性。另外一个我觉得很好的地方是换行符,对于编写字符串非常有用。

let person = {name: 'John Smith', age: 24, greeting: 'Cool!' };

let usualHtmlStr = "<p>My name is " + person.name + ",</p>\n" +
                   "<p>I am " + person.age + " old</p>\n" +
                   "<strong>\"" + person.greeting +"\" is what I usually say</strong>";


let newHtmlStr = 
 `<p>My name is ${person.name},</p>
  <p>I am ${person.age} old</p>
  <p>"${person.greeting}" is what I usually say</strong>`;


console.log(usualHtmlStr);
console.log(newHtmlStr);

我不明白在字符串和字面量中是否有重大区别。请查看此链接http://www.es6fiddle.net/i3vj1ldl/。字面量只会插入一个空格而不是换行符。 - Naeem Shaikh
1
哦,我并没有说这是一个很大的区别。字面换行只是语法糖。这只是为了可读性而已。 - Rigotti
但是你指出了一个很好的区别。但在接受你的答案之前,我会等待更多时间以获得更好的答案,如果有任何重大差异的话! :) - Naeem Shaikh
2
@NaeemShaikh,非常抱歉,字面换行实际上是可行的。我刚刚注意到ES6Fiddle只是测试它的一种可怕方式。我会编辑我的答案。 - Rigotti

6
虽然我的回答并没有直接回答问题,但我认为指出使用模板文字而不是数组连接的一个缺点可能会引起某些人的兴趣。
假设我有以下代码:
let patient1 = {firstName: "John", lastName: "Smith"};
let patient2 = {firstName: "Dwayne", lastName: "Johnson", middleName: "'The Rock'"};

有些患者有中间名,有些则没有。

如果我想要一个代表患者全名的字符串

let patientName = `${patient1.firstName} ${patient1.middleName} ${patient1.lastName}`;

那么这将变成 "John undefined Smith"

然而,如果我做了以下操作

let patientName = [patient1.firstName, patient1.middleName,  patient1.lastName].join(" ");

然后这将变成只有"John Smith" General_Twyckenham指出,在“”上进行连接会在“John”和“Smith”之间产生额外的空格。
为了避免这种情况,您可以在连接之前添加一个过滤器来消除虚假值。
[patient1.firstName, patient1.middleName, patient1.lastName].filter(el => el).join(" ");

3
实际上,那并不完全正确 - join 版本会给你 John Smith,带有额外的空格。可以想象,这通常是不可取的。解决这个问题的方法是使用 map,像这样:[patient1.firstName, patient1.middleName, patient1.lastName].map(el => el).join(" "); - General_Twyckenham
@General_Twyckenham 哦,我明白你的意思了。很好的发现。另外,应该使用过滤器而不是映射来去掉那个额外的空格。我会编辑我的答案,谢谢。 - Dhruv Prakash
糟糕 - 是的,我想说的是 filter 函数。 - General_Twyckenham
根据这次讨论,字符串连接比数组连接快。 https://dev59.com/Pmw05IYBdhLWcg3wVwc4 - Michael Harley

0
我发现了两者之间的一个微小差异:如果表达式求值为具有 @@toPrimitive 方法的对象,则在模板字面量版本中将调用该方法,并传入 'default',而在字符串拼接版本中将传入 'string'

console.config({ maximize: true });

class C {
  [Symbol.toPrimitive](hint) {
    console.log(hint); // 'string', 'default'

    switch (hint) {
      case 'string':
        return 'Hello';
      default: // case 'default':
        return 'Good bye';
    }
  }
};

console.log(`${new C()} world!`); // 'Hello world!'
console.log(new C() + ' world!'); // 'Good bye world!'
<script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script>

换句话说,${}内部的表达式被视为独立的,而用于字符串拼接的表达式则与另一个操作数一起进行评估。
规范中的此部分如下所述:
引用:

应用于Expression值的字符串转换语义类似于String.prototype.concat而不是+运算符。

据我所知(我相信我可能遗漏了某些内容),这意味着${new C()} world!''.concat(new C(), ' world!')的解析方式相同,而不是'' + new C() + ' world!'。然而,在该表达式中,new C()也会根据'string'作为hint进行评估。

console.config({ maximize: true });

class C {
  [Symbol.toPrimitive](hint) {
    console.log(hint); // 'string', 'string'

    switch (hint) {
      case 'string':
        return 'Hello';
      default: // case 'default':
        return 'Good bye';
    }
  }
};

console.log(''.concat(new C(), ' world!')); // 'Hello world!'
console.log(''.concat(new C()).concat(' world!')); // 'Hello world!'
<script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接