Skip to content

Эта страница предназначена как введение в работу с двоичными данными в JavaScript. Bun реализует ряд типов данных и утилит для работы с двоичными данными, большинство из которых являются веб-стандартами. Любые API, специфичные для Bun, будут отмечены.

Ниже приведена быстрая «шпаргалка», которая также служит оглавлением. Нажмите на элемент в левом столбце, чтобы перейти к этому разделу.

КлассОписание
TypedArrayСемейство классов, предоставляющих интерфейс, подобный Array, для взаимодействия с двоичными данными. Включает Uint8Array, Uint16Array, Int8Array и другие.
BufferПодкласс Uint8Array, реализующий широкий спектр вспомогательных методов. В отличие от других элементов в этой таблице, это API Node.js (который реализует Bun). Не может использоваться в браузере.
DataViewКласс, предоставляющий API get/set для записи некоторого количества байтов в ArrayBuffer по определённому смещению в байтах. Часто используется для чтения или записи двоичных протоколов.
BlobБлоб только для чтения, обычно представляющий файл. Имеет MIME-type, size и методы для преобразования в ArrayBuffer, ReadableStream и строку.
FileПодкласс Blob, представляющий файл. Имеет name и метку времени lastModified. Имеется экспериментальная поддержка в Node.js v20.
BunFileТолько Bun. Подкласс Blob, представляющий лениво загружаемый файл на диске. Создаётся с помощью Bun.file(path).

ArrayBuffer и представления

До 2009 года в JavaScript не было встроенного способа хранения и обработки двоичных данных. ECMAScript v5 представил ряд новых механизмов для этого. Наиболее фундаментальным строительным блоком является ArrayBuffer — простая структура данных, представляющая последовательность байтов в памяти.

ts
// этот буфер может хранить 8 байт
const buf = new ArrayBuffer(8);

Несмотря на название, это не массив и не поддерживает ни один из методов и операторов массива, которых можно было бы ожидать. На самом деле, нет способа напрямую читать или записывать значения из ArrayBuffer. С ним можно сделать очень мало, кроме проверки его размера и создания «срезов» из него.

ts
const buf = new ArrayBuffer(8);
buf.byteLength; // => 8

const slice = buf.slice(0, 4); // возвращает новый ArrayBuffer
slice.byteLength; // => 4

Чтобы сделать что-то интересное, нам нужна конструкция, известная как «представление» (view). Представление — это класс, который оборачивает экземпляр ArrayBuffer и позволяет читать и изменять лежащие в основе данные. Существует два типа представлений: типизированные массивы и DataView.

DataView

Класс DataView — это низкоуровневый интерфейс для чтения и изменения данных в ArrayBuffer.

Ниже мы создаём новый DataView и устанавливаем первый байт в 3.

ts
const buf = new ArrayBuffer(4);
// [0b00000000, 0b00000000, 0b00000000, 0b00000000]

const dv = new DataView(buf);
dv.setUint8(0, 3); // записать значение 3 по смещению байта 0
dv.getUint8(0); // => 3
// [0b00000011, 0b00000000, 0b00000000, 0b00000000]

Теперь запишем Uint16 по смещению байта 1. Это требует два байта. Мы используем значение 513, которое равно 2 * 256 + 1; в байтах это 00000010 00000001.

ts
dv.setUint16(1, 513);
// [0b00000011, 0b00000010, 0b00000001, 0b00000000]

console.log(dv.getUint16(1)); // => 513

Мы присвоили значение первым трём байтам в нашем базовом ArrayBuffer. Хотя второй и третий байты были созданы с помощью setUint16(), мы всё ещё можем прочитать каждый из их составляющих байтов с помощью getUint8().

ts
console.log(dv.getUint8(1)); // => 2
console.log(dv.getUint8(2)); // => 1

