ArrayBuffer, бинарные массивы

Работа с бинарными данными в JavaScript реализована нестандартно по сравнению с другими языками программирования.

Базовый объект для работы с бинарными данными имеет тип ArrayBuffer и представляет собой ссылку на непрерывную область памяти фиксированной длины.

let buffer = new ArrayBuffer(16); // создаётся буфер длиной 16 байт
alert(buffer.byteLength); // 16

Инструкция выше выделяет непрерывную область памяти размером 16 байт и заполняет её нулями.

ArrayBuffer – это не массив!

ArrayBuffer не имеет ничего общего с Array:

ArrayBuffer – это область памяти. Что там хранится? Этой информации нет. Просто необработанный («сырой») массив байтов.

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

Такие объекты не хранят какое-то собственное содержимое. Они интерпретируют бинарные данные, хранящиеся в ArrayBuffer.

Например:

Таким образом, бинарные данные из ArrayBuffer размером 16 байт могут быть представлены как 16 чисел маленькой разрядности или как 8 чисел большей разрядности (по 2 байта каждое), или как 4 числа ещё большей разрядности (по 4 байта каждое), или как 2 числа с плавающей точкой высокой точности (по 8 байт каждое).

Но если мы собираемся что-то записать в него или пройтись по его содержимому, да и вообще для любых действий мы должны использовать какой-то объект-представление («view»), например:

let buffer = new ArrayBuffer(16);   // создаётся буфер длиной 16 байт

let view = new Uint32Array(buffer); // интерпретируем содержимое как последовательность 32-битных целых чисел без знака


let s = Uint32Array.BYTES_PER_ELEMENT; // 4 байта на каждое целое число

s += '\n' + view.length;               // 4, именно столько чисел сейчас хранится в буфере
s += '\n' + view.byteLength + '\n';    // 16, размер содержимого в байтах

// давайте запишем какое-нибудь значение
view[0] = 123456;

// теперь пройдёмся по всем значениям
for(let num of view) {
  s += num+'  '; // 123456, потом 0, 0, 0 (всего 4 значения)
}
alert( s );
TypedArray

Общий термин для всех таких представлений (Uint8Array, Uint32Array и т.д.) – это TypedArray, типизированный массив. У них имеется набор одинаковых свойств и методов.

Они уже намного больше напоминают обычные массивы: элементы проиндексированы, и возможно осуществить обход содержимого.

Конструкторы типизированных массивов (будь то Int8Array или Float64Array, без разницы) ведут себя по-разному в зависимости от типа передаваемого им аргумента.

Синтаксис

new TypedArray(length);
new TypedArray(typedArray);
new TypedArray(object);
new TypedArray(buffer [, byteOffset [, length]]);
new TypedArray();
TypedArray() это одно из следующих значений:

Параметры

length
При вызове в памяти создаётся буфер длины length * BYTES_PER_ELEMENT байт, содержащий нули
let arr = new Uint16Array(4); // создаём типизированный массив для 4 целых 16-битных чисел без знака
let s = Uint16Array.BYTES_PER_ELEMENT;  // 2 байта на число
alert ( s +'\n' + arr.byteLength );     // 8 (размер массива в байтах)
typedArray
Когда вызывается с аргументом typedArray, который может быть объектом любого из типов типизированных массивов (например, Int32Array), тогда переданный массив typedArray копируется в новый массив. Каждое значение из typedArray конвертируется в соответствующий конструктору тип прямо перед копированием. Длина нового объекта typedArray будет такой же как и длина переданного в параметре typedArray
let arr16 = new Uint16Array([1, 1000]);
let arr8 = new Uint8Array(arr16);
let s = arr8[0];         // 1
alert( s+'\n'+arr8[1] ); // 232, потому что 1000 не помещается в 8 бит
object
Новый массив создаётся так, как если бы была вызвана функция TypedArray.from()
let arr = new Uint8Array([0, 1, 2, 3]);
let s = arr.length;         // 4, создан бинарный массив той же длины
alert (s + '\n' + arr[1] ); // 1, заполнен 4-мя байтами с указанными значениями
buffer, byteOffset, length

Когда происходит вызов с параметрами buffer и опциональными параметрами byteOffset и length, то будет создан новый типизированный массив, который будет отражать buffer типа ArrayBuffer. Параметры byteOffset и length определяют диапазон (размер) памяти, выводимый данным массивоподобным представлением. Если оба этих параметра (byteOffset и length) опущены, то будет использован весь буфер buffer; если опущен только length, то будет выведен весь остаток буфера после смещения начала отсчёта элементов, заданного параметром byteOffset.

При вызове без аргументов будет создан пустой типизированный массив.

Свойства

Для доступа к ArrayBuffer в TypedArray есть следующие свойства:

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

let arr8 = new Uint8Array([0, 1, 2, 3]);

// другое представление на тех же данных
let arr16 = new Uint16Array(arr8.buffer);

Методы TypedArray

Типизированные массивы TypedArray, за некоторыми заметными исключениями, имеют те же методы, что и массивы Array.

Мы можем обходить их, вызывать map, slice, find, reduce и т.д.

Однако, есть некоторые вещи, которые нельзя осуществить:

Но зато есть два дополнительных метода:

arr.set ( fromArr, [offset] )

копирует все элементы из fromArr в arr, начиная с позиции offset (0 по умолчанию).
arr.subarray ( [begin, end] )

создаёт новое представление того же типа для данных, начиная с позиции begin до end (не включая). Это похоже на метод slice (который также поддерживается), но при этом ничего не копируется – просто создаётся новое представление, чтобы совершать какие-то операции над указанными данными.

Эти методы позволяют нам копировать типизированные массивы, смешивать их, создавать новые на основе существующих и т.д.

DataView

DataView – это специальное супергибкое нетипизированное представление данных из ArrayBuffer. Оно позволяет обращаться к данным на любой позиции и в любом формате.

Синтаксис:

new DataView(buffer, [byteOffset], [byteLength])

Параметры

buffer
ссылка на бинарные данные ArrayBuffer. В отличие от типизированных массивов, DataView не создаёт буфер автоматически. Нам нужно заранее подготовить его самим.
byteOffset
начальная позиция данных для представления (по умолчанию 0).
byteLength
длина данных (в байтах), используемых в представлении (по умолчанию – до конца buffer).

Например, извлечение числа в разных форматах из одного и того же буфера двоичных данных:

// бинарный массив из 4х байт, каждый имеет максимальное значение 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;

let dataView = new DataView(buffer);

// получим 8-битное число на позиции 0
let s = dataView.getUint8(0); // 255

// а сейчас мы получим 16-битное число на той же позиции 0, оно состоит из 2-х байт, вместе составляющих число 65535
s += '\n' + dataView.getUint16(0); // 65535 (максимальное 16-битное беззнаковое целое)

// получим 32-битное число на позиции 0
alert( s + '\n' + dataView.getUint32(0) ); // 4294967295 (максимальное 32-битное беззнаковое целое)

dataView.setUint32(0, 0); // при установке 4-байтового числа в 0, во все его 4 байта будут записаны нули

Представление DataView отлично подходит, когда мы храним данные разного формата в одном буфере. Например, мы храним последовательность пар, первое значение пары 16-битное целое, а второе – 32-битное с плавающей точкой. DataView позволяет легко получить доступ к обоим.