模板字面量(模板字符串)

模板字面量是由反引号 (`) 字符分隔的字面量,允许使用多行字符串字符串插值(嵌入表达式)以及称为带标签的模板的特殊结构。

模板字面量有时非正式地称为模板字符串,因为它们最常用于字符串插值(通过替换占位符来创建字符串)。但是,带标签的模板字面量可能不会生成字符串;它可以与自定义标签函数一起使用,以对模板字面量的不同部分执行任何你想要的运算。

语法

js
`string text`

`string text line 1
 string text line 2`

`string text ${expression} string text`

tagFunction`string text ${expression} string text`

参数

字符串文本

将成为模板字面量一部分的字符串文本。几乎所有字符都允许直接使用,包括换行符和其他空白字符。但是,无效的转义序列会导致语法错误,除非使用标签函数

表达式

要插入到当前位置的表达式,其值将转换为字符串或传递给 tagFunction

tagFunction

如果指定,它将使用模板字符串数组和替换表达式进行调用,并且返回值成为模板字面量的值。请参阅带标签的模板

描述

模板字面量用反引号 (`) 字符而不是双引号或单引号括起来。

除了包含普通字符串之外,模板字面量还可以包含其他称为占位符的部分,这些占位符是由美元符号和花括号分隔的嵌入表达式:${expression}。字符串和占位符将传递给一个函数——要么是默认函数,要么是你提供的函数。默认函数(当你没有提供自己的函数时)只是执行字符串插值以替换占位符,然后将这些部分连接成单个字符串。

要提供你自己的函数,请在模板字面量前加上函数名称;结果称为带标签的模板。在这种情况下,模板字面量将传递给你提供的标签函数,然后你可以在其中对模板字面量的不同部分执行任何你想要的运算。

要在模板字面量中转义反引号,请在反引号 (\) 前面加上反斜杠。

js
`\`` === "`"; // true

美元符号也可以转义以防止插值。

js
`\${1}` === "${1}"; // true

多行字符串

在源代码中插入的任何换行符都是模板字面量的一部分。

使用普通字符串,你需要使用以下语法才能获得多行字符串

js
console.log("string text line 1\n" + "string text line 2");
// "string text line 1
// string text line 2"

使用模板字面量,你可以使用以下方法实现相同的功能

js
console.log(`string text line 1
string text line 2`);
// "string text line 1
// string text line 2"

普通字符串字面量类似,你可以通过使用反斜杠 (\) 转义换行符,将单行字符串跨多行编写,以提高源代码的可读性

js
console.log(`string text line 1 \
string text line 2`);
// "string text line 1 string text line 2"

字符串插值

在没有模板字面量的情况下,当你想要将表达式的输出与字符串组合时,你需要使用连接运算符 + 进行连接

js
const a = 5;
const b = 10;
console.log("Fifteen is " + (a + b) + " and\nnot " + (2 * a + b) + ".");
// "Fifteen is 15 and
// not 20."

这可能难以阅读——尤其是在你有多个表达式时。

使用模板字面量,你可以避免使用连接运算符——并提高代码的可读性——通过使用 ${expression} 形式的占位符来对嵌入表达式进行替换

js
const a = 5;
const b = 10;
console.log(`Fifteen is ${a + b} and
not ${2 * a + b}.`);
// "Fifteen is 15 and
// not 20."

请注意,这两种语法之间存在细微的差别。模板字面量将其表达式直接强制转换为字符串,而加法运算符则首先将其操作数强制转换为原始值。有关更多信息,请参阅+ 运算符的参考页面。

嵌套模板

在某些情况下,嵌套模板是获得可配置字符串的最简单(也许也是更易读的)方法。在反引号分隔的模板中,可以通过在模板中的 ${expression} 占位符内使用它们来轻松允许内部反引号。

例如,在没有模板字面量的情况下,如果你想根据特定条件返回某个值,你可以执行以下操作

js
let classes = "header";
classes += isLargeScreen()
  ? ""
  : item.isCollapsed
    ? " icon-expander"
    : " icon-collapser";

使用模板字面量但不进行嵌套,你可以执行以下操作