Попытка записать значение, требующее больше места, чем доступно в базовом ArrayBuffer, вызовет ошибку. Ниже мы пытаемся записать Float64 (который требует 8 байтов) по смещению байта 0, но в буфере всего четыре байта.

ts
dv.setFloat64(0, 3.1415);
// ^ RangeError: Доступ за пределами диапазона

Следующие методы доступны в DataView:

ГеттерыСеттеры
getBigInt64()setBigInt64()
getBigUint64()setBigUint64()
getFloat32()setFloat32()
getFloat64()setFloat64()
getInt16()setInt16()
getInt32()setInt32()
getInt8()setInt8()
getUint16()setUint16()
getUint32()setUint32()
getUint8()setUint8()

TypedArray

Типизированные массивы — это семейство классов, предоставляющих интерфейс, подобный Array, для взаимодействия с данными в ArrayBuffer. В то время как DataView позволяет записывать числа разного размера по определённому смещению, TypedArray интерпретирует базовые байты как массив чисел, каждое фиксированного размера.

NOTE

Обычно это семейство классов collectively называют их общим суперклассом `TypedArray`. Этот класс является _внутренним_ для JavaScript; вы не можете напрямую создавать его экземпляры, и `TypedArray` не определён в глобальной области. Думайте о нём как об `interface` или абстрактном классе.
ts
const buffer = new ArrayBuffer(3);
const arr = new Uint8Array(buffer);

// содержимое инициализируется нулями
console.log(arr); // Uint8Array(3) [0, 0, 0]

// присваивать значения как массиву
arr[0] = 0;
arr[1] = 10;
arr[2] = 255;
arr[3] = 255; // нет операции, за пределами диапазона

В то время как ArrayBuffer — это последовательность байтов, эти классы типизированных массивов интерпретируют байты как массив чисел заданного размера в байтах. Верхняя строка содержит необработанные байты, а последующие строки содержат то, как эти байты будут интерпретироваться при представлении с помощью различных классов типизированных массивов.

Следующие классы являются типизированными массивами вместе с описанием того, как они интерпретируют байты в ArrayBuffer:

КлассОписание
Uint8ArrayКаждый один (1) байт интерпретируется как беззнаковое 8-битное целое число. Диапазон от 0 до 255.
Uint16ArrayКаждые два (2) байта интерпретируются как беззнаковое 16-битное целое число. Диапазон от 0 до 65535.
Uint32ArrayКаждые четыре (4) байта интерпретируются как беззнаковое 32-битное целое число. Диапазон от 0 до 4294967295.
Int8ArrayКаждый один (1) байт интерпретируется как знаковое 8-битное целое число. Диапазон от -128 до 127.
Int16ArrayКаждые два (2) байта интерпретируются как знаковое 16-битное целое число. Диапазон от -32768 до 32767.
Int32ArrayКаждые четыре (4) байта интерпретируются как знаковое 32-битное целое число. Диапазон от -2147483648 до 2147483647.
Float16ArrayКаждые два (2) байта интерпретируются как 16-битное число с плавающей точкой. Диапазон от -6.104e5 до 6.55e4.
Float32ArrayКаждые четыре (4) байта интерпретируются как 32-битное число с плавающей точкой. Диапазон от -3.4e38 до 3.4e38.
Float64ArrayКаждые восемь (8) байтов интерпретируются как 64-битное число с плавающей точкой. Диапазон от -1.7e308 до 1.7e308.
BigInt64ArrayКаждые восемь (8) байтов интерпретируются как знаковый BigInt. Диапазон от -9223372036854775808 до 9223372036854775807 (хотя BigInt способен представлять большие числа).
BigUint64ArrayКаждые восемь (8) байтов интерпретируются как беззнаковый BigInt. Диапазон от 0 до 18446744073709551615 (хотя BigInt способен представлять большие числа).
Uint8ClampedArrayТо же, что Uint8Array, но автоматически «ограничивает» диапазоном 0-255 при присваивании значения элементу.

