7 июня 2022 г.

Строки

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Есть ряд улучшений и новых методов для строк.

Начнём с, пожалуй, самого важного.

Строки-шаблоны

Добавлен новый вид кавычек для строк:

let str = `обратные кавычки`;

Основные отличия от двойных "…" и одинарных '…' кавычек:

  • В них разрешён перевод строки.

    Например:

    alert(`моя
      многострочная
      строка`);

    Заметим, что пробелы и, собственно, перевод строки также входят в строку, и будут выведены.

  • Можно вставлять выражения при помощи ${…}.

    Например:

    'use strict';
    let apples = 2;
    let oranges = 3;
    
    alert(`${apples} + ${oranges} = ${apples + oranges}`); // 2 + 3 = 5

    Как видно, при помощи ${…} можно вставлять как и значение переменной ${apples}, так и более сложные выражения, которые могут включать в себя операторы, вызовы функций и т.п. Такую вставку называют «интерполяцией».

Функции шаблонизации

Можно использовать свою функцию шаблонизации для строк.

Название этой функции ставится перед первой обратной кавычкой:

let str = func`моя строка`;

Эта функция будет автоматически вызвана и получит в качестве аргументов строку, разбитую по вхождениям параметров ${…} и сами эти параметры.

Например:

'use strict';

function f(strings, ...values) {
  alert(JSON.stringify(strings));     // ["Sum of "," + "," =\n ","!"]
  alert(JSON.stringify(strings.raw)); // ["Sum of "," + "," =\\n ","!"]
  alert(JSON.stringify(values));      // [3,5,8]
}

let apples = 3;
let oranges = 5;

//          |  s[0] | v[0] |s[1]| v[1]  |s[2]  |      v[2]      |s[3]
let str = f`Sum of ${apples} + ${oranges} =\n ${apples + oranges}!`;

В примере выше видно, что строка разбивается по очереди на части: «кусок строки» – «параметр» – «кусок строки» – «параметр».

  • Участки строки идут в первый аргумент-массив strings.
  • У этого массива есть дополнительное свойство strings.raw. В нём находятся строки в точности как в оригинале. Это влияет на спец-символы, например в strings символ \n – это перевод строки, а в strings.raw – это именно два символа \n.
  • Дальнейший список аргументов функции шаблонизации – это значения выражений в ${...}, в данном случае их три.
Зачем strings.raw?

В отличие от strings, в strings.raw содержатся участки строки в «изначально введённом» виде.

То есть, если в строке находится \n или \u1234 или другое особое сочетание символов, то оно таким и останется.

Это нужно в тех случаях, когда функция шаблонизации хочет произвести обработку полностью самостоятельно (свои спец. символы?). Или же когда обработка спец. символов не нужна – например, строка содержит «обычный текст», набранный непрограммистом без учёта спец. символов.

Как видно, функция имеет доступ ко всему: к выражениям, к участкам текста и даже, через strings.raw – к оригинально введённому тексту без учёта стандартных спец. символов.

Функция шаблонизации может как-то преобразовать строку и вернуть новый результат.

В простейшем случае можно просто «склеить» полученные фрагменты в строку:

'use strict';

// str восстанавливает строку
function str(strings, ...values) {
  let str = "";
  for(let i=0; i<values.length; i++) {
    str += strings[i];
    str += values[i];
  }

  // последний кусок строки
  str += strings[strings.length-1];
  return str;
}

let apples = 3;
let oranges = 5;

// Sum of 3 + 5 = 8!
alert( str`Sum of ${apples} + ${oranges} = ${apples + oranges}!`);

Функция str в примере выше делает то же самое, что обычные обратные кавычки. Но, конечно, можно пойти намного дальше. Например, генерировать из HTML-строки DOM-узлы (функции шаблонизации не обязательно возвращать именно строку).

Или можно реализовать интернационализацию. В примере ниже функция i18n осуществляет перевод строки.

Она подбирает по строке вида "Hello, ${name}!" шаблон перевода "Привет, {0}!" (где {0} – место для вставки параметра) и возвращает переведённый результат со вставленным именем name:

'use strict';

let messages = {
  "Hello, {0}!": "Привет, {0}!"
};

function i18n(strings, ...values) {
  // По форме строки получим шаблон для поиска в messages
  // На месте каждого из значений будет его номер: {0}, {1}, …
  let pattern = "";
  for(let i=0; i<values.length; i++) {
    pattern += strings[i] + '{' + i + '}';
  }
  pattern += strings[strings.length-1];
  // Теперь pattern = "Hello, {0}!"

  let translated = messages[pattern]; // "Привет, {0}!"

  // Заменит в "Привет, {0}" цифры вида {num} на values[num]
  return translated.replace(/\{(\d)\}/g, (s, num) => values[num]);
}

// Пример использования
let name = "Вася";

// Перевести строку
alert( i18n`Hello, ${name}!` ); // Привет, Вася!

Итоговое использование выглядит довольно красиво, не правда ли?

Разумеется, эту функцию можно улучшить и расширить. Функция шаблонизации – это своего рода «стандартный синтаксический сахар» для упрощения форматирования и парсинга строк.

Улучшена поддержка Юникода

Внутренняя кодировка строк в JavaScript – это UTF-16, то есть под каждый символ отводится ровно два байта.

Но под всевозможные символы всех языков мира 2 байт не хватает. Поэтому бывает так, что одному символу языка соответствует два Юникодных символа (итого 4 байта). Такое сочетание называют «суррогатной парой».

Самый частый пример суррогатной пары, который можно встретить в литературе – это китайские иероглифы.

Заметим, однако, что не всякий китайский иероглиф – суррогатная пара. Существенная часть «основного» Юникод-диапазона как раз отдана под китайский язык, поэтому некоторые иероглифы – которые в неё «влезли» – представляются одним Юникод-символом, а те, которые не поместились (реже используемые) – двумя.

Например:

alert( '我'.length ); // 1
alert( '𩷶'.length ); // 2

В тексте выше для первого иероглифа есть отдельный Юникод-символ, и поэтому длина строки 1, а для второго используется суррогатная пара. Соответственно, длина – 2.

Китайскими иероглифами суррогатные пары, естественно, не ограничиваются.

Ими представлены редкие математические символы, а также некоторые символы для эмоций, к примеру:

alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X
alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY

В современный JavaScript добавлены методы String.fromCodePoint и str.codePointAt – аналоги String.fromCharCode и str.charCodeAt, корректно работающие с суррогатными парами.

Например, charCodeAt считает суррогатную пару двумя разными символами и возвращает код каждой:

// как будто в строке два разных символа (на самом деле один)
alert( '𝒳'.charCodeAt(0) + ' ' + '𝒳'.charCodeAt(1) ); // 55349 56499

…В то время как codePointAt возвращает его Unicode-код суррогатной пары правильно:

// один символ с "длинным" (более 2 байт) unicode-кодом
alert( '𝒳'.codePointAt(0) ); // 119987

Метод String.fromCodePoint(code) корректно создаёт строку из «длинного кода», в отличие от старого String.fromCharCode(code).

Например:

// Правильно
alert( String.fromCodePoint(119987) ); // 𝒳
// Неверно!
alert( String.fromCharCode(119987) ); // 풳

Более старый метод fromCharCode в последней строке дал неверный результат, так как он берёт только первые два байта от числа 119987 и создаёт символ из них, а остальные отбрасывает.

\u{длинный код}

Есть и ещё синтаксическое улучшение для больших Unicode-кодов.

В JavaScript-строках давно можно вставлять символы по Unicode-коду, вот так:

alert( "\u2033" ); // ″, символ двойного штриха

Синтаксис: \uNNNN, где NNNN – четырёхзначный шестнадцатиричный код, причём он должен быть ровно четырёхзначным.

«Лишние» цифры уже не войдут в код, например:

alert( "\u20331" ); // Два символа: символ двойного штриха ″, а затем 1

Чтобы вводить более длинные коды символов, добавили запись \u{NNNNNNNN}, где NNNNNNNN – максимально восьмизначный (но можно и меньше цифр) код.

Например:

alert( "\u{20331}" ); // 𠌱, китайский иероглиф с этим кодом

Unicode-нормализация

Во многих языках есть символы, которые получаются как сочетание основного символа и какого-то значка над ним или под ним.

Например, на основе обычного символа a существуют символы: àáâäãåā. Самые часто встречающиеся подобные сочетания имеют отдельный Юникодный код. Но отнюдь не все.

Для генерации произвольных сочетаний используются несколько Юникодных символов: основа и один или несколько значков.

Например, если после символа S идёт символ «точка сверху» (код \u0307), то показано это будет как «S с точкой сверху» .

Если нужен ещё значок над той же буквой (или под ней) – без проблем. Просто добавляем соответствующий символ.

К примеру, если добавить символ «точка снизу» (код \u0323), то будет «S с двумя точками сверху и снизу» Ṩ .

Пример этого символа в JavaScript-строке:

alert("S\u0307\u0323"); // Ṩ

Такая возможность добавить произвольной букве нужные значки, с одной стороны, необходима, а с другой стороны – возникает проблемка: можно представить одинаковый с точки зрения визуального отображения и интерпретации символ – разными сочетаниями Unicode-кодов.

Вот пример:

alert("S\u0307\u0323"); // Ṩ
alert("S\u0323\u0307"); // Ṩ

alert( "S\u0307\u0323" == "S\u0323\u0307" ); // false

В первой строке после основы S идёт сначала значок «верхняя точка», а потом – нижняя, во второй – наоборот. По кодам строки не равны друг другу. Но символ задают один и тот же.

С целью разрешить эту ситуацию, существует Юникодная нормализация, при которой строки приводятся к единому, «нормальному», виду.

В современном JavaScript это делает метод str.normalize().

alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true

Забавно, что в данной конкретной ситуации normalize() приведёт последовательность из трёх символов к одному: \u1e68 (S с двумя точками).

alert( "S\u0307\u0323".normalize().length ); // 1, нормализовало в один символ
alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true

Это, конечно, не всегда так, просто в данном случае оказалось, что именно такой символ в Юникоде уже есть. Если добавить значков, то нормализация уже даст несколько символов.

Для большинства практических задач информации, данной выше, должно быть вполне достаточно, но если хочется более подробно ознакомиться с вариантами и правилами нормализации – они описаны в приложении к стандарту Юникод Unicode Normalization Forms.

Полезные методы

Добавлен ряд полезных методов общего назначения:

  • str.includes(s) – проверяет, включает ли одна строка в себя другую, возвращает true/false.
  • str.endsWith(s) – возвращает true, если строка str заканчивается подстрокой s.
  • str.startsWith(s) – возвращает true, если строка str начинается со строки s.
  • str.repeat(times) – повторяет строку str times раз.

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

Итого

Улучшения:

  • Строки-шаблоны – для удобного задания строк (многострочных, с переменными), плюс возможность использовать функцию шаблонизации для самостоятельного форматирования.
  • Юникод – улучшена работа с суррогатными парами.
  • Полезные методы для проверок вхождения одной строки в другую.
Карта учебника