Решил собрать волю в кулак и прочитать-таки документацию про ArrayBuffer, Uint8Array, DataView и всех их родственников. Оказалось, что типизированные массивы это милая и широко-используемая штука по всему фронту JS/HTML5, и я — последний человек на земле, который ими не пользуется.
Вообще-то, обычные массивы тоже отлично работают — размер устанавливается динамически, положить туда можно всё, что угодно, уйма методов для манипуляций есть прямо из коробки. Но в этом есть и недостаток. Первые разработчики WebGL столкнулись с проблемой, что скормить видеокарте JavaScript массив очень проблематично. Ведь нужно пробежаться по всем элементам, привести их к одному типу, скопировать результат в Си-образный массив и уже потом передать указатель видюхе. Как обходной маневр, они создали скриптовый враппер для Си-шных массивов Float. Через какое-то время это породило целый маленький зоопарк типов, которые мы сейчас и рассмотрим.
Точка входа — ArrayBuffer. В принципе, это просто readonly указатель на бинарный буфер в памяти. Читать напрямую из него нельзя. Писать тоже. Зато можно посмотреть размер в байтах и сделать копию.
| 
					 1 2 3 4  | 
						var buf1 = new ArrayBuffer(16),     buf2 = buf1.slice(); console.log(buf1.byteLength, buf2.byteLength); //16, 16  | 
					
В этом месте внимательный читатель может задаться вопросом «нафига?». Терпение, мой дорогой товарищ.
Для того, чтобы читать и писать в буфер, нужно создать view, который будет знать, данные какого типа лежат в буфере. Под каждый числовой тип есть свой view. Целых девять штук. Говоря типизированный массив, мы обычно имеем ввиду именно их:
- Uint8Array
 - Int8Array
 - Uint8ClampedArray
 - Uint16Array
 - Int16Array
 - Uint32Array
 - Int32Array
 - Float32Array
 - Float64Array
 
Про ClampedArray будет чуть позже. Все типизированные массивы могут принимать на вход буфер или какой-то его кусок:
| 
					 1 2 3 4  | 
						var buffer = new ArrayBuffer(4),     u16 = new Uint16Array(buffer); u16[0] = 0xFF;  | 
					
Что здорово, на один и тот же буфер можно натравить несколько вьюшек, и читать одну и ту же область памяти как, например, массив байт и массив int32 одновременно. Эту фишку можно применить так: есть у нас, например, растр в памяти — последовательность Red, Green, Blue & Alpha компонент цвета каждой точки какой-то картинки. Мы создадим Uint8Array и Uint32Array и сможем читать и писать цвета как по компонентам, так и сразу RGBA точку целиком. Записать int32 будет быстрее, чем четыре int8.
Раз уж пошла речь о растре, Uint8ClampedArray используется canvas для хранения растра текущей картинки. Честно-честно. Если вызвать, например,
canvasContext.getImageData(0, 0, 100, 100).data , то вернется ClampedArray. Он отличается от Uint8Array только тем, что при записи в него значений больше 255, он будет обрезать их до 255. Если меньше нуля — ставить 0. Очень удобно для манипуляций с RGB. А Uint8Array просто отбросит старшие байты записываемого числа, и результат может быть чем угодно в пределах 0-255.
| 
					 1 2 3 4 5  | 
						var uint8 = new Uint8Array(buffer1); var clamped = new Uint8ClampedArray(buffer2); uint8[0] = 0x0100; //256; сохранит 0 clamped[0] = 0x100; //256; сохранит 255  | 
					
Другой способ работать с буфером — DataView. Он позволяет читать int8-32, uint8-32, float32-64 по заданному смещению. Это удобно, когда мы работаем с ArrayBuffer, в котором смешанные типы данных. Например, когда мы читаем заголовок файла, в котором тип файла может храниться в первом байте, потом идет uint32 контрольной суммы, потом, например, пары широта/долгота GPS координат в float32 до конца файла. Не создавать же типизированный массив для каждого типа поля. А с DataView все получится проще:
| 
					 1 2 3 4 5 6  | 
						var view = new DataView(fileBuffer); var type = view.getUint8(0); //offset 0 var crc = view.getUint32(1); //offset 1 var latitude0 = view.getFloat32(5); //offset 5 var longitude0 = view.getFloat32(9);  | 
					
Все get/set методы могут взять дополнительный опциональный параметр — little-endian. Он определяет порядок байт в памяти у чисел, которые больше чем один байт. По умолчанию, DataView использует big-endian — от старшего байта к младшему. Процессоры x86, на секунду, используют little-endian. Типизированные массивы используют правила текущей архитектуры, то есть тоже little-endian. То есть можно увидеть такую картину:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13  | 
						var buffer = new ArrayBuffer(2),     view = new DataView(buffer),     word = new Uint16Array(buffer),     bytes = new Uint8Array(buffer),     testValue = 0xFF00; word[0] = testValue; //2-byte value bytes[0] === view.getUint8(0); // true  bytes[1] === view.getUint8(1); // true word[0] === view.getUint16(0); // > false word[0] === view.getUint16(0, true); // > true  | 
					
Побайтовое сравнение проходит. Мультибайтовое — нет, до тех пор, пока я явно не укажу, что в буфере данные лежат в little-endian формате.
Зачем вообще нужны типизированные массивы. Они быстрые. Они удобны для работы с сырыми двоичными данными. Так как типизированный массив лежит как есть в памяти, то его можно без потерь и колдовства передавать в нативные браузерные расширения.
Ну и напоследок, кто умеет работать с ArrayBuffer. Во-первых, это XMLHttpRequest (level 2), WebSocket и fetch() — для отправки и получения бинарных сообщений. Canvas для работы с растром, WebGL для буферов (текстур, вершин). Web Audio API тоже понимает массивы. Самая любопытная и, кажется, недооцененная возможность — передача (Transfer) буфера через postMessage между окном и WebWorker (либо два WebWorker). В этом случае буфер не клонируется, как обычно происходит в postMessage, а просто меняет владельца. Для предыдущего владельца ссылка становится невалидной. Можно, например, в воркерах собирать тяжелые картинки и по готовности отдавать родительскому окну. Скорость «передачи» данных при этом будет просто огромной.