Таблица ниже демонстрирует, как байты в ArrayBuffer интерпретируются при представлении с помощью различных классов типизированных массивов.

Байт 0Байт 1Байт 2Байт 3Байт 4Байт 5Байт 6Байт 7
ArrayBuffer0000000000000001000000100000001100000100000001010000011000000111
Uint8Array01234567
Uint16Array256 (1 * 256 + 0)770 (3 * 256 + 2)1284 (5 * 256 + 4)1798 (7 * 256 + 6)
Uint32Array50462976117835012
BigUint64Array506097522914230528n

Для создания типизированного массива из предопределённого ArrayBuffer:

ts
// создать типизированный массив из ArrayBuffer
const buf = new ArrayBuffer(10);
const arr = new Uint8Array(buf);

arr[0] = 30;
arr[1] = 60;

// все элементы инициализируются нулями
console.log(arr); // => Uint8Array(10) [ 30, 60, 0, 0, 0, 0, 0, 0, 0, 0 ];

Если бы мы попытались создать Uint32Array из этого же ArrayBuffer, мы бы получили ошибку.

ts
const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf);
//          ^  RangeError: Длина ArrayBuffer минус byteOffset
//             не кратна размеру элемента

Значение Uint32 требует четыре байта (16 бит). Поскольку ArrayBuffer имеет длину 10 байтов, нет способа чисто разделить его содержимое на 4-байтовые блоки.

Чтобы исправить это, мы можем создать типизированный массив над определённым «срезом» ArrayBuffer. Uint16Array ниже только «представляет» первые 8 байтов базового ArrayBuffer. Для этого мы указываем byteOffset равным 0 и length равным 2, что указывает количество чисел Uint32, которые должен содержать наш массив.

ts
// создать типизированный массив из среза ArrayBuffer
const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf, 0, 2);

/*
  buf    _ _ _ _ _ _ _ _ _ _    10 байт
  arr   [_______,_______]       2 элемента по 4 байта
*/

arr.byteOffset; // 0
arr.length; // 2

Вам не нужно явно создавать экземпляр ArrayBuffer; вы можете напрямую указать длину в конструкторе типизированного массива:

ts
const arr2 = new Uint8Array(5);

// все элементы инициализируются нулями
// => Uint8Array(5) [0, 0, 0, 0, 0]

Типизированные массивы также могут быть созданы напрямую из массива чисел или другого типизированного массива:

ts
// из массива чисел
const arr1 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
arr1[0]; // => 0;
arr1[7]; // => 7;

// из другого типизированного массива
const arr2 = new Uint8Array(arr);

В целом, типизированные массивы предоставляют те же методы, что и обычные массивы, за некоторыми исключениями. Например, push и pop недоступны в типизированных массивах, потому что они потребовали бы изменения размера базового ArrayBuffer.

ts
const arr = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);

// поддерживает распространённые методы массивов
arr.filter(n => n > 128); // Uint8Array(1) [255]
arr.map(n => n * 2); // Uint8Array(8) [0, 2, 4, 6, 8, 10, 12, 14]
arr.reduce((acc, n) => acc + n, 0); // 28
arr.forEach(n => console.log(n)); // 0 1 2 3 4 5 6 7
arr.every(n => n < 10); // true
arr.find(n => n > 5); // 6
arr.includes(5); // true
arr.indexOf(5); // 5

Обратитесь к документации MDN для получения дополнительной информации о свойствах и методах типизированных массивов.

Uint8Array

Стоит особо выделить Uint8Array, так как он представляет классический «байтовый массив» — последовательность 8-битных беззнаковых целых чисел от 0 до 255. Это наиболее распространённый типизированный массив, с которым вы столкнётесь в JavaScript.

В Bun и однажды в других движках JavaScript он имеет методы для преобразования между байтовыми массивами и сериализованными представлениями этих массивов в виде строк base64 или hex.

