模板字面量(模板字串)

Baseline 已廣泛支援

此特性已相當成熟,可在許多裝置和瀏覽器版本上使用。自 2015 年 9 月以來,該特性已在各大瀏覽器中可用。

模板字面量 是由反引號 (`) 字元包圍的字面量,允許使用多行字串、透過嵌入表示式實現字串插值,以及稱為標籤模板的特殊構造。

模板字面量有時也被非正式地稱為 模板字串,因為它們最常用於字串插值(透過替換佔位符來建立字串)。然而,標籤模板字面量可能不會生成字串;它可以與自定義的標籤函式一起使用,以對模板字面量的不同部分執行任何所需的操作。

語法

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\nstring 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."

標籤函式接收的第一個引數是一個字串陣列。對於任何模板字面量,其長度等於替換次數(${…} 的出現次數)加一,因此總是非空的。

對於任何特定的標籤模板字面量表達式,無論字面量被評估多少次,標籤函式都將始終使用完全相同的字面量陣列進行呼叫。

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 開頭而不受 JavaScript 語法限制的 LaTeX 宏。因此,標籤模板中移除了格式良好轉義序列的語法限制。下面的示例使用 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® 2026 語言規範
# sec-template-literals

瀏覽器相容性

另見