В цьому розділі ми зануримося у внутрішню будову рядків. Ці знання знадобляться вам, якщо ви плануєте мати справу з емодзі, рідкісними математичними чи ієрогліфічними символами та іншими винятковими символами.
Як вже відомо, рядки в 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, математичний символ, велика X
alert( '😂'.length ); // 2, обличчя зі сльозами радості
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, 8)
і очікувати, що це буде дійсний рядок, наприклад:
alert( 'Привіт 😂'.slice(0, 8) ); // Привіт [?]
Тут ми можемо побачити незрозумілий символ (першу половину сурогатної пари посмішки) у виведених даних.
Просто пам’ятайте про це, якщо ви хочете надійно працювати із сурогатними парами. Можливо, це не велика проблема, але принаймні ви повинні розуміти, що відбувається.
Діакритичні знаки та нормалізація
У багатьох мовах є символи, які складаються з основного символу та знаку над/під ним.
Наприклад, літера 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()
фактично об’єднує послідовність із 3 символів в один: \u1e68
(S з двома крапками).
alert( "S\u0307\u0323".normalize().length ); // 1
alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true
Насправді, це не завжди так. Причина в тому, що символ Ṩ
є “достатньо поширеним”, тому творці Юнікоду включили його в основну таблицю та дали йому окремий код.
Якщо ви хочете дізнатися більше про правила та варіанти нормалізації – вони описані в додатку до стандарту Юнікод: Unicode Normalization Forms, але для більшості практичних задач інформації з цього розділу достатньо.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)