ts
new Uint8Array([1, 2, 3, 4, 5]).toBase64(); // "AQIDBA=="
Uint8Array.fromBase64("AQIDBA=="); // Uint8Array(4) [1, 2, 3, 4, 5]

new Uint8Array([255, 254, 253, 252, 251]).toHex(); // "fffefdfcfb"
Uint8Array.fromHex("fffefdfcfb"); // Uint8Array(5) [255, 254, 253, 252, 251]

Это возвращаемое значение TextEncoder#encode и тип входных данных TextDecoder#decode — двух классов утилит, предназначенных для перевода строк и различных двоичных кодировок, наиболее notably "utf-8".

ts
const encoder = new TextEncoder();
const bytes = encoder.encode("hello world");
// => Uint8Array(11) [ 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 ]

const decoder = new TextDecoder();
const text = decoder.decode(bytes);
// => hello world

Buffer

Bun реализует Buffer — API Node.js для работы с двоичными данными, который появился до введения типизированных массивов в спецификации JavaScript. С тех пор он был переделан как подкласс Uint8Array. Он предоставляет широкий спектр методов, включая несколько методов, подобных Array и DataView.

ts
const buf = Buffer.from("hello world");
// => Buffer(11) [ 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 ]

buf.length; // => 11
buf[0]; // => 104, ascii для 'h'
buf.writeUInt8(72, 0); // => ascii для 'H'

console.log(buf.toString());
// => Hello world

Полную документацию см. в документации Node.js.

Blob

Blob — это веб-API, обычно используемый для представления файлов. Blob изначально был реализован в браузерах (в отличие от ArrayBuffer, который является частью самого JavaScript), но теперь поддерживается в Node и Bun.

Необычно напрямую создавать экземпляры Blob. Чаще вы будете получать экземпляры Blob из внешнего источника (например, элемента <input type="file"> в браузере) или библиотеки. Тем не менее, можно создать Blob из одной или нескольких строковых или двоичных «частей блоба».

ts
const blob = new Blob(["<html>Hello</html>"], {
  type: "text/html",
});

blob.type; // => text/html
blob.size; // => 19

Эти части могут быть string, ArrayBuffer, TypedArray, DataView или другими экземплярами Blob. Части блоба объединяются в порядке их предоставления.

ts
const blob = new Blob([
  "<html>",
  new Blob(["<body>"]),
  new Uint8Array([104, 101, 108, 108, 111]), // "hello" в двоичном виде
  "</body></html>",
]);

Содержимое Blob может быть асинхронно прочитано в различных форматах.

ts
await blob.text(); // => <html><body>hello</body></html>
await blob.bytes(); // => Uint8Array (копирует содержимое)
await blob.arrayBuffer(); // => ArrayBuffer (копирует содержимое)
await blob.stream(); // => ReadableStream

BunFile

BunFile — это подкласс Blob, используемый для представления лениво загружаемого файла на диске. Как и File, он добавляет свойства name и lastModified. В отличие от File, он не требует загрузки файла в память.

ts
const file = Bun.file("index.txt");
// => BunFile

File

File — это подкласс Blob, который добавляет свойства name и lastModified. Обычно используется в браузере для представления файлов, загруженных через элемент <input type="file">. Node.js и Bun реализуют File.

ts
// в браузере!
// <input type="file" id="file" />

const files = document.getElementById("file").files;
// => File[]
ts
const file = new File(["<html>Hello</html>"], "index.html", {
  type: "text/html",
});

Полную информацию см. в документации MDN.


Потоки

Потоки являются важной абстракцией для работы с двоичными данными без загрузки их всех в память одновременно. Они обычно используются для чтения и записи файлов, отправки и получения сетевых запросов и обработки больших объёмов данных.

Bun реализует веб-API ReadableStream и WritableStream.

NOTE

