ArrayBuffer, Buffer в Node.js, SharedArrayBuffer и Blob. Но кое-какие инструменты мы тогда не обсудили, а именно TextEncoder и TextDecoder, пару, которая переводит строки в байты и обратно.API у них достаточно простое: у одного есть
encode(), у другого decode(). Однако своих нюансов в их работу добавляет то, что строки в JavaScript хранятся в UTF-16, а данные вокруг (файлы, сеть и т.д.) почти всегда в UTF-8. Поэтому TextEncoder умеет кодировать только в UTF-8, и других вариантов у него нет, а вот TextDecoder принимает десятки кодировок.Дальше начинаются суррогатные пары. Символы выше U+FFFF (эмодзи, редкие иероглифы, некоторые математические знаки) не помещаются в один 16-битный код и хранятся как два. Отдельного внимания заслуживает случай, когда суррогат остался без пары, например, строку обрезали ровно посередине символа. Тогда
TextEncoder заменит его на U+FFFD (тот самый знаменитый �) и не выбросит никакого исключения.Дело в том, что в спецификации
encode() принимает USVString — строку, в которой суррогатов не бывает по определению. Обычные JS-строки — это DOMString, и при передаче аргумента происходит конвертация одного в другое, при которой каждый одинокий суррогат и заменяется. Таким образом до самого кодировщика битая строка уже не доходит.Похожая тихая механика, без всяких ошибок, есть и у BOM — метки порядка байтов U+FEFF, которую любят добавлять в начало файла редакторы под Windows. По умолчанию
TextDecoder её срезает, и в результирующей строке она не появляется. Опция ignoreBOM работает противоположно тому, что можно предположить по названию: она означает не «игнорировать метку», а «не обрабатывать её специальным образом», то есть оставить в строке как обычный символ.Последний нюанс касается потокового чтения. Если данные приходят чанками, многобайтовый символ вполне может оказаться разрезанным на границе. Стандартный вызов декодера посчитает такую последовательность некорректной и выдаст всё тот же символ �. Однако если передать
{ stream: true }, то декодер сохранит незавершённый хвост до следующего вызова, а финальный decode() без аргументов сбросит остаток.
const decoder = new TextDecoder();
decoder.decode(chunk, { stream: true });
decoder.decode();