ArrayBuffer и Typed Arrays

ArrayBuffer и Typed Arrays

Технический фундамент бинарного представления

При работе с бинарными данными нужно разделять три уровня:

  • ArrayBuffer как сырая область памяти в байтах;
  • TypedArray как типизированное представление этой памяти;
  • DataView как низкоуровневый доступ по смещениям и типам.

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

Почему эта тема нужна в JavaScript

Обычно в JS ты работаешь со строками, объектами и обычными массивами. Но для файлов, аудио, изображений, сетевых пакетов и Web API часто нужны бинарные данные. Для этого есть ArrayBuffer и типизированные массивы (Typed Arrays).

Ключевой момент: ArrayBuffer это "сырой" блок памяти, а TypedArray это удобный способ читать и писать в этот блок как числа конкретного типа.

Проверь себя: почему для изображения в байтах неудобно использовать обычный массив number[]?

ArrayBuffer: контейнер памяти

ArrayBuffer задает размер буфера в байтах, но сам по себе не дает методов чтения/записи по индексам.

const buffer = new ArrayBuffer(8);
console.log(buffer.byteLength); // 8

Смотри, что важно: это только "память", без интерпретации данных.

TypedArray: представление данных

Типизированные массивы дают доступ к буферу как к массиву фиксированного числового типа.

const buffer = new ArrayBuffer(8);
const bytes = new Uint8Array(buffer);

bytes[0] = 255;
bytes[1] = 10;

console.log(bytes[0]); // 255
console.log(bytes.length); // 8

Популярные типы:

  • Uint8Array - беззнаковые 8-битные числа (0..255);
  • Int16Array - знаковые 16-битные;
  • Float32Array - числа с плавающей точкой.

Один буфер, несколько представлений

Можно создать несколько представлений над одним буфером.

const buffer = new ArrayBuffer(4);
const bytes = new Uint8Array(buffer);
const words = new Uint16Array(buffer);

bytes[0] = 1;
bytes[1] = 0;

console.log(words[0]); // зависит от порядка байтов, обычно 1

Здесь часто путаются: разные представления читают те же байты по-разному.

Проверь себя: почему изменение в bytes сразу отражается в words?

DataView для гибкого чтения

DataView позволяет вручную читать/писать разные типы по смещениям.

const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);

view.setUint16(0, 500);
console.log(view.getUint16(0)); // 500

Смотри, что важно: DataView работает в байтовых смещениях. В протоколах ты часто заранее фиксируешь layout: что лежит в байте 0, что в байте 1, где начинается Uint16 и т.д.

// header: version (1 byte), type (1 byte), length (Uint16)
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);

view.setUint8(0, 2); // version
view.setUint8(1, 9); // type
view.setUint16(2, 1024); // length

const version = view.getUint8(0);
const type = view.getUint8(1);
const length = view.getUint16(2);
console.log(version, type, length);

Смотри, что важно: у setUint16/getUint16 есть параметр littleEndian. Если перепутать порядок байтов при записи/чтении, числа будут "ломаться".

const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);

view.setUint16(0, 500, true); // little-endian
console.log(view.getUint16(0, true)); // 500
console.log(view.getUint16(0, false)); // 62465 (прочитали в другом порядке)

Это полезно при работе с бинарными протоколами, где структура данных строго определена по байтам.

Реальные микро-сценарии

  1. Работа с fetch и бинарным ответом (arrayBuffer()).
  2. Обработка аудио-данных в браузере.
  3. Парсинг бинарного формата файла (например, заголовки изображений).
async function loadBinary(url) {
  const response = await fetch(url);
  const buffer = await response.arrayBuffer();
  return new Uint8Array(buffer);
}

Дополнительный пример: выделение части буфера через subarray.

const bytes = new Uint8Array([10, 20, 30, 40, 50]);
const header = bytes.subarray(0, 2);
const payload = bytes.subarray(2);

console.log(header); // Uint8Array [10, 20]
console.log(payload); // Uint8Array [30, 40, 50]

Смотри, что важно: subarray(...) не копирует данные, а создает "окно" в том же буфере. Если изменить header[0], изменится и bytes[0]. Если нужен независимый кусок, используй slice(...) у typed array.

const bytes = new Uint8Array([10, 20, 30]);
const headView = bytes.subarray(0, 2);
const headCopy = bytes.slice(0, 2);

headView[0] = 99;
console.log(bytes[0]); // 99 (общая память)
console.log(headCopy[0]); // 10 (копия)

Дополнительный пример: восстановление текста из ASCII-байтов.

const codes = new Uint8Array([72, 73, 33]);
let text = '';
for (const code of codes) text += String.fromCharCode(code);
console.log(text); // HI!

Частые ошибки новичков

  • Путать размер в байтах и количество элементов.
  • Ожидать, что ArrayBuffer работает как обычный массив.
  • Игнорировать диапазоны типов (Uint8 не хранит -1 как в number).
  • Смешивать разные представления без понимания формата.

Анти-провал: сначала зафиксируй, какой именно формат данных ты читаешь (тип числа, длина, смещение), и только потом выбирай TypedArray.

Что будет, если изменить входные данные

Если в Uint8Array записать значение 300, оно будет приведено по модулю диапазона (получится 44). Это edge case, который часто удивляет новичков. Значит, перед записью стоит проверять диапазон, если важна точность.

Проверь себя: какой тип выбрать для хранения значения -20 - Uint8Array или Int16Array?

Краткий итог

  • ArrayBuffer хранит сырые байты, а TypedArray дает удобный доступ к ним.
  • Типизированные массивы важны для бинарных данных, медиа и низкоуровневых API.
  • Разные представления над одним буфером видят одни и те же байты.
  • DataView нужен для точного чтения/записи по смещению.
  • При работе с бинарными структурами критичны диапазоны типов и формат данных.