Bun также реализует модуль `node:stream`, включая [`Readable`](https://nodejs.org/api/stream.html#stream_readable_streams), [`Writable`](https://nodejs.org/api/stream.html#stream_writable_streams) и [`Duplex`](https://nodejs.org/api/stream.html#stream_duplex_and_transform_streams). Полную документацию см. в документации Node.js.

Для создания простого потока чтения:

ts
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("hello");
    controller.enqueue("world");
    controller.close();
  },
});

Содержимое этого потока можно читать по частям с помощью синтаксиса for await.

ts
for await (const chunk of stream) {
  console.log(chunk);
}

// => "hello"
// => "world"

Более полное обсуждение потоков в Bun см. в API > Потоки.


Преобразование

Преобразование из одного двоичного формата в другой — распространённая задача. Этот раздел предназначен как справочник.

Из ArrayBuffer

Поскольку ArrayBuffer хранит данные, лежащие в основе других двоичных структур, таких как TypedArray, приведённые ниже фрагменты не преобразуют из ArrayBuffer в другой формат. Вместо этого они создают новый экземпляр, используя данные, хранящиеся в основе.

В TypedArray

ts
new Uint8Array(buf);

В DataView

ts
new DataView(buf);

В Buffer

ts
// создать Buffer над всем ArrayBuffer
Buffer.from(buf);

// создать Buffer над срезом ArrayBuffer
Buffer.from(buf, 0, 10);

В string

Как UTF-8:

ts
new TextDecoder().decode(buf);

В number[]

ts
Array.from(new Uint8Array(buf));

В Blob

ts
new Blob([buf], { type: "text/plain" });

В ReadableStream

Следующий фрагмент создаёт ReadableStream и помещает весь ArrayBuffer как один чанк.

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(buf);
    controller.close();
  },
});

С чанкованием

Для потоковой передачи ArrayBuffer по частям используйте представление Uint8Array и помещайте каждый чанк.

ts
const view = new Uint8Array(buf);
const chunkSize = 1024;

new ReadableStream({
  start(controller) {
    for (let i = 0; i < view.length; i += chunkSize) {
      controller.enqueue(view.slice(i, i + chunkSize));
    }
    controller.close();
  },
});

Из TypedArray

В ArrayBuffer

Это извлекает базовый ArrayBuffer. Обратите внимание, что TypedArray может быть представлением среза базового буфера, поэтому размеры могут отличаться.

ts
arr.buffer;

В DataView

Для создания DataView над тем же диапазоном байтов, что и TypedArray.

ts
new DataView(arr.buffer, arr.byteOffset, arr.byteLength);

В Buffer

ts
Buffer.from(arr);

В string

Как UTF-8:

ts
new TextDecoder().decode(arr);

В number[]

ts
Array.from(arr);

В Blob

ts
// только если arr является представлением всего своего базового TypedArray
new Blob([arr.buffer], { type: "text/plain" });

В ReadableStream

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(arr);
    controller.close();
  },
});

С чанкованием

Для потоковой передачи ArrayBuffer по частям разделите TypedArray на чанки и помещайте каждый по отдельности.

ts
new ReadableStream({
  start(controller) {
    for (let i = 0; i < arr.length; i += chunkSize) {
      controller.enqueue(arr.slice(i, i + chunkSize));
    }
    controller.close();
  },
});

Из DataView

В ArrayBuffer

ts
view.buffer;

В TypedArray

Работает только если byteLength DataView кратен BYTES_PER_ELEMENT подкласса TypedArray.

ts
new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
new Uint16Array(view.buffer, view.byteOffset, view.byteLength / 2);
new Uint32Array(view.buffer, view.byteOffset, view.byteLength / 4);
// и т.д...

В Buffer

ts
Buffer.from(view.buffer, view.byteOffset, view.byteLength);

В string

Как UTF-8:

ts
new TextDecoder().decode(view);

В number[]

ts
Array.from(view);

В Blob

ts
new Blob([view.buffer], { type: "text/plain" });

В ReadableStream

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(view.buffer);
    controller.close();
  },
});

С чанкованием