js
const classes = `header ${
  isLargeScreen() ? "" : item.isCollapsed ? "icon-expander" : "icon-collapser"
}`;

使用模板字面量的嵌套,你可以执行以下操作

js
const classes = `header ${
  isLargeScreen() ? "" : `icon-${item.isCollapsed ? "expander" : "collapser"}`
}`;

带标签的模板

模板字面量更高级的形式是带标签的模板。

标签允许你使用函数解析模板字面量。标签函数的第一个参数包含一个字符串值的数组。其余参数与表达式相关。

然后,标签函数可以对你希望对这些参数执行的任何操作,并返回处理后的字符串。(或者,它可以返回完全不同的内容,如下面的示例之一所述。)

用于标签的函数名称可以是你想要的任何名称。

js
const person = "Mike";
const age = 28;

function myTag(strings, personExp, ageExp) {
  const str0 = strings[0]; // "That "
  const str1 = strings[1]; // " is a "
  const str2 = strings[2]; // "."

  const ageStr = ageExp < 100 ? "youngster" : "centenarian";

  // We can even return a string built using a template literal
  return `${str0}${personExp}${str1}${ageStr}${str2}`;
}

const output = myTag`That ${person} is a ${age}.`;

console.log(output);
// That Mike is a youngster.

标签不必是简单的标识符。你可以使用任何优先级大于 16 的表达式,包括属性访问器、函数调用、new 表达式,甚至另一个带标签的模板字面量。

js
console.log`Hello`; // [ 'Hello' ]
console.log.bind(1, 2)`Hello`; // 2 [ 'Hello' ]
new Function("console.log(arguments)")`Hello`; // [Arguments] { '0': [ 'Hello' ] }

function recursive(strings, ...values) {
  console.log(strings, values);
  return recursive;
}
recursive`Hello``World`;
// [ 'Hello' ] []
// [ 'World' ] []

虽然语法上允许,但未加标签的模板字面量是字符串,并且当链接时会抛出TypeError

js
console.log(`Hello``World`); // TypeError: "Hello" is not a function

唯一的例外是可选链,它会抛出语法错误。

js
console.log?.`Hello`; // SyntaxError: Invalid tagged template on optional chain
console?.log`Hello`; // SyntaxError: Invalid tagged template on optional chain

请注意,这两个表达式仍然可解析。这意味着它们不会受到自动分号插入的影响,自动分号插入只会插入分号以修复其他不可解析的代码。

js
// Still a syntax error
const a = console?.log
`Hello`

标签函数甚至不需要返回字符串!

js
function template(strings, ...keys) {
  return (...values) => {
    const dict = values[values.length - 1] || {};
    const result = [strings[0]];
    keys.forEach((key, i) => {
      const value = Number.isInteger(key) ? values[key] : dict[key];
      result.push(value, strings[i + 1]);
    });
    return result.join("");
  };
}

const t1Closure = template`${0}${1}${0}!`;
// const t1Closure = template(["","","","!"],0,1,0);
t1Closure("Y", "A"); // "YAY!"

const t2Closure = template`${0} ${"foo"}!`;
// const t2Closure = template([""," ","!"],0,"foo");
t2Closure("Hello", { foo: "World" }); // "Hello World!"

const t3Closure = template`I'm ${"name"}. I'm almost ${"age"} years old.`;
// const t3Closure = template(["I'm ", ". I'm almost ", " years old."], "name", "age");
t3Closure("foo", { name: "MDN", age: 30 }); // "I'm MDN. I'm almost 30 years old."
t3Closure({ name: "MDN", age: 30 }); // "I'm MDN. I'm almost 30 years old."

标签函数接收的第一个参数是一个字符串数组。对于任何模板字面量,其长度等于替换次数 (${…} 的出现次数) 加 1,因此始终非空。

对于任何特定的带标签的模板字面量表达式,无论字面量计算多少次,标签函数都将始终使用完全相同的字面量数组进行调用。

js
const callHistory = [];

function tag(strings, ...values) {
  callHistory.push(strings);
  // Return a freshly made object
  return {};
}

function evaluateLiteral() {
  return tag`Hello, ${"world"}!`;
}

