Кодирование Unicode и Emoji: от кодовых точек до рендеринга

КодированиеEncoding, hashing, and QR

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

  1. Откройте инструмент кодирования/декодирования Unicode
  2. Введите текст или кодовую точку
  3. Проверьте результаты конвертации:
    • Текст → escape-последовательность Unicode (\u4E2D)
    • Кодовая точка → символ
    • Последовательность байтов UTF-8/16/32

Кодирование Emoji

  1. Откройте инструмент кодирования Emoji
  2. Введите Emoji
  3. Проверьте полную информацию о кодировании:
    • Последовательность кодовых точек
    • Байты 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#字符编码#码点