Для потоковой передачи ArrayBuffer по частям разделите DataView на чанки и помещайте каждый по отдельности.

ts
new ReadableStream({
  start(controller) {
    for (let i = 0; i < view.byteLength; i += chunkSize) {
      controller.enqueue(view.buffer.slice(i, i + chunkSize));
    }
    controller.close();
  },
});

Из Buffer

В ArrayBuffer

ts
buf.buffer;

В TypedArray

ts
new Uint8Array(buf);

В DataView

ts
new DataView(buf.buffer, buf.byteOffset, buf.byteLength);

В string

Как UTF-8:

ts
buf.toString();

Как base64:

ts
buf.toString("base64");

Как hex:

ts
buf.toString("hex");

В number[]

ts
Array.from(buf);

В Blob

ts
new Blob([buf], { type: "text/plain" });

В ReadableStream

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(buf);
    controller.close();
  },
});

С чанкованием

Для потоковой передачи ArrayBuffer по частям разделите Buffer на чанки и помещайте каждый по отдельности.

ts
new ReadableStream({
  start(controller) {
    for (let i = 0; i < buf.length; i += chunkSize) {
      controller.enqueue(buf.slice(i, i + chunkSize));
    }
    controller.close();
  },
});

Из Blob

В ArrayBuffer

Класс Blob предоставляет удобный метод для этой цели.

ts
await blob.arrayBuffer();

В TypedArray

ts
await blob.bytes();

В DataView

ts
new DataView(await blob.arrayBuffer());

В Buffer

ts
Buffer.from(await blob.arrayBuffer());

В string

Как UTF-8:

ts
await blob.text();

В number[]

ts
Array.from(await blob.bytes());

В ReadableStream

ts
blob.stream();

Из ReadableStream

Обычно используется Response как удобное промежуточное представление для упрощения преобразования ReadableStream в другие форматы.

ts
stream; // ReadableStream

const buffer = new Response(stream).arrayBuffer();

Однако этот подход многословен и добавляет накладные расходы, которые unnecessarily замедляют общую производительность. Bun реализует набор оптимизированных вспомогательных функций для преобразования ReadableStream в различные двоичные форматы.

В ArrayBuffer

ts
// с Response
new Response(stream).arrayBuffer();

// с функцией Bun
Bun.readableStreamToArrayBuffer(stream);

В Uint8Array

ts
// с Response
new Response(stream).bytes();

// с функцией Bun
Bun.readableStreamToBytes(stream);

В TypedArray

ts
// с Response
const buf = await new Response(stream).arrayBuffer();
new Int8Array(buf);

// с функцией Bun
new Int8Array(Bun.readableStreamToArrayBuffer(stream));

В DataView

ts
// с Response
const buf = await new Response(stream).arrayBuffer();
new DataView(buf);

// с функцией Bun
new DataView(Bun.readableStreamToArrayBuffer(stream));

В Buffer

ts
// с Response
const buf = await new Response(stream).arrayBuffer();
Buffer.from(buf);

// с функцией Bun
Buffer.from(Bun.readableStreamToArrayBuffer(stream));

В string

Как UTF-8:

ts
// с Response
await new Response(stream).text();

// с функцией Bun
await Bun.readableStreamToText(stream);

В number[]

ts
// с Response
const arr = await new Response(stream).bytes();
Array.from(arr);

// с функцией Bun
Array.from(new Uint8Array(Bun.readableStreamToArrayBuffer(stream)));

Bun предоставляет утилиту для разрешения ReadableStream в массив его чанков. Каждый чанк может быть строкой, типизированным массивом или ArrayBuffer.

ts
// с функцией Bun
Bun.readableStreamToArray(stream);

В Blob

ts
new Response(stream).blob();

В ReadableStream

Для разделения ReadableStream на два потока, которые могут потребляться независимо:

ts
const [a, b] = stream.tee();

Bun от www.bunjs.com.cn