Кодирование Unicode и Emoji: от кодовых точек до рендеринга
Unicode: стандарт кодирования, объединяющий все символы
Unicode назначает уникальный номер (кодовую точку) каждому символу почти всех письменностей мира.
| Понятие | Описание | Пример |
|---|---|---|
| Кодовая точка (Code Point) | Уникальный номер символа | U+4E2D (中) |
| Кодовая плоскость | Каждые 65536 кодовых точек образуют плоскость | BMP (плоскость 0) |
| BMP | Базовая многоязычная плоскость, U+0000~U+FFFF | Большинство常用ных символов |
| Дополнительная плоскость | U+10000~U+10FFFF | Emoji, древние письменности, редкие символы |
1. Семейство кодировок UTF
| Кодировка | Длина в байтах | Кодировка "中" | Особенности |
|---|---|---|---|
| UTF-8 | 1-4 байта | E4 B8 AD | Переменная длина, совместима с ASCII, стандарт для Web |
| UTF-16 | 2 или 4 байта | 4E2D | Переменная длина, внутреннее использование в JS |
| UTF-32 | Фиксированные 4 байта | 00004E2D | Фиксированная длина, большой расход памяти |
Правила кодирования UTF-8
| Диапазон кодовых точек | Байты | Шаблон кодировки |
|---|---|---|
| U+0000 ~ U+007F | 1 | 0xxxxxxx |
| U+0080 ~ U+07FF | 2 | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 ~ U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Пример кодировки
Символ: 中
Кодовая точка: U+4E2D = 0100 1110 0010 1101
UTF-8 (3 байта):
1110xxxx 10xxxxxx 10xxxxxx
1110 0100 10 111000 10 101101
= E4 B8 AD
UTF-16 (2 байта):
4E2D
UTF-32 (4 байта):
00004E2D
2. Ловушки Unicode в JavaScript
Неверная длина строки
'中'.length // 1 ✅
'𠮷'.length // 2 ❌ (символ дополнительной плоскости, суррогатная пара)
'👨👩👧👦'.length // 11 ❌ (последовательность ZWJ Emoji)
'🏳️🌈'.length // 6 ❌ (Emoji флаг)
Причина: JS использует UTF-16
Символы дополнительной плоскости (кодовая точка > U+FFFF) требуют суррогатных пар (2 кодовых единицы UTF-16):
Символ: 𠮷 (U+20BB7)
Суррогатная пара UTF-16: D842 DF87
JS: length = 2 (две кодовые единицы)
Правильная обработка
// Способ 1: оператор расширения
[...'𠮷'].length // 1 ✅
[...'👨👩👧👦'].length // 1 ✅ (но последовательность ZWJ считается как одна)
// Способ 2: Array.from
Array.from('𠮷').length // 1 ✅
// Способ 3: for...of
for (const char of '𠮷👨👩👧👦') {
console.log(char); // посимвольная итерация
}
Операции с кодовыми точками
// Получить кодовую точку
'中'.codePointAt(0) // 20013 (0x4E2D)
'𠮷'.codePointAt(0) // 134047 (0x20BB7)
// Создать символ из кодовой точки
String.fromCodePoint(0x4E2D) // '中'
String.fromCodePoint(0x20BB7) // '𠮷'
// Примечание: charCodeAt не может обработать дополнительную плоскость
'𠮷'.charCodeAt(0) // 55362 (только первый суррогат)
3. Глубокий анализ кодирования Emoji
Четыре типа Emoji
| Тип | Пример | Кодовая точка | Длина |
|---|---|---|---|
| Базовое Emoji | 😀 | U+1F600 | 1 (кодовая точка) |
| Emoji с модификатором | 👩🏽 | U+1F469 + U+1F3FD | 2 (кодовые точки) |
| Последовательность ZWJ | 👨👩👧👦 | Несколько кодовых точек + ZWJ | 7 (кодовых точек) |
| Emoji флаг | 🇨🇳 | U+1F1E8 + U+1F1F3 | 2 (кодовые точки) |
Соединитель нулевой ширины (ZWJ)
ZWJ (U+200D) объединяет несколько Emoji в одно составное Emoji:
👨 (U+1F468) + ZWJ (U+200D) + 👩 (U+1F469) + ZWJ (U+200D) + 👧 (U+1F467) + ZWJ (U+200D) + 👦 (U+1F466)
= 👨👩👧👦 (семья из четырёх человек)
Модификаторы цвета кожи
Модификаторы цвета кожи (U+1F3FB ~ U+1F3FF) изменяют цвет кожи Emoji:
👩 (U+1F469) + 🏽 (U+1F3FD) = 👩🏽 (женщина со средним цветом кожи)
| Модификатор | Кодовая точка | Цвет кожи |
|---|---|---|
| U+1F3FB | 🏻 | Светлый |
| U+1F3FC | 🏼 | Средне-светлый |
| U+1F3FD | 🏽 | Средний |
| U+1F3FE | 🏾 | Средне-тёмный |
| U+1F3FF | 🏿 | Тёмный |
Emoji флаги
Флаги состоят из двух региональных индикаторных символов (ISO 3166-1 alpha-2):
🇨 (U+1F1E8) + 🇳 (U+1F1F3) = 🇨🇳 (Китай)
🇺 (U+1F1FA) + 🇸 (U+1F1F8) = 🇺🇸 (США)
🇯 (U+1F1EF) + 🇵 (U+1F1F5) = 🇯🇵 (Япония)
4. Селекторы вариантов
VS16 (U+FE0F) преобразует текстовое представление в представление Emoji:
❤ (U+2764) → текстовое представление: ❤ (чёрное сердце)
❤️ (U+2764 + U+FE0F) → представление Emoji: ❤️ (красное сердце)
| Символ | Без VS16 | С VS16 |
|---|---|---|
| ☕ | ☕ (текст) | ☕️ (Emoji) |
| ⚡ | ⚡ (текст) | ⚡️ (Emoji) |
| ❤ | ❤ (текст) | ❤️ (Emoji) |
5. Использование библиотек инструментов для обработки Unicode
Кодирование/декодирование Unicode
- Откройте инструмент кодирования/декодирования Unicode
- Введите текст или кодовую точку
- Проверьте результаты конвертации:
- Текст → escape-последовательность Unicode (
\u4E2D) - Кодовая точка → символ
- Последовательность байтов UTF-8/16/32
- Текст → escape-последовательность Unicode (
Кодирование Emoji
- Откройте инструмент кодирования Emoji
- Введите Emoji
- Проверьте полную информацию о кодировании:
- Последовательность кодовых точек
- Байты UTF-8
- Разложение ZWJ/модификаторов
- Краткое имя CLDR
6. Распространённые ловушки и решения
Ловушка 1: Обрезка строк
// Ошибка: обрезка может разорвать суррогатные пары
text.substring(0, 10) // может создать недопустимые символы
// Правильно: обрезать по кодовой точке
function sliceByCodePoint(str, start, end) {
const chars = [...str];
return chars.slice(start, end).join('');
}
Ловушка 2: Реверс строк
// Ошибка: реверс разрушает комбинированные символы
'👨👩👧👦'.split('').reverse().join('') // нечитаемый текст
// Правильно: реверс по кодовой точке
[...'👨👩👧👦'].reverse().join('') // ✅
Ловушка 3: Регулярные выражения
// Ошибка: . не соответствует символам дополнительной плоскости
/^.$/.test('𠮷') // false
// Правильно: использовать флаг u
/^.$/u.test('𠮷') // true
Ловушка 4: Сортировка
// Сортировка по умолчанию по кодовым единицам UTF-16
['中', '𠮷', 'a'].sort() // может быть не как ожидается
// Правильно: сортировать по кодовой точке
['中', '𠮷', 'a'].sort((a, b) =>
a.codePointAt(0) - b.codePointAt(0)
)
7. Практические API
// Нормализация (обработка эквивалентности комбинированных символов)
'café'.normalize('NFC') // café (комбинация)
'café'.normalize('NFD') // café (разложение)
// Определение Emoji
function isEmoji(char) {
const code = char.codePointAt(0);
return (code >= 0x1F600 && code <= 0x1F64F) || // Эмотиконы
(code >= 0x1F300 && code <= 0x1F5FF) || // Разные символы
(code >= 0x1F680 && code <= 0x1F6FF) || // Транспорт
(code >= 0x1F900 && code <= 0x1F9FF); // Дополнительные
}
// Получить реальную длину символов
function graphemeLength(str) {
const segmenter = new Intl.Segmenter('ru', { granularity: 'grapheme' });
return [...segmenter.segment(str)].length;
}
graphemeLength('👨👩👧👦') // 1 ✅
graphemeLength('🏳️🌈') // 1 ✅
#Unicode#Emoji#UTF-8#字符编码#码点