Управление кодировкой UTF-8 в JavaScript: практическое пособие

Раздел: JavaScript -> Настройка кодировки

Обзор работы с кодировкой UTF-8 в JavaScript

Важность понимания UTF-8

Строки в JavaScript хранятся в кодировке UTF-16. Для взаимодействия с бинарными данными, файлами или сетевыми протоколами требуется преобразование в UTF-8. Статья рассматривает основные способы кодирования и декодирования, их цели, ограничения и возможные ошибки.

Как эффективно кодировать и декодировать строки в UTF-8?

Стандартный API TextEncoder и TextDecoder предоставляет встроенную поддержку UTF-8. Он доступен в современных браузерах и Node.js (начиная с v11).

// Кодирование строки в Uint8Array
const encoder = new TextEncoder();
const bytes = encoder.encode('Привет, мир!');
console.log(bytes); // Uint8Array(18) [208, 159, 209, 128, ...]

// Декодирование обратно
const decoder = new TextDecoder();
const str = decoder.decode(bytes);
console.log(str); // Привет, мир!

Js utf 8 (кодировка utf-8 в javascript)

Типичные проблемы:

  • TextDecoder по умолчанию не выбрасывает ошибку при невалидных байтах, а заменяет их на символ замены (U+FFFD). Чтобы получить исключение, используйте {fatal: true}.
  • Кодировщик всегда производит только UTF-8; для других кодировок (например, Windows-1251) необходимо применять сторонние библиотеки или платформенные решения.
  • Полученный Uint8Array имеет длину, равную количеству байт, а не символов - это путает разработчиков, привыкших к String.length.

Как безопасно передать строку через URL в UTF-8?

Функции encodeURIComponent и decodeURIComponent кодируют строку в процентное представление, где каждый байт UTF-8 представлен в виде %XX. Это не даёт сырой массив байтов, но удобно для URL.

const encoded = encodeURIComponent('тест');
console.log(encoded); // %D1%82%D0%B5%D1%81%D1%82
const decoded = decodeURIComponent(encoded);
console.log(decoded); // тест

Ошибки при использовании:

  • Не заменяет символы, недопустимые в URL (пробелы, кириллица), но делает это корректно в UTF-8.
  • Для получения чистых байтов потребуется дополнительная обработка: разбор процентной последовательности в числа.

Как вручную преобразовать строку в UTF-8 байты, используя только базовые методы?

Цель - понимание внутреннего механизма кодирования. Алгоритм: для каждого символа получить его кодовую точку, затем разбить на один, два, три или четыре байта в зависимости от диапазона.

function stringToUtf8Bytes(str) {
  const bytes = [];
  for (let i = 0; i < str.length; i++) {
    let charCode = str.charCodeAt(i);
    if (charCode < 0x80) {
      bytes.push(charCode);
    } else if (charCode < 0x800) {
      bytes.push(0xC0 | (charCode >> 6), 0x80 | (charCode & 0x3F));
    } else if (charCode < 0xD800 || charCode >= 0xE000) {
      bytes.push(0xE0 | (charCode >> 12),
                 0x80 | ((charCode >> 6) & 0x3F),
                 0x80 | (charCode & 0x3F));
    } else {
      // суррогатная пара
      i++;
      const nextCode = str.charCodeAt(i);
      const codePoint = ((charCode - 0xD800) << 10) + (nextCode - 0xDC00) + 0x10000;
      bytes.push(0xF0 | (codePoint >> 18),
                 0x80 | ((codePoint >> 12) & 0x3F),
                 0x80 | ((codePoint >> 6) & 0x3F),
                 0x80 | (codePoint & 0x3F));
    }
  }
  return new Uint8Array(bytes);
}
console.log(stringToUtf8Bytes('A?')); // [65, 240, 160, 156, 142]

Проблемы реализации:

  • Необходимость обработки суррогатных пар: charCodeAt возвращает суррогатную единицу, а не полную кодовую точку. Использование codePointAt упрощает задачу.
  • Метод устарел: современные движки оптимизированы для TextEncoder, ручное кодирование работает на порядок медленнее.

Как работать с UTF-8 в Node.js с использованием Buffer?

В Node.js класс Buffer обеспечивает быструю работу с бинарными данными и поддерживает множество кодировок.

const buf = Buffer.from('Привет', 'utf8');
console.log(buf); // 
console.log(buf.toString('utf8')); // Привет

// Декодирование частичного буфера
const partial = Buffer.from([0xD0, 0x9F, 0xD1, 0x80]);
console.log(partial.toString('utf8')); // Пр

Ошибки:

  • Buffer доступен только в Node.js. В браузерах его нет, но есть Uint8Array и TextDecoder.
  • При неправильной длине буфера (например, обрыв многобайтного символа) toString('utf8') молча обрезает символ, не сообщая об ошибке.

Как получить UTF-8 байты через Blob и FileReader (браузерный способ без TextEncoder)?

Для обратной совместимости можно использовать Blob и FileReader

async function stringToBytesUsingBlob(str) {
  const blob = new Blob([str]);  // Blob использует UTF-8 по умолчанию
  const arrayBuffer = await blob.arrayBuffer();
  return new Uint8Array(arrayBuffer);
}
stringToBytesUsingBlob('тест').then(b => console.log(b));
// Uint8Array(8) [209, 130, 208, 181, 209, 129, 209, 130]

Проблемы:

  • Асинхронный метод, менее производительный, чем синхронный TextEncoder.encode.
  • Поддерживается не во всех старых браузерах (требуется полифиллы).

Расширенные примеры работы с UTF-8

Потоковое декодирование с TextDecoder

При получении данных по частям (например, по сети) важно корректно обрабатывать границы многобайтных символов. TextDecoder поддерживает режим {stream: true}, который сохраняет внутреннее состояние.

