Управление кодировкой UTF-8 в 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 указывают, сколько было обработано. Следует организовать цикл для дописывания оставшейся части.