Шаблонні літерали (Шаблонні рядки)

Шаблонні літерали — це літерали, які виділяються символами тупого наголосу (`), і дають можливість використовувати багаторядковий текст, інтерполяцію рядків із вбудованими виразами, і також особливими конструкціями, які називаються тегованими шаблонами.

Шаблонні літерали інколи неформально називають шаблонними рядками, оскільки одним з основних способів їхнього застосування є інтерполяція рядків (створення рядків шляхом підставляння полів для заповнення). Хоча теговані шаблонні літерали можуть і не виводити рядок взагалі; їх можна застосувати із довільною теговою функцією для виконання будь-яких операцій із різними частинами шаблонного літерала.

Синтаксис

`стрічка тексту`

`стрічка тексту — рядок 1
 стрічка тексту — рядок 2`

`стрічка тексту ${вираз} стрічка тексту`

tagFunction`стрічка тексту ${вираз} стрічка тексту`

Параметри

стрічка тексту

Рядок, що стане частиною шаблонного літерала. Майже всі символи дозволені в буквальному вигляді, включно з розривами рядка та іншими пробільними символами. Проте недійсні екрановані послідовності призведуть до синтаксичної помилки, якщо не використана тегова функція.

вираз

Вираз для вставлення у поточне положення, чиє значення перетворюється на рядок чи передається до tagFunction.

tagFunction

Функція, котра, коли вказана, буде викликана з масивом шаблонних рядків і виразами підставляння, а її повернене значення стане значенням шаблонного літерала. Дивіться теговані шаблони.

Опис

Шаблонні літерали виділяються символами тупого наголосу (`) замість одинарних чи подвійних лапок.

Поряд зі звичайними рядками, шаблонні літерали можуть містити інші частини — так звані поля для заповнення, які являються вбудованими виразами, виділеними знаком долара і фігурними дужками (${вираз}). Рядки й поля для заповнення передаються до функції: або усталеної, або переданої в коді. Усталена функція (що спрацьовує, коли явно не вказано іншої) виконує лише інтерполяцію рядків для підставляння значень у поля для заповнення, і потім з'єднує всі частини у цілий рядок.

Аби передати власну функцію, достатньо вказати її ім'я перед шаблонним літерам. Вираз, отриманий в результаті, називається тегований шаблон. В цьому випадку шаблонний літерал буде передано до вказаної тегової функції, де можна буде виконати будь-які операції з різними частинами шаблонного літерала.

Для екранування символу тупого наголосу всередині шаблонного літерала достатньо поставити перед ним зворотний скіс (\).

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

Також для запобігання інтерполяції можна екранувати знак долара.

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

Багаторядковий текст

Будь-який символ нового рядка, присутній у початковому коді, входить до складу шаблонного літерала.

Під час використання звичайних рядків слід використовувати наступний синтаксис, аби отримати багаторядний текст:

console.log("стрічка тексту — рядок 1\n" + "стрічка тексту — рядок 2");
// "стрічка тексту — рядок 1
// стрічка тексту — рядок 2"

З шаблонними літералами аналогічного результату можна досягнути так:

console.log(`стрічка тексту — рядок 1
стрічка тексту — рядок 2`);
// "стрічка тексту — рядок 1
// стрічка тексту — рядок 2"

Інтерполяція рядків

За відсутності шаблонних літералів, коли є потреба скомбінувати вивід виразу із рядками, знадобилося б зчепити їх докупи за допомогою оператора додавання +:

const a = 5;
const b = 10;
console.log("П'ятнадцять — це " + (a + b) + ", а\nне " + (2 * a + b) + ".");
// "П'ятнадцять — це 15, а
// не 20."

Це може бути складним для прочитання, особливо якщо там є декілька виразів.

Можна уникнути вживання оператора конкатенації — а також покращити зрозумілість коду — використавши шаблонні літерали шляхом додавання полів для заповнення у формі ${вираз}, для виконання заміни вбудованих виразів:

const a = 5;
const b = 10;
console.log(`П'ятнадцять — це ${a + b}, а
не ${2 * a + b}.`);
// "П'ятнадцять — це 15, а
// не 20."

Зверніть увагу, що між двома синтаксисами присутня певна відмінність. Шаблонні літерали напряму зводять свої вирази до рядків, натомість додавання спершу зводить свої операнди до примітивів. Для отримання подробиць дивіться довідкову сторінку оператора +.

Вкладення шаблонів

В деяких випадках вкладання шаблону — це найпростіший (і, ймовірно, зручніший для читання) спосіб отримання рядків, які можна налаштовувати. Всередині шаблону, виділеного тупими наголосами, можна використати вкладений шаблон, просто вживши його всередині поля для заповнення ${вираз} в шаблоні.

