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