Пошук уроків, статей та іншого контенту
Освоїте ArrayBuffer, TypedArray і DataView для компактної роботи з двійковими даними та пам’яттю.
У JavaScript звичайний Array зберігає числа як значення високої точності, а кожен елемент може мати додаткові накладні витрати. Для роботи з мережевими пакетами, файлами, аудіо, зображеннями, WebAssembly або великими масивами чисел це не завжди ефективно.
Двійкові API JavaScript розділяють:
пам’ять — ArrayBuffer;
спосіб інтерпретації пам’яті — TypedArray;
низькорівневе читання та запис із контролем порядку байтів — DataView.
Ці об’єкти не зберігають числа як окремі JavaScript-об’єкти. Вони працюють із послідовністю байтів.
ArrayBuffer: сирий блок пам’ятіArrayBuffer описує фіксований блок пам’яті.
const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16Число 16 означає кількість байтів, а не кількість елементів певного типу. Сам по собі ArrayBuffer не надає зручного способу прочитати число або рядок:
console.log(buffer[0]); // undefinedЩоб інтерпретувати байти, потрібно створити представлення — typed array або DataView.
Типові розміри числових типів:
| Тип | Розмір | |---|---:| | Int8Array, Uint8Array, Uint8ClampedArray | 1 байт | | Int16Array, Uint16Array | 2 байти | | Int32Array, Uint32Array, Float32Array | 4 байти | | Float64Array, BigInt64Array, BigUint64Array | 8 байт |
У коді розмір доступний через статичну властивість:
console.log(Uint32Array.BYTES_PER_ELEMENT); // 4
console.log(Float64Array.BYTES_PER_ELEMENT); // 8Назва TypedArray часто використовується як узагальнення. Це не конкретний конструктор, який зазвичай створюють напряму. До typed arrays належать:
Int8Array;
Uint8Array;
Uint8ClampedArray;
Int16Array;
Uint16Array;
Int32Array;
Uint32Array;
Float32Array;
Float64Array;
BigInt64Array;
BigUint64Array.
Масив можна створити за кількістю елементів:
const values = new Int16Array(4);
values[0] = -100;
values[1] = 200;
console.log(values); // Int16Array(4) [ -100, 200, 0, 0 ]Або з JavaScript-масиву:
const numbers = new Uint8Array([10, 20, 30]);
console.log(numbers.length); // 3
console.log(numbers.byteLength); // 3У цьому випадку створюється власний буфер, а значення копіюються.
ArrayBufferconst buffer = new ArrayBuffer(8);
const values = new Uint16Array(buffer);
values[0] = 500;
values[1] = 1000;
console.log(values.length); // 4
console.log(values.byteLength); // 8Uint16Array має елементи по 2 байти, тому з буфера на 8 байтів утворюються 4 елементи.
Властивості представлення:
console.log(values.buffer === buffer); // true
console.log(values.byteOffset); // 0
console.log(values.byteLength); // 8
console.log(values.length); // 4buffer — базовий ArrayBuffer;
byteOffset — початкова позиція представлення в байтах;
byteLength — кількість байтів представлення;
length — кількість елементів.
Кілька typed arrays можуть посилатися на один і той самий буфер:
const buffer = new ArrayBuffer(4);
const bytes = new Uint8Array(buffer);
const numbers = new Uint32Array(buffer);
bytes[0] = 1;
console.log(numbers[0]);Результат залежить від порядку байтів платформи. Саме тому не варто використовувати typed array для протоколів, у яких порядок байтів заданий явно. Для таких випадків краще застосовувати DataView.
Зміни через одне представлення одразу видно через інше, адже копіювання буфера не відбувається.
subarray() і slice()subarray() створює нове представлення тієї самої пам’яті:
const source = new Uint8Array([10, 20, 30, 40]);
const part = source.subarray(1, 3);
part[0] = 99;
console.log(source); // Uint8Array(4) [ 10, 99, 30, 40 ]slice() створює копію:
const source = new Uint8Array([10, 20, 30, 40]);
const copy = source.slice(1, 3);
copy[0] = 99;
console.log(source); // Uint8Array(4) [ 10, 20, 30, 40 ]
console.log(copy); // Uint8Array(2) [ 99, 30 ]Практичне правило:
subarray() — коли потрібне швидке представлення частини тих самих даних;
slice() — коли потрібна незалежна копія.
Typed arrays примусово перетворюють значення до свого типу. Наприклад, Uint8Array зберігає лише цілі числа від 0 до 255:
const values = new Uint8Array(4);
values[0] = 300;
values[1] = -1;
values[2] = 12.9;
values[3] = NaN;
console.log(values); // Uint8Array(4) [ 44, 255, 12, 0 ]Для беззнакового 8-бітного типу:
300 циклічно перетворюється на 44;
-1 перетворюється на 255;
дробова частина відкидається;
NaN стає 0.
Uint8ClampedArray поводиться інакше: значення обмежуються діапазоном 0..255, а не переходять через переповнення.
const pixels = new Uint8ClampedArray(3);
pixels[0] = 300;
pixels[1] = -10;
pixels[2] = 127.8;
console.log(pixels); // Uint8ClampedArray(3) [ 255, 0, 128 ]Такий тип часто використовують для значень кольорових каналів.
Знакові та беззнакові typed arrays зберігають число в обмеженій кількості бітів:
const values = new Int8Array(3);
values[0] = 127;
values[1] = 128;
values[2] = -129;
console.log(values); // Int8Array(3) [ 127, -128, 127 ]Int8Array має діапазон від -128 до 127. Значення поза цим діапазоном обгортаються за правилами двійкового представлення.
Для великих цілих чисел існують:
const values = new BigInt64Array(2);
values[0] = 9_000_000_000_000_000_000n;
values[1] = -1n;
console.log(values[0]);BigInt64Array і BigUint64Array працюють із BigInt, тому літерали мають суфікс n. Змішувати Number і BigInt в арифметичних операціях не можна:
const a = 10n;
const b = 2;
// a + b; // TypeError
const result = a + BigInt(b);
console.log(result); // 12nDataViewDataView надає методи для читання та запису різних типів у довільних позиціях буфера:
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setUint16(0, 500);
view.setInt32(2, -1000);
view.setFloat32(6, 1.5);
console.log(view.getUint16(0));
console.log(view.getInt32(2));
console.log(view.getFloat32(6));На відміну від typed arrays, DataView:
не має індексації на кшталт view[0];
дозволяє читати різні типи в одному буфері;
дозволяє явно вказувати порядок байтів;
може працювати з позиціями, які не кратні розміру типу.
Конструктор має такий вигляд:
const view = new DataView(buffer, byteOffset, byteLength);byteOffset і byteLength необов’язкові. Вони обмежують частину буфера, з якою працює представлення.
Число, яке займає кілька байтів, може зберігатися двома способами:
big-endian — старший байт зберігається першим;
little-endian — молодший байт зберігається першим.
Наприклад, число 0x1234 у двох байтах:
big-endian: 12 34
little-endian: 34 12Методи DataView для 16-, 32- і 64-бітних типів мають аргумент littleEndian.
const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
view.setUint16(0, 0x1234, true);
console.log([...new Uint8Array(buffer)]); // [52, 18]
console.log(view.getUint16(0, true)); // 4660
console.log(view.getUint16(0, false)); // 13330true означає little-endian, а false — big-endian. Якщо аргумент не передано, DataView використовує big-endian.
У протоколах порядок байтів має бути частиною контракту. Не можна записувати в одному місці з true, а читати в іншому місці, покладаючись на значення за замовчуванням.
Розглянемо пакет такого формату:
байти 0–1 magic: Uint16, little-endian
байт 2 version: Uint8
байт 3 flags: Uint8
байти 4–7 payloadLength: Uint32, little-endian
байти 8–11 timestamp: Uint32, little-endian
байти 12–... payloadРеалізація кодування та декодування:
const HEADER_SIZE = 12;
const MAGIC = 0x4a53;
function encodePacket({ version, flags, timestamp, payload }) {
if (!Number.isInteger(version) || version < 0 || version > 255) {
throw new RangeError("version має бути в діапазоні 0..255");
}
if (!Number.isInteger(flags) || flags < 0 || flags > 255) {
throw new RangeError("flags має бути в діапазоні 0..255");
}
if (!Number.isInteger(timestamp) || timestamp < 0 || timestamp > 0xffffffff) {
throw new RangeError("timestamp має бути беззнаковим 32-бітним числом");
}
const payloadBytes = new TextEncoder().encode(payload);
const buffer = new ArrayBuffer(HEADER_SIZE + payloadBytes.byteLength);
const view = new DataView(buffer);
// Записуємо заголовок у форматі little-endian.
view.setUint16(0, MAGIC, true);
view.setUint8(2, version);
view.setUint8(3, flags);
view.setUint32(4, payloadBytes.byteLength, true);
view.setUint32(8, timestamp, true);
// Копіюємо UTF-8-байти після заголовка.
new Uint8Array(buffer, HEADER_SIZE).set(payloadBytes);
return buffer;
}
function decodePacket(buffer) {
if (!(buffer instanceof ArrayBuffer)) {
throw new TypeError("Очікується ArrayBuffer");
}
if (buffer.byteLength < HEADER_SIZE) {
throw new RangeError("Буфер коротший за заголовок");
}
const view = new DataView(buffer);
const magic = view.getUint16(0, true);
if (magic !== MAGIC) {
throw new Error("Невірний magic number");
}
const payloadLength = view.getUint32(4, true);
const expectedSize = HEADER_SIZE + payloadLength;
if (buffer.byteLength !== expectedSize) {
throw new Error("Розмір payload не відповідає заголовку");
}
const payloadBytes = new Uint8Array(buffer, HEADER_SIZE, payloadLength);
return {
version: view.getUint8(2),
flags: view.getUint8(3),
timestamp: view.getUint32(8, true),
payloadLength,
payload: new TextDecoder().decode(payloadBytes)
};
}
const packet = encodePacket({
version: 1,
flags: 0b00000010,
timestamp: 1_700_000_000,
payload: "Привіт, binary world!"
});
console.log(decodePacket(packet));Цей підхід корисний, коли потрібно:
зменшити розмір повідомлення;
мати чіткий формат пакета;
читати тільки потрібні поля без перетворення всього буфера;
обмінюватися даними між різними мовами або системами.
TextEncoder перетворює рядок на UTF-8, а TextDecoder виконує зворотну операцію. Сам payload у прикладі залишається послідовністю байтів, а не JavaScript-рядком.
Typed array може представляти лише частину великого буфера:
const buffer = new ArrayBuffer(16);
const header = new Uint32Array(buffer, 0, 2);
const body = new Uint8Array(buffer, 8, 8);
header[0] = 100;
header[1] = 200;
body.set([1, 2, 3, 4]);
console.log(header.byteOffset); // 0
console.log(header.byteLength); // 8
console.log(body.byteOffset); // 8
console.log(body.byteLength); // 8Для typed array зміщення має бути кратним розміру одного елемента:
const buffer = new ArrayBuffer(8);
// Коректно: 2 кратне 2.
const values = new Uint16Array(buffer, 2, 2);Некоректне зміщення призведе до RangeError:
// new Uint32Array(buffer, 2); // RangeErrorDataView не має такої вимоги:
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer, 1, 4);
view.setUint32(0, 123, true);Це важливо під час розбору щільно упакованих форматів, де поле може починатися з будь-якого байта.
Бінарні дані часто надходять як:
ArrayBuffer;
Uint8Array;
Blob;
File;
результат читання потоку;
буфер із API криптографії або WebAssembly.
Передача типізованого масиву у функцію не завжди означає передачу саме його частини. Якщо typed array є вікном у більший буфер, властивість buffer може містити зайві байти:
const buffer = new ArrayBuffer(10);
const bytes = new Uint8Array(buffer, 2, 4);
console.log(bytes.byteLength); // 4
console.log(bytes.buffer.byteLength); // 10Якщо API очікує рівно діапазон bytes, потрібно врахувати byteOffset і byteLength:
const exactBytes = bytes.slice();
console.log(exactBytes.byteLength); // 4Або передати явно:
const exactView = new Uint8Array(
bytes.buffer,
bytes.byteOffset,
bytes.byteLength
);exactView і bytes посилаються на ту саму пам’ять, а slice() створює копію.
WebAssembly експортує свою лінійну пам’ять як WebAssembly.Memory. Її buffer можна використовувати через typed arrays:
const memory = new WebAssembly.Memory({ initial: 1 });
const bytes = new Uint8Array(memory.buffer);
bytes[0] = 42;
console.log(bytes[0]); // 42Якщо пам’ять розширюється через memory.grow(), її буфер може бути замінений. Старе представлення не слід безумовно використовувати після розширення:
const memory = new WebAssembly.Memory({ initial: 1 });
let bytes = new Uint8Array(memory.buffer);
memory.grow(1);
// Створюємо представлення заново після зміни розміру пам’яті.
bytes = new Uint8Array(memory.buffer);Це типова причина помилок у коді, який кешує typed array над пам’яттю WebAssembly.
SharedArrayBuffer і атомарні операціїSharedArrayBuffer схожий на ArrayBuffer, але його пам’ять може бути спільною для потоків або Web Workers. Для синхронізації використовують Atomics.
const shared = new SharedArrayBuffer(4);
const counter = new Int32Array(shared);
Atomics.store(counter, 0, 10);
Atomics.add(counter, 0, 5);
console.log(Atomics.load(counter, 0)); // 15Звичайне читання та запис спільної пам’яті не замінюють синхронізацію. Якщо кілька потоків одночасно змінюють значення, застосовуйте відповідні операції Atomics.
Atomics працює з integer typed arrays, які підтримують атомарні операції, зокрема Int32Array і BigInt64Array. Float32Array та Float64Array для цього не використовуються.
У браузерному середовищі доступність спільної пам’яті також залежить від політики безпеки документа та налаштування ізоляції контексту.
Методи DataView перевіряють межі буфера:
const view = new DataView(new ArrayBuffer(2));
view.setUint16(0, 1000);
// view.getUint32(0); // RangeErrorTyped arrays під час доступу за індексом не кидають помилку для звичайного виходу за межі:
const values = new Uint8Array(2);
values[10] = 1;
console.log(values[10]); // undefinedТому під час розбору зовнішніх даних потрібно самостійно перевіряти:
мінімальний розмір заголовка;
значення довжин;
допустимі версії та прапорці;
відповідність заявленої довжини фактичній;
відсутність переповнення під час обчислення позицій.
Зовнішній двійковий буфер потрібно вважати ненадійним незалежно від того, чи прийшов він із мережі, файлу або іншого процесу.
Вибір типу залежить від формату даних:
Uint8Array — універсальна послідовність байтів;
Int8Array — знакові однобайтові значення;
Uint16Array або Uint32Array — однорідні цілочислові масиви;
Float32Array — компактні числа з плаваючою крапкою;
Float64Array — більша точність;
DataView — неоднорідний формат із полями різних типів;
BigInt64Array і BigUint64Array — 64-бітні цілі числа, що виходять за безпечний діапазон Number.
Для великих однорідних масивів typed arrays зазвичай простіші та компактніші за звичайні масиви. Але не слід створювати копії без потреби: subarray() та вікна через byteOffset дають змогу обробляти частину буфера без додаткового виділення пам’яті.
const values = new Uint32Array(4);
console.log(values.length); // 4
console.log(values.byteLength); // 16Аргумент new Uint32Array(4) означає чотири елементи, а не чотири байти.
Дані, записані як little-endian, потрібно читати з таким самим параметром:
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, 0x12345678, true);
console.log(view.getUint32(0, true)); // 305419896Неправильний порядок байтів не обов’язково спричинить помилку — код просто отримає інше число.
subarray() копієюconst original = new Uint8Array([1, 2, 3]);
const part = original.subarray(0, 2);
part[0] = 99;
console.log(original[0]); // 99Для незалежної пам’яті використовуйте slice() або створюйте новий typed array.
buffer замість вікнаЯкщо typed array починається не з нульового байта, typedArray.buffer може містити дані до та після нього. Для точного діапазону враховуйте byteOffset і byteLength.
Number і BigIntconst value = new BigInt64Array([10n]);
// value[0] + 1; // TypeError
console.log(value[0] + 1n); // 11nNumber для довільного 64-бітного цілогоNumber безпечно представляє цілі числа лише до Number.MAX_SAFE_INTEGER. Для точних 64-бітних значень використовуйте BigInt64Array, BigUint64Array або відповідні методи DataView із BigInt.
ArrayBuffer — це сирий блок байтів без типу даних.
Typed arrays задають однорідну інтерпретацію буфера.
DataView підходить для складних двійкових форматів із полями різного розміру.
byteLength вимірюється в байтах, а length — в елементах.
subarray() створює вікно спільної пам’яті, а slice() — копію.
Для протоколів потрібно явно визначати та послідовно використовувати порядок байтів.
Uint8Array є зручним універсальним представленням байтів.
BigInt64Array і BigUint64Array призначені для точних 64-бітних цілих значень.
Під час розбору зовнішніх даних необхідно перевіряти межі та заявлені довжини.
SharedArrayBuffer разом з Atomics призначені для контрольованого обміну даними між потоками.