Наприклад, якби потрібно було повернути певне значення залежно від конкретної умови, за відсутності шаблонних літералів — можна було б зробити щось схоже на наступний приклад:

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

Із шаблонним літералом, але без вкладання, можна було б зробити таким чином:

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

З вкладеними шаблонними літералами можна робити так:

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

Теговані шаблони

Більш розширена форма шаблонних літералів — це теговані шаблони.

Теги дають змогу розбирати шаблонні літерали за допомогою функції. Перший аргумент тегової функції містить масив рядкових значень. Решта аргументів належать до виразів.

Тегова функція далі може виконати які завгодно операції на цих аргументах і повернути оброблений рядок. (Вона також може повертати щось абсолютно інше, як описано в одному з прикладів нижче.)

Назва функції, яку вжито як тег, може бути якою завгодно.

const person = "Михась";
const age = 28;

function myTag(strings, personExp, ageExp) {
  const str0 = strings[0]; // "Цей "
  const str1 = strings[1]; // " — "
  const str2 = strings[2]; // "."

  const ageStr = ageExp < 100 ? "юнак" : "довгожитель";

  // Можна повертати навіть рядок, сформований за допомогою шаблонного літерала
  return `${str0}${personExp}${str1}${ageStr}${str2}`;
}

const output = myTag`Цей ${person}${age}.`;

console.log(output);
// Цей Михась — юнак.

Тег не зобов'язаний бути простим ідентифікатором. Можна використати будь-який вираз з пріоритетом понад 16, в тому числі звертання до властивості, виклик функції, вираз new, або навіть інший тегований шаблонний літерал.

console.log`Привіт`; // [ 'Привіт' ]
console.log.bind(1, 2)`Привіт`; // 2 [ 'Привіт' ]
new Function("console.log(arguments)")`Привіт`; // [Arguments] { '0': [ 'Привіт' ] }
function recursive(strings, ...values) {
  console.log(strings, values);
  return recursive;
}
recursive`Привіт``Світ`;
// [ 'Привіт' ] []
// [ 'Світ' ] []

Хоч це технічно дозволено синтаксисом, нетеговані шаблонні літерали є рядками та викинуть TypeError, якщо їх додати в ланцюжок.

console.log(`Привіт``Світ`); // TypeError: "Привіт" is not a function

Єдиним винятком є необов'язкове зв'язування, котре викине синтаксичну помилку.

console.log?.`Привіт`; // SyntaxError: Invalid tagged template on optional chain
console?.log`Привіт`; // SyntaxError: Invalid tagged template on optional chain

Зверніть увагу, що ці два вирази усе ж є розбірливими. Це означає, що вони не будуть піддані автоматичному доданню крапки з комою, котре додає крапку з комою для виправлення коду, котрий без цього є нерозбірливим.

// Усе ж синтаксична помилка
const a = console?.log
`Привіт`

Тегові функції навіть не зобов'язані повертати саме рядок!

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("Привіт", { foo: "Світе" }); // "Привіт, Світе!"

const t3Closure = template`Мене звати ${"name"}. Мені майже ${"age"} рік.`;
// const t3Closure = template(["Мене звати ", ". Мені майже ", " рік."], "name", "age");
t3Closure("foo", { name: "WebDoky", age: 1 }); //"Мене звати WebDoky. Мені майже 1 рік."
t3Closure({ name: "WebDoky", age: 1 }); //"Мене звати WebDoky. Мені майже 1 рік."

Перший аргумент, отриманий теговою функцією, є масивом рядків. Для будь-якого шаблонного літерала довжина такого масиву рівна кількості підставлень (входжень ${…}) плюс один, а отже – цей масив ніколи не є порожнім.

Для кожного конкретного виразу тегованого шаблонного літерала тегова функція завжди викликатиметься з тим самим літералом масиву, незалежно від того, скільки разів обчислюється літерал.

const callHistory = [];
function tag(strings, ...values) {
  callHistory.push(strings);
  // Повертає свіжозроблений об'єкт
  return {};
}
function evaluateLiteral() {
  return tag`Привіт, ${"Світе"}!`;
}
console.log(evaluateLiteral() === evaluateLiteral()); // false; кожен виклик `tag` повертає новий об'єкт
console.log(callHistory[0] === callHistory[1]); // true; всі обчислення того самого тегованого літерала принесуть той самий масив рядків

Це дає тегові змогу кешувати результат на основі ідентичності його першого аргументу. Щоб іще краще пересвідчитися щодо стабільності значення масиву, і перший аргумент, і його властивість rawзаморожуються, тож їх не можна змінити в жоден спосіб.

Необроблені рядки

Особлива властивість raw, наявна в першому аргументі тегової функції, дає змогу доступитися до необроблених рядків в тому вигляді, в якому їх було введено, без обробки екранованих послідовностей.

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

