Шаблонные литералы в ES2015 имели большое значение. Наконец-то обезьяны JS получили возможность объявлять многострочную строку по нескольким строкам кода, не прибегая к беспорядочной конкатенации. А для форматирования строк приветственный синтаксис ${} упростил интерполяцию переменных. Этот:

["<span class=" + getElementClassName() + ">",
"  " + getElementContent(),
"</span>"].join("\n")

Превратился в это:

`<span class="${getElementClassName}">
  ${getElementContent()}
</span>`

И веб-разработчики обрадовались.

Но среди всего ликования кажется, что тесно связанная языковая функция, помеченный литерал шаблона, в основном упускалась из виду. Это позор, потому что помеченный литерал шаблона — это более крупный и плохой родственник простого литерала шаблона со многими приложениями для упрощения написания простых решений и большей выразительности.

Теги: какие они?

Во-первых, краткое освежение в синтаксисе. Если у вас есть функция тега tag , вы можете применить ее к обычному литералу шаблона следующим образом:

tag`I am an ordinary template literal! ${param} is my param.`;

Вывод обычно будет строкой, как обычный литерал шаблона, но это может быть что угодно.

tag может быть абсолютно любой функцией, и на нее можно ссылаться любым способом, которым обычно является функция:

window.tag`I'm a template literal tagged with something global!`;
getTaggingFunctionByName("tag")`I'm a template literal tagged with the output of a second-order function!`

Конечно, хотя любую функцию можно использовать в качестве тега, большинство из них будут ломаться или создавать мусор. Их нужно правильно написать.

Одним из препятствий на пути к популярности строк шаблонов с тегами является то, что кривая обучения их написанию намного круче, чем кривая обучения их использованию. Однажды написанные, они легко используются; но я подозреваю, что люди, которые их не писали, не чувствуют себя достаточно комфортно с ними, чтобы предпочесть их более знакомым языковым особенностям.

Как это работает

Наша функция tag, вероятно, написана примерно так:

function tag(strings, ...values) {
  ...
}

Когда ваша среда выполнения JS видит помеченный литерал шаблона, она разбивает его на куски. Биты строки (т.е. все, что находится за пределами ${}) собираются в массив строк; они передаются функции тега в качестве ее первых аргументов. Все последующие аргументы берутся из интерполированных переменных в шаблоне (т. е. все, внутри ${}).

(В моем примере ...values использует оператор распространения для сбора этих аргументов обратно в массив. Для каждого варианта использования, с которым я сталкивался, это имеет больше смысла и намного надежнее, чем именование аргументов по отдельности.)

Вот что я хотел бы увидеть в одном из руководств по ES2015: что такое тривиальная функция тега? т. е. какая функция тега выдает тот же результат из литерала шаблона, что и без тега?

Насколько я могу определить, это:

function noop(strings, ...values) {
  const components = [];
  strings.forEach((str, i) => {
    components.push(str);
    if (i in values) {
      components.push(String(values[i]));
    }
  });
  return components.join("");
}
assert(
  noop`I'm a template and ${1} is my param!`
  === `I'm a template and ${1} is my param!`
);