Пример
const decoder = new TextDecoder('utf-8', {fatal: true, stream: true});

// Первая часть данных
const part1 = new Uint8Array([0xD0, 0x9F]); // начало 'П'
console.log(decoder.decode(part1, {stream: true})); // '' - символ не завершён

// Вторая часть
const part2 = new Uint8Array([0xD1, 0x80, 0xD0, 0xB8]); // 'ри'
console.log(decoder.decode(part2, {stream: true})); // 'При'

// Последняя часть
const part3 = new Uint8Array([0xD0, 0xB5, 0xD1, 0x82]); // 'ет'
console.log(decoder.decode(part3, {stream: false})); // 'Привет'

Примечание:

Если не указать stream: true, декодер будет считать каждый вызов самостоятельным и попытается декодировать неполную последовательность, вызывая ошибку (при fatal) или замену символа.

Работа с суррогатными парами: codePointAt вместо charCodeAt

Символы за пределами Basic Multilingual Plane (U+10000 – U+10FFFF) занимают два суррогата в UTF-16. Метод String.prototype.codePointAt возвращает полную кодовую точку, что упрощает кодирование.

Пример
const emoji = '?'; // U+1F600
console.log(emoji.length); // 2 (суррогатная пара)
console.log(emoji.charCodeAt(0).toString(16)); // d83d
console.log(emoji.charCodeAt(1).toString(16)); // de00
console.log(emoji.codePointAt(0).toString(16)); // 1f600

// Кодирование с codePointAt
function utf8EncodeUsingCodePoint(str) {
  const bytes = [];
  for (let cp of str) {
    const code = cp.codePointAt(0);
    if (code <= 0x7F) {
      bytes.push(code);
    } else if (code <= 0x7FF) {
      bytes.push(0xC0 | (code >> 6), 0x80 | (code & 0x3F));
    } else if (code <= 0xFFFF) {
      bytes.push(0xE0 | (code >> 12),
                 0x80 | ((code >> 6) & 0x3F),
                 0x80 | (code & 0x3F));
    } else {
      bytes.push(0xF0 | (code >> 18),
                 0x80 | ((code >> 12) & 0x3F),
                 0x80 | ((code >> 6) & 0x3F),
                 0x80 | (code & 0x3F));
    }
  }
  return new Uint8Array(bytes);
}
console.log(utf8EncodeUsingCodePoint('?')); // Uint8Array(4) [240, 159, 152, 128]

Преобразование hex-строки в UTF-8

Иногда данные передаются в шестнадцатеричном представлении (например, в API). Для декодирования требуется сначала преобразовать hex в массив байт, затем применить TextDecoder.

Пример
function hexToUtf8(hexStr) {
  // удаляем пробелы и приводим к единому виду
  const clean = hexStr.replace(/\s/g, '');
  const bytes = new Uint8Array(clean.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(clean.substr(i * 2, 2), 16);
  }
  return new TextDecoder().decode(bytes);
}

const hex = 'D0 9F D1 80 D0 B8 D0 B2 D0 B5 D1 82'; // 'Привет'
console.log(hexToUtf8(hex)); // 'Привет'

Ошибка:

Если hex-строка содержит нечётное количество символов или невалидные шестнадцатеричные цифры, parseInt может вернуть NaN, что приведёт к искажению данных. Необходима валидация входной строки.

Удаление BOM (Byte Order Mark) из UTF-8 данных

Некоторые источники добавляют BOM (0xEF, 0xBB, 0xBF) в начало потока. TextDecoder имеет опцию ignoreBOM: true, которая автоматически пропускает BOM.

Пример
const bytesWithBOM = new Uint8Array([0xEF, 0xBB, 0xBF, 0xD0, 0x9F]); // BOM + 'П'
const decoder1 = new TextDecoder('utf-8', {ignoreBOM: true});
console.log(decoder1.decode(bytesWithBOM)); // 'П' (без BOM)

const decoder2 = new TextDecoder('utf-8', {ignoreBOM: false});
console.log(decoder2.decode(bytesWithBOM)); // '\uFEFFП' (BOM как символ)

Сравнение производительности методов кодирования

Для больших строк (сотни тысяч символов) различия существенны. TextEncoder оптимизирован на уровне браузера, а ручное кодирование может быть в 10–20 раз медленнее.

Пример
// Измерение времени (упрощённо)
const longString = 'A'.repeat(100000) + '?'.repeat(10000);

console.time('TextEncoder');
new TextEncoder().encode(longString);
console.timeEnd('TextEncoder');

console.time('Manual UTF-8');
stringToUtf8Bytes(longString);
console.timeEnd('Manual UTF-8');

// Примерный результат:
// TextEncoder: 0.5ms
// Manual UTF-8: 12ms

Использование TextEncoder.encodeInto для частичного кодирования

Метод encodeInto позволяет кодировать строку в готовый Uint8Array, контролируя смещение и ограничивая количество записываемых байт. Полезно при работе с буферами фиксированного размера.

Пример
const encoder = new TextEncoder();
const dest = new Uint8Array(10); // маленький буфер
const result = encoder.encodeInto('Привет, мир!', dest);
console.log(result.read);   // 7 (сколько символов обработано)
console.log(result.written); // 10 (сколько байт записано)
console.log(dest);          // первые 10 байт строки

// Оставшиеся символы можно дописать в следующий буфер
const rest = encoder.encodeInto('мир!', dest);
console.log(rest);

Важно:

Если буфер слишком мал, часть символов (особенно многобайтных) может оказаться недописанной. Поля read и written указывают, сколько было обработано. Следует организовать цикл для дописывания оставшейся части.

Кодировка UTF-8 в JavaScript - comments

En
Js utf 8 (javascript)