Юникод

Строки в JavaScript основаны на Юникоде: каждый символ представляет из себя последовательность байтов из 1-4 байтов.

JavaScript позволяет вставить символ в строку, указав его шестнадцатеричный Юникод с помощью одной из этих трех нотаций:

Суррогатные пары

Все часто используемые символы имеют 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.

Справочник JavaScript
×
Справочник JavaScript