tag`стрічка тексту — рядок 1 \n стрічка тексту — рядок 2`;
// Друкує "стрічка тексту — рядок 1 \n стрічка тексту — рядок 2" ,
// включно з двома символами '\' та 'n'

На додаток, для створення необроблених рядків існує метод String.raw(), який працює точнісінько так само, як усталена тегова функція і конкатенація рядків.

const str = String.raw`Привіт\n${2 + 3}!`;
// "Привіт\\n5!"

str.length;
// 10

Array.from(str).join(",");
// "П,р,и,в,і,т,\\,n,5,!"

String.raw працює як тег "ідентичності", якщо літерал не містить жодних послідовностей екранування. Якщо потрібен справжній тег ідентичності, котрий завжди працює так, ніби літерал є нетегованим, можна створити власну функцію, котра передає "приготований" (тобто такий, в якому послідовності екранування були оброблені) літерал масиву в String.raw, удаючи, ніби це необроблені рядки.

const identity = (strings, ...values) =>
  String.raw({ raw: strings }, ...values);
console.log(identity`Привіт\n${2 + 3}!`);
// Привіт
// 5!

Це корисно для багатьох інструментів, що надають особливу обробку літералам, тегованим певним конкретним ім'ям.

const html = (strings, ...values) => String.raw({ raw: strings }, ...values);
// Певні форматери форматують вміст цього літерала як HTML
const doc = html`<!doctype html>
  <html lang="uk">
    <head>
      <title>Привіт</title>
    </head>
    <body>
      <h1>Привіт, Світе!</h1>
    </body>
  </html> `;

Теговані шаблони й екрановані послідовності

У звичайних шаблонних літералах дозволені усі екрановані послідовності рядкових літералів. Будь-які інші погано сформовані екрановані послідовності є синтаксичною помилкою. Серед них:

  • \, після якої – будь-яка десяткова цифра, крім 0, або \0 з десятковою цифрою опісля; наприклад, \9 і \07 (що є нерекомендованим синтаксисом)
  • \x з менш ніж двома шістнадцятковими цифрами опісля (в тому числі жодними); наприклад, \xz
  • \u без {, але з менш ніж чотирма шістнадцятковими цифрами опісля (в тому числі жодними); наприклад, \uz
  • \u{}, що огортає недійсну кодову точку Unicode – таку, котра містить нешістнадцяткову цифру, або значення якої перевищує 10FFFF; наприклад, \u{110000} і \u{z}

Примітка: \ з іншими символами опісля, хоч це може бути безглуздим, адже нічого не екранується, не є синтаксичною помилкою.

Проте це є проблемним для тегованих шаблонів, котрі, на додачу до "приготованого" літерала, також мають доступ до необроблених лібералів (у котрих екрановані послідовності збережені як є).

Теговані шаблони повинні дозволяти вбудовування мов (наприклад, предметноорієнтованих мов чи LaTeX), у котрих поширені інакші екрановані послідовності. Таким чином, синтаксичне обмеження щодо добре сформованих екранованих послідовностей з тегованих шаблонів було зняте.

latex`\unicode`;
// Викидає помилку в старіших версіях ECMAScript (ES2016 і раніших)
// SyntaxError: malformed Unicode character escape sequence

Все ж, недійсні екрановані послідовності все одно необхідно показати в "приготованому" виразі. Їх буде показано як невизначені елементи в масиві "cooked":

function latex(str) {
  return { cooked: str[0], raw: str.raw[0] };
}

latex`\unicode`;

// { cooked: undefined, raw: "\\unicode" }

Слід зауважити, що обмеження екранованих послідовностей було прибрано лише з тегованих шаблонів, проте в нетегованих літералах вони залишилися:

const bad = `неправильна екранована послідовність: \unicode`;

Специфікації

Сумісність із браузерами

desktop mobile server
Chrome Edge Firefox Internet Explorer Opera Safari WebView Android Chrome Android Firefox for Android Opera Android Safari on iOS Samsung Internet Deno Node.js
Template literals
Chrome Full support 41
Edge Full support 12
Firefox Full support 34
Internet Explorer No support No
Opera Full support 28
Safari Full support 9
WebView Android Full support 41
Chrome Android Full support 41
Firefox for Android Full support 34
Opera Android Full support 28
Safari on iOS Full support 9
Samsung Internet Full support 4.0
Deno Full support 1.0
Node.js Full support 4.0.0
Escape sequences allowed in tagged template literals
Chrome Full support 62
Edge Full support 79
Firefox Full support 53
Internet Explorer No support No
Opera Full support 49
Safari Full support 11
WebView Android Full support 62
Chrome Android Full support 62
Firefox for Android Full support 53
Opera Android Full support 46
Safari on iOS Full support 11
Samsung Internet Full support 8.0
Deno Full support 1.0
Node.js Full support 8.10.0

Дивіться також