console.log(evaluateLiteral() === evaluateLiteral()); // false; each time `tag` is called, it returns a new object
console.log(callHistory[0] === callHistory[1]); // true; all evaluations of the same tagged literal would pass in the same strings array

这允许标签根据其第一个参数的标识缓存结果。为了进一步确保数组值的稳定性,第一个参数及其raw 属性都已冻结,因此你无法以任何方式修改它们。

原始字符串

可用于标签函数第一个参数的特殊 raw 属性允许你访问输入的原始字符串,而无需处理转义序列

js
function tag(strings) {
  console.log(strings.raw[0]);
}

tag`string text line 1 \n string text line 2`;
// Logs "string text line 1 \n string text line 2",
// including the two characters '\' and 'n'

此外,String.raw() 方法用于创建原始字符串,就像默认模板函数和字符串连接一样。

js
const str = String.raw`Hi\n${2 + 3}!`;
// "Hi\\n5!"

str.length;
// 6

Array.from(str).join(",");
// "H,i,\\,n,5,!"

如果字面量不包含任何转义序列,则 String.raw 的功能类似于“标识”标签。如果你想要一个始终像未加标签的字面量一样工作的实际标识标签,你可以创建一个自定义函数,将“已处理”(即已处理转义序列)的字面量数组传递给 String.raw,将其伪装成原始字符串。

js
const identity = (strings, ...values) =>
  String.raw({ raw: strings }, ...values);
console.log(identity`Hi\n${2 + 3}!`);
// Hi
// 5!

这对许多会对用特定名称标记的字面量进行特殊处理的工具很有用。

js
const html = (strings, ...values) => String.raw({ raw: strings }, ...values);
// Some formatters will format this literal's content as HTML
const doc = html`<!doctype html>
  <html lang="en-US">
    <head>
      <title>Hello</title>
    </head>
    <body>
      <h1>Hello world!</h1>
    </body>
  </html>`;

标记模板和转义序列

在普通模板字面量中,字符串字面量中的转义序列都是允许的。任何其他非良构的转义序列都是语法错误。这包括

  • \ 后跟除 0 以外的任何十进制数字,或 \0 后跟十进制数字;例如 \9\07(这是一种已弃用的语法
  • \x 后跟少于两个十六进制数字(包括零个);例如 \xz
  • \u 后面没有 { 并且后面跟着少于四个十六进制数字(包括零个);例如 \uz
  • \u{} 包含无效的 Unicode 代码点 - 它包含非十六进制数字,或者其值大于 10FFFF;例如 \u{110000}\u{z}

注意:\ 后跟其他字符,虽然它们可能没有用,因为没有转义任何内容,但不是语法错误。

但是,这对标记模板来说是有问题的,因为除了“已处理”的字面量之外,它们还可以访问原始字面量(转义序列按原样保留)。

标记模板允许嵌入任意字符串内容,其中转义序列可能遵循不同的语法。考虑一个简单的示例,我们通过 String.raw 在 JavaScript 中嵌入 LaTeX 源文本。我们仍然希望能够使用以 ux 开头的 LaTeX 宏,而无需遵循 JavaScript 语法限制。因此,良构转义序列的语法限制从标记模板中删除。下面的示例使用 MathJax 在一个元素中呈现 LaTeX

js
const node = document.getElementById("formula");
MathJax.typesetClear([node]);
// Throws in older ECMAScript versions (ES2016 and earlier)
// SyntaxError: malformed Unicode character escape sequence
node.textContent = String.raw`$\underline{u}$`;
MathJax.typesetPromise([node]);

但是,非法转义序列必须仍然在“已处理”表示中表示。它们将在“已处理”数组中显示为 undefined 元素

js
function log(str) {
  console.log("Cooked:", str[0]);
  console.log("Raw:", str.raw[0]);
}

log`\unicode`;
// Cooked: undefined
// Raw: \unicode

请注意,转义序列限制仅从标记的模板中删除,而不是从未标记的模板字面量中删除

js
const bad = `bad escape sequence: \unicode`;

规范

规范
ECMAScript 语言规范
# sec-template-literals

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