Давайте рассмотрим это шаг за шагом.

  1. components : это корзина, в которой мы будем собирать части шаблона.
  2. Для каждого фрагмента шаблона возьмите значение по соответствующему индексу, если он существует. Проверка необходима, потому что у последнего фрагмента не будет сопутствующего значения, которое можно было бы добавить в конце. Сначала нажмите строку, затем значение, если оно есть (принуждая к строке по мере продвижения). Результатом будет серия чередующихся фрагментов шаблона и интерполированных значений.
  3. (Примечание: первое и последнее значения всегда будут фрагментами шаблона, потому что, если строка шаблона начинается или заканчивается символом ${}, перед ним или после него будет передана пустая строка соответственно. Это было мудрое решение авторов спецификации. ' часть, потому что это значительно упрощает объединение двух массивов вместе.)
  4. Наконец, когда все собрано, соберите его в строку с .join("").

Создание

Как только вы узнаете, как воспроизвести тривиальный случай, станет намного проще увидеть, как вы можете применить теги к своему собственному коду.

Возможно, вы слышали о простом случае, когда вы хотите использовать литерал шаблона для HTML, но хотите обернуть все интерполированные переменные в тег — скажем, в тег <var/> — чтобы браузер мог видеть, что они интерполированы. Проверьте это:

function varWrap(strings, ...values) {
  const components = [];
  strings.forEach((str, i) => {
    components.push(str);
    if (i in values) {
      components.push(`<var>${values[i]}</var>`);
    }
  });
  return components.join("");
}
assert(
  varWrap`<span>${2} points</span>` ===
  "<span><var>2</var> points</span>"
);

Насколько это отличается от первой функции тегирования, которую мы написали? Мы изменили только одну строку. Вместо того, чтобы просто отправить строковую версию интерполированного значения, мы сначала оборачиваем его в выбранный нами тег.

(Обратите внимание, что для этого мы используем шаблонный литерал. А почему бы и нет?)

Что, если бы нам нужна была функция, которая могла бы оборачивать интерполированные переменные для тега любой? Мы можем легко сделать это с помощью функции более высокого порядка:

function htmlWrap(htmlTag) {
  return function (strings, ...values) {
    const components = [];
    strings.forEach((str, i) => {
      components.push(str);
      if (i in values) {
        components.push(`<${htmlTag}>${values[i]}</${htmlTag}>`);
      }
    });
    return components.join("");
  }
}
assert(
  htmlWrap("code")`<span>${2} points</span>` ===
  "<span><code>2</code> points</span>"
);

В данном случае htmlWrap — это функция, которая генерирует функцию тегирования. Если так удобнее, мы всегда можем присвоить вывод htmlWrap некоторым переменным — citeWrap, blockquoteWrap — и использовать их для тегирования.

Все это достаточно повторяется. Во многих случаях все, что мы хотим изменить при обработке литерала шаблона, — это то, что мы делаем с интерполируемыми переменными. Давайте напишем функцию, которая обрабатывает весь шаблон и создает тег из функции, представляющий то, что мы хотим сделать:

function getTag(fn) {
  return function(strings, ...values) {
    const components = [];
    strings.forEach((str, i) => {
      components.push(str);
      if (i in values) {
        components.push(fn(values[i], i, values));
      }
    });
    return components.join("");
  };
}

Теперь приведенные выше теги писать намного проще:

const varWrap = getTag(val => `<var>${val}</val>`;
const htmlTagWrap = (htmlTag) => getTag(val => `<${htmlTag}>${val}</${htmlTag}>`);
const noop = getTag(String);

Когда-нибудь при отладке хотели автоматически преобразовать объекты в строки, когда они появляются в строках, а не преобразовывать их в очень полезный '[object Object]'? Очень просто:

const json = getTag(JSON.stringify);
assert(
  json`Interpolated with ${{ingredients: "pride"}}` ===
  'Interpolated with {"ingredients": "pride"}'

Или предположим, что вы хотите округлить все числа до некоторого заданного числа знаков после запятой, но оставить все остальное нетронутым:

const places = (nbr) => getTag(val => typeof val === "number" ? val.toFixed(nbr) : val);

Теги: почему они?

Думайте о теге как о своего рода крючке во внутренней логике JS-движка. В данном случае загвоздка во внутренностях форматирования строк.

Формат строки шаблона удобен тем, что позволяет собрать все данные, необходимые для одной задачи, а именно для составления строки, в один оператор. Литералы шаблонов с тегами расширяют это, позволяя преобразовывать собранные данные указанным способом.

Затем теговые литералы шаблонов можно использовать для сбора данных для задач, для которых они в противном случае не подходили бы, например для составления запросов SQL (например, см. https://www.npmjs.com/package/sql-template-strings) . Я надеюсь изучить некоторые другие примеры в будущем посте.