ES6 в деталях: шаблонные строки

ES6 в деталях — это цикл статей о новых возможностях языка программирования JavaScript, появившихся в 6 редакции стандарта ECMAScript, кратко — ES6.

На прошлой неделе я обещал сбавить темп. Я говорил, что после итераторов и генераторов мы возьмёмся за что-нибудь полегче, что-то, что не вынесет вам мозг. Посмотрим, смогу ли сдержать своё обещание.

А пока начнём с чего-нибудь простого.

Обратные кавычки

В ES6 появился новый вид синтаксиса строкового литерала под названием шаблонные строки. Они выглядят как обычные строки за исключением того, что обёрнуты символами обратных кавычек ` вместо обычных кавычек ' или ". И в простейшем случае это действительно всего лишь строки.

context.fillText(`Ceci n'est pas une chaîne.`, x, y);

Но они неспроста называются «шаблонные строки», а не «старые и скучные ничем не примечательные обыкновенные строки, но только с обратными кавычками». Вместе с шаблонными строками в JavaScript появляется простая строковая интерполяция. Иными словами, это способ опрятно и удобно подставлять значения JavaScript в строки.

Их можно применять в миллионах случаев, но моё сердце греет такое скромное сообщение об ошибке:

function authorize(user, action{
  if (!user.hasPrivilege(action)) {
    throw new Error(
      `Пользователю ${user.name} не разрешено ${action}.`);
  }
}

В этом примере ${user.name} and ${action} называются шаблонными подстановками. JavaScript вставит значения user.name и action в получившуюся строку. Так можно сгенерировать сообщение вроде Пользователю jorendorff не разрешено играть в хоккей. (Что между прочим, правда. У меня нет хоккейной лицензии.)

Пока что это просто слегка более опрятный синтаксис оператора +, но есть несколько деталей, на которые следует обратить внимание:

В отличие от обычных строк, в шаблонных строках можно использовать символы переноса строк:

$("#warning").html(`
  <h1>Внимание!</h1>
  <p>Несанкционированная игра в хоккей может повлечь
  пенальти на срок до ${maxPenalty} минут.</p>
`);

Все пробельные символы в шаблонной строке, включая переносы строк и отступы, включаются «как есть» в результат.

Хорошо. Из-за того, что я пообещал на прошлой неделе, я чувствую свою ответственность за сохранность вашего мозга. Можете прекратить читать прямо сейчас, возможно, пойти выпить чашечку кофе и насладиться своим невредимым мозгом, который все еще находится в вашей черепной коробке. Серьёзно, нет ничего постыдного в том, чтобы отступить. Разве Лопес Гонсальвес ринулся целиком исследовать южное полушарие после того, как доказал, что суда могут пересекать экватор не будучи разбитыми морскими чудищами и не падая с края Земли? Нет. Он повернул обратно домой и хорошенько пообедал. Вам же нравится обедать, верно?

С обратными кавычками в будущее

Давайте поговорим немного о том, чего шаблонные строки не делают.

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

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

Обратите внимание, SaferHTML — это не что-то, что предоставляется стандартной библиотекой. Сейчас мы напишем её сами.

var message =
  SaferHTML`<p>${bonk.sender} отвесил вам леща.</p>`;

В качестве метки здесь выступает одиночный идентификатор SaferHTML, но меткой также может быть свойство, например SaferHTML.escape, или даже вызов метода, например SaferHTML.escape({unicodeControlCharacters: false}). (Если быть совсем точным, любое выражение MemberExpression или CallExpression может быть тегом.)

Мы видели, что шаблонные строки без меток — это краткий способ простой строковой конкатенации. Помеченные шаблоны — это нечто совсем другое, вызов функции.

Код выше эквивалентен такому:

var message =
  SaferHTML(templateData, bonk.sender);

где templateData — это неизменяемый массив всех частей строки в шаблоне, созданный движком JS. В нашем случае в массиве будет два элемента, потому что в помеченной шаблонной строке две части, разделённых подстановкой. Так что templateData будет Object.freeze(["<p>", " отвесил вам леща.</p>"])

(На самом деле, в templateData есть ещё одно свойство, templateData.raw — это другой массив, содержащий все строковые части шаблона, но на этот раз они в точности в таком виде, в каком они были в исходном коде. Экранирующие последовательности вроде \n оставлены в них как есть, вместо того, чтобы превратиться в перевод каретки, ну и тому подобное. Стандартная метка String.raw использует эти сырые строки.)

Это даёт функции SaferHTML свободу интерпретировать как строку, так и подстановки миллионом различных способов.

Прежде чем продолжить чтение, может быть вы захотите попробовать разобраться в том, что SaferHTML должна делать и попробуете вручную её реализовать? В конце концов, это же всего лишь обычная функция. Мы можете проверять, что у вас получается, в консоли разработчика в Firefox.

Вот одно из возможных решений (также доступное как gist).

function SaferHTML(templateData{
  var s = templateData[0];
  for (var i = 1; i < arguments.length; i++) {
    var arg = String(arguments[i]);
 
    // Экранируем спецсимволы в подстановках.
    s += arg.replace(/&/g"&amp;")
            .replace(/</g"&lt;")
            .replace(/>/g"&gt;");
 
    // Не экранируем спецсимволы в шаблоне.
    s += templateData[i];
  }
  return s;
}

В таком определении помеченный шаблон SaferHTML`<p>${bonk.sender} отвесил вам леща.</p>` может развернуться в строку "<p>ES6&lt;3er отвесил вам леща.</p>". Ваши пользователи в безопасности даже если пользователь со зловредным именем вроде Хакер Стив <script>alert('xss');</script> отвесит им леща. Что бы это ни значило.

(Кстати, если такой способ использования функцией объекта arguments кажется вам неуклюжим, заходите на следующей неделе. В ES6 есть ещё одна фича, которая, думаю, вам понравится.)

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

Гибкость этим не ограничивается. Заметьте, что аргументы функции-метки не приводятся автоматически к строкам. Они могут быть чем угодно. То же самое касается и возвращаемого значения. Помеченные шаблоны даже не обязательно должны быть строками! Вы можете использовать собственные метки, чтобы создавать регулярки, деревья DOM, изображения, промисы над целыми асинхронными процессами, структуры данных JS, шейдеры GL…

Помеченные шаблоны призывают разработчиков библиотек создавать мощные предметно-ориентированные языки. Эти языки могут быть вообще непохожими на JS, но при этом встраиваться в JS как влитые и разумно взаимодействовать с остальным языком. Я сходу не могу вспомнить ничего подобного в других языках. Я не знаю, к чему эта возможность нас приведёт. Возможности потрясающие.

Когда можно начинать этим пользоваться?

На сервере шаблонные строки поддерживаются в io.js уже сегодня.

Из браузеров их поддерживает Firefox 34+. В Chrome поддержка зависит от настройки «Экспериментальный JavaScript», которая по умолчанию выключена. Пока что, если вы хотите применять шаблонные строки в вебе, нужно пользоваться Babel или Traceur. Вы также можете использовать их прямо сейчас в TypeScript!

Подождите! А что насчёт Markdown?

Хм-м?

Ой. …Хороший вопрос.

(Этот раздел не про JavaScript. Если вы не пользуетесь Markdown, можете смело его пропускать.)

С появлением шаблонных строк выходит, что и Markdown и JavaScript теперь используют один и тот же символ ` для обозначения чего-то особенного. Фактически, в Markdown это разделитель кусков кода посреди обычного текста.

А вот тут небольшая проблема! Если вы напишете в документе Markdown так:

Чтобы показать сообщение, напишите `alert(`hello world!`)`.

то оно отобразится как:

Чтобы показать сообщение, напишите alert(hello world!).

Заметьте, на выходе нет обратных кавычек. Markdown интерпретировал все четыре обратные кавычки как разделители кода и заменил их на теги HTML.

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

Чтобы показать сообщение, напишите ``alert(`hello world!`)``.

В этом Gist все подробности на эту тему, и он написан на Markdown, так что вы можете посмотреть на исходник.

Что дальше

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

Мы посмотрим на них глазами человека, который реализовал их в Firefox. Так что пожалуйста присоединяйтесь к нам на следующей неделе, и наш приглашённый автор Бенджамин Петерсон (Benjamin Peterson) представит в деталях параметры по умолчанию и остаточные параметры из ES6.