Шаблонные литералы в 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!` );
Давайте рассмотрим это шаг за шагом.
components
: это корзина, в которой мы будем собирать части шаблона.- Для каждого фрагмента шаблона возьмите значение по соответствующему индексу, если он существует. Проверка необходима, потому что у последнего фрагмента не будет сопутствующего значения, которое можно было бы добавить в конце. Сначала нажмите строку, затем значение, если оно есть (принуждая к строке по мере продвижения). Результатом будет серия чередующихся фрагментов шаблона и интерполированных значений.
- (Примечание: первое и последнее значения всегда будут фрагментами шаблона, потому что, если строка шаблона начинается или заканчивается символом
${}
, перед ним или после него будет передана пустая строка соответственно. Это было мудрое решение авторов спецификации. ' часть, потому что это значительно упрощает объединение двух массивов вместе.) - Наконец, когда все собрано, соберите его в строку с
.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) . Я надеюсь изучить некоторые другие примеры в будущем посте.