Этот раздел более подробно описывает, как устроены строки. Такие знания пригодятся, если вы намерены работать с эмодзи, редкими математическими символами, иероглифами, и т.д.
Как мы уже знаем, строки в JavaScript основаны на Юникоде: каждый символ представляет из себя последовательность байтов из 1-4 байтов.
JavaScript позволяет нам вставить символ в строку, указав его шестнадцатеричный Юникод с помощью одной из этих трех нотаций:
-
\xXX
Вместо
XX
должны быть указаны две шестнадцатеричные цифры со значением от00
доFF
. В этом случае\xXX
– это символ, Юникод которого равенXX
.Поскольку нотация
\xXX
поддерживает только две шестнадцатеричные цифры, ее можно использовать только для первых 256 символов Юникода.Эти 256 символов включают в себя латинский алфавит, большинство основных синтаксических символов и некоторые другие. Например,
"\x7A"
– это то же самое, что"z"
(ЮникодU+007A
).alert( "\x7A" ); // z alert( "\xA9" ); // ©, символ авторского права
-
\uXXXX
Вместо
XXXX
должны быть указаны ровно 4 шестнадцатеричные цифры со значением от0000
доFFFF
. В этом случае\uXXXX
– это символ, Юникод которого равенXXXX
.Символы со значениями Юникода, превышающими
U+FFFF
, также могут быть представлены с помощью этой нотации, но в таком случае нам придется использовать так называемую суррогатную пару (о ней мы поговорим позже в этой главе).alert( "\u00A9" ); // ©, то же самое, что \xA9, используя 4-значную шестнадцатеричную нотацию alert( "\u044F" ); // я, буква кириллического алфавита alert( "\u2191" ); // ↑, символ стрелки вверх
-
\u{X…XXXXXX}
Вместо
X…XXXXXX
должно быть шестнадцатеричное значение от 1 до 6 байт от0
до10FFFF
(максимальная точка кода, определенная стандартом Юникод). Эта нотация позволяет нам легко представлять все существующие символы Юникода.alert( "\u{20331}" ); // 佫, редкий китайский иероглиф (длинный Юникод) alert( "\u{1F60D}" ); // 😍, символ улыбающегося лица (ещё один длинный Юникод)
Суррогатные пары
Все часто используемые символы имеют 2-байтовые коды (4 шестнадцатеричные цифры). В большинстве европейских языков буквы, цифры и основные унифицированные идеографические наборы CJK (CJK – от китайской, японской и корейской систем письма) имеют 2-байтовое представление.
Изначально JavaScript был основан на кодировке UTF-16, которая предусматривала только 2 байта на один символ. Однако 2 байта допускают только 65536 комбинаций, и этого недостаточно для всех возможных символов Юникода.
Поэтому редкие символы, требующие более 2 байт, кодируются парой 2-байтовых символов, которые называются «суррогатной парой».
Побочным эффектом является то, что длина таких символов равна 2
:
alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X
alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY
alert( '𩷶'.length ); // 2, редкий китайский иероглиф
Это происходит потому, что суррогатные пары не существовали в то время, когда был создан JavaScript, и поэтому они не обрабатываются языком корректно.
На самом деле в каждой из приведенных строк у нас по одному символу, но свойство length
показывает длину 2
.
Получить такой символ также может быть непросто, поскольку большинство языковых функций рассматривают суррогатные пары как два символа.
Например, здесь мы видим два странных символа в выводе:
alert( '𝒳'[0] ); // показывает странные символы...
alert( '𝒳'[1] ); // ...части суррогатной пары
Части суррогатной пары не имеют никакого значения друг без друга.
Технически, суррогатные пары также можно определить по их кодам: если символ имеет код в интервале 0xd800...0xdbff
, то он является первой частью суррогатной пары. Следующий символ (вторая часть) должен иметь код в интервале 0xdc00...0xdfff
. Эти интервалы зарезервированы стандартом исключительно для суррогатных пар.
Поэтому для работы с суррогатными парами в JavaScript были добавлены методы String.fromCodePoint
и str.codePointAt
.
По сути, они аналогичны String.fromCharCode и str.charCodeAt, но они правильно обрабатывают суррогатные пары.
Здесь можно увидеть разницу:
// charCodeAt не учитывает суррогатные пары, поэтому он выдает коды для 1-й части 𝒳:
alert( '𝒳'.charCodeAt(0).toString(16) ); // d835
// codePointAt учитывает суррогатные пары
alert( '𝒳'.codePointAt(0).toString(16) ); // 1d4b3, считывает обе части суррогатной пары
При этом, если брать с позиции 1 (а это здесь скорее неверно), то они оба возвращают только 2-ю часть пары:
alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3
alert( '𝒳'.codePointAt(1).toString(16) ); // dcb3
// бессмысленная 2-я половина пары
Другие способы работы с суррогатными парами вы найдете в главе Перебираемые объекты. Возможно, для этого тоже существуют специальные библиотеки, но они не настолько известные, чтобы предлагать их в учебнике.
Разделив строку в случайном месте, например, с помощью str.slice(0, 4)
, мы не можем гарантировать валидность полученного значения. Например:
alert( 'hi 😂'.slice(0, 4) ); // hi [?]
Здесь мы видим мусорный символ (первая половина суррогатной пары 😂) в выводе.
Просто имейте это в виду, если вы намерены надежно работать с суррогатными парами. Может быть, это не очень большая проблема, но, по крайней мере, вы должны понимать, что происходит.
Диакритические знаки и нормализация
Во многих языках есть символы, состоящие из основного символа и знака над/под ним.
Например, буква a
может быть основой для этих символов: àáâäãåā
.
Большинство распространенных «составных» символов имеют свой собственный код в таблице Юникода. Но не все, потому что существует слишком большое количество возможных комбинаций.
Для поддержки любых комбинаций стандарт Юникод позволяет нам использовать несколько Юникодных символов: основной символ, за которым следует один или много символов-«меток», которые «украшают» его.
Например, если за S
следует специальный символ «точка сверху» (код \u0307
), то он отобразится как Ṡ.
alert( 'S\u0307' ); // Ṡ
Если нам нужен дополнительный знак над буквой (или под ней) – нет проблем, просто добавляем соответствующий символ.
Например, если мы добавим символ «точка снизу» (код \u0323
), то получим «S с точками сверху и снизу»: Ṩ
.
Вот, как это будет выглядеть:
alert( 'S\u0307\u0323' ); // Ṩ
Это обеспечивает большую гибкость, но при этом возникает определенная проблема: два символа могут визуально выглядеть одинаково, но при этом они будут представлены разными комбинациями Юникода.
Например:
let s1 = 'S\u0307\u0323'; // Ṩ, S + точка сверху + точка снизу
let s2 = 'S\u0323\u0307'; // Ṩ, S + точка снизу + точка сверху
alert( `s1: ${s1}, s2: ${s2}` );
alert( s1 == s2 ); // false, хотя символы выглядят одинаково (?!)
Для решения этой проблемы предусмотрен алгоритм «Юникодной нормализации», приводящий каждую строку к единому «нормальному» виду.
Его реализует метод 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, но для большинства практических целей достаточно информации из этого раздела.