Квази-литералы [1] являются синтаксической конструкцией, которая облегчает реализацию встроенных доменных языков (DSL) в JavaScript. В настоящее время их планируется включить в следующую версию ECMAScript [2]. Этот пост объясняет, как работают квази-литералы.
Вступление
Идея заключается в следующем:
квази-литерал (короткий: квази-) похож на строковый литерал и литерал регулярного выражения в том, что он обеспечивает простой синтаксис для создания данных. Ниже приведен пример.
quasiHandler`Hello ${firstName} ${lastName}`
Это просто компактный способ записи (примерно) следующего вызова функции:
quasiHandler("Hello ", firstName, " ", lastName)
Таким образом, имя перед контентом в обратных кавычках является именем вызываемой функции,
квази-обработчиком . Обработчик получает два разных типа данных:
- Буквальные разделы, такие как «Привет».
- Подстановки, такие как firstName (ограничены знаком доллара и фигурными скобками). Подстановка может быть любым выражением. Если подстановка является просто идентификатором, вы можете опустить фигурные скобки:
quasiHandler`Hello $firstName $lastName`
Литеральные разделы известны статически, замены известны только во время выполнения. Поскольку многим обработчикам необходимо различать эти два вида данных (см. Примеры ниже), фактический вызов обработчика немного сложнее, чем показано выше, и позволяет обработчику сделать это различие.
Примеры
Квази достаточно универсальны, потому что квази-литерал становится вызовом функции, а текст, который получает эта функция, структурирован. Поэтому вам нужно всего лишь написать новую функцию для реализации нового предметно-ориентированного языка. Следующие примеры взяты из [1] (с которым вы можете обратиться за подробностями):
- Необработанные строки: строковые литералы с несколькими строками текста без интерпретации экранированных символов.
var str = raw`This is a text with multiple lines. Escapes are not interpreted, \n is not a newline.`;
- Параметризованные литералы регулярного выражения: существует два способа создания экземпляров регулярного выражения.
- Статически, через литерал регулярного выражения.
- Динамически, через конструктор RegExp.
Если вы используете последний способ, это потому, что вам нужно подождать до времени выполнения, чтобы все необходимые ингредиенты были доступны: вы обычно объединяете фрагменты регулярного выражения и текст, который должен быть дословно сопоставлен. Последний должен быть правильно экранирован (точки, квадратные скобки и т. Д.). Определив обработчик регулярного выражения re, мы можем помочь с этой задачей:
re`\d+(${localeSpecificDecimalPoint}\d+)?`
- Запрашивать языки. Пример:
$`a.${className}[href=~'//${domain}/']`
Это запрос DOM, который ищет все теги <a>, у которых класс CSS — className, а целью является URL с данным доменом. Квази-обработчик $ обеспечивает правильное экранирование аргументов, что делает этот подход более безопасным, чем ручная конкатенация строк.
- Локализация текста (L10N): в L10N есть два компонента. Сначала язык и второй язык (как форматировать числа, время и т. Д.). Учитывая следующее сообщение.
alert(msg`Welcome to ${siteName}, you are visitor number ${visitorNumber}:d!`);
Обработчик сообщения будет работать следующим образом.
- Он создает следующий скелет для поиска перевода в таблице.
Welcome to {0}, you are visitor number {1}
Перевод может быть:
Besucher Nr. {1}, willkommen bei {0}!
- Затем метаданные замещения, такие как: d, извлекаются из буквальных частей и используются для форматирования данных, которые должны быть заполнены. В примере: d указывает, что для замены {1 должен использоваться специфичный для локали десятичный разделитель }. Таким образом, возможный английский результат:
Welcome to ACME Corp., you are visitor number 1,300!
На немецком языке у нас есть такие результаты, как:
Besucher Nr. 1.300, willkommen bei ACME Corp.!
- Он создает следующий скелет для поиска перевода в таблице.
- Генерация защищенного контента. С помощью квази можно различить доверенный контент, полученный из программы, и ненадежный контент, полученный от пользователя. Например:
safehtml`<a href="${url}">${text}</a>`
Буквальные разделы берутся из программы, а URL-адреса и текст — от пользователя. Квази-обработчик safehtml может гарантировать, что никакой вредоносный код не будет введен через замены. Для HTML полезна возможность вложения квази:
rows = [['Unicorns', 'Sunbeams', 'Puppies'], ['<3', '<3', '<3']], safehtml`<table>${ rows.map(function(row) { return safehtml`<tr>${ row.map(function(cell) { return safehtml`<td>${cell}</td>` }) }</tr>` }) }</table>`
Объяснение: Строки таблицы создаются выражением — вызовом метода row.map (). Результатом этого вызова является массив строк, которые создаются путем рекурсивного вызова квази. safehtml объединяет эти строки и вставляет их в заданный кадр. Ячейки для каждого ряда производятся одинаково.
- Шаблоны: Шаблоны очень похожи на квази, так как они представляют собой текст с отверстиями в них. Но обычно для заполнения дыр используются объекты (например, данные JSON). Например, ниже приведен шаблон:
<h1>${{title}}</h1> ${{content}}
Использование квази вместо строкового литерала для определения этого текста имеет два преимущества: квази выполняет синтаксический анализ для вас, а квази может содержать несколько строк. Шаблон будет определен следующим образом:
var myTmpl = tmpl` <h1>${{title}}</h1> ${{content}} `;
Это работает, потому что {title} и {content} являются фактическими выражениями ECMAScript.next: {foo, bar} является синтаксическим сахаром для {foo: foo, bar: bar}. Таким образом, обработчик получит значение, например {title: undefined} для первой замены. С шаблонами обработчик не интересуется значением заголовка, просто его именем, и этот прием позволяет ему получить к нему доступ. Недостатком использования квази таким образом является то, что такие переменные, как заголовок и контент, должны существовать (но они не должны иметь значения). Следовательно, вышесказанное должно быть записано как
var title, content; var myTmpl = tmpl` ...
Реализация обработчика
Следующее является квази-буквальным:
handlerName`lit1\n${subst1} lit2 ${subst2}`
Это внутренне преобразуется в вызов функции (адаптировано из [1]):
// hoisted declaration. const callSiteId1234 = { raw: [ 'lit1\\n', ' lit2 ', '' ], // newline as written cooked: [ 'lit1\n', ' lit2 ', '' ], // newline interpreted }; // in-situ handlerName(callSiteId1234, subst1, subst2)
Параметры обработчика делятся на две категории:
- CallSiteID, где вы получаете буквенные части как с экранированными значениями, такими как \ n интерпретированные («приготовленные») и не интерпретированные («сырые»). Количество буквенных частей всегда равно одному плюс количество подстановок. Если подстановка является последней в литерале, то создается пустая литеральная часть (как в примере выше).
- Подстановки, значения которых становятся конечными параметрами.
Идея состоит в том, что один и тот же литерал может выполняться несколько раз (например, в цикле); с помощью callSiteID обработчик может кэшировать данные из предыдущих вызовов. (1) потенциально кешируемые данные, (2) изменяется с каждым вызовом.
Назначение замен. Расширенная версия квазиса (которая, вероятно, не будет частью ECMAScript.next) позволяет присваивать подстановки. Например:
if (re_match`before (${=x}\d+) after`(myString)) { // Do something with x }
re_match создает функцию, которая немедленно вызывается в myString. Эта функция возвращает true, если myString является совпадающим, и назначает первую совпадающую группу переменной x
одновременно . Сравните приведенный выше код с эквивалентным квази-менее JavaScript-кодом ниже. Обратите внимание, что вам нужна дополнительная переменная для хранения соответствия.
var match = /before (\d+) after/.exec(myString); if (match) { x = match[1]; // Do something with x }
Чтобы сделать замену присваиваемой, происходит следующий перевод: каждая доступная для записи подстановка $ {= x} передается в обработчик как следующая функция (другие записываемые замены, такие как $ {= obj.prop}, работают одинаково).
function () { return arguments.length ? (x = arguments[0]) : x }
Объяснение: Если вы вызываете эту функцию без аргументов, вы получаете значение подстановки. Если вы предоставляете аргумент, он присваивается замене.
Каждая подстановка только для чтения $ {x} передается обработчику как функция.
function() { return x }
Вывод
Как видите, существует множество приложений для квази-литералов. Вы можете спросить, почему ECMAScript.next не представляет полноценную систему макросов. Это потому, что довольно сложно создать систему макросов для языка, синтаксис которого так же сложен, как и в JavaScript. Таким образом, эта задача займет больше времени и, возможно, исследования. Однако есть надежда: если повезет, мы увидим макросы в ECMAScript 8 [3].
Подтверждение. Спасибо Брендану Айчу, Марку С. Миллеру, Майку Сэмюэлу и Аллену Уирфс-Броку за ответы на мои вопросы, связанные с квазисом, в списке рассылки es-обсуждения.
Рекомендации
- Квази-литералы ECMAScript [предложение для ECMAScript.next]
- ECMAScript.next: обновление «TXJS» от Eich
- Первый взгляд на то, что может быть в ECMAScript 7 и 8