У веб-розробці ми маємо справу з бінарними даними переважно при роботі з файлами (створення, вивантаження та завантаження). Іншим частим випадком є обробка зображень.
Все це є можливим в JavaScript, ба більше, бінарні операції ще й високопродуктивні.
Хоча й велика кількість різних класів може спантеличити. Деякі з них:
ArrayBuffer
,Uint8Array
,DataView
,Blob
,File
тощо.
Бінарні дані в JavaScript реалізовано не так, як в інших мовах програмування. Але, якщо трошки розібратися, все виявиться досить простим.
Базовим об’єктом для роботи з бінарними даними є ArrayBuffer
– посилання на неперервну область пам’яті фіксованої довжини.
Масив створюється наступним чином:
let buffer = new ArrayBuffer(16); // створити буфер з довжиною 16
alert(buffer.byteLength); // 16
Це виділяє неперервну область пам’яті з довжиною 16 байт та заповнює нулями.
ArrayBuffer
не є масивомПозбудьмося можливого джерела непорозумінь. ArrayBuffer
не має нічого спільного з Array
:
- Він має фіксовану довжину, що не може бути збільшена чи зменшена.
- Він займає саме стільки місця, скільки виділено при створенні.
- Для доступу до окремих байтів нам знадобиться окремий об’єкт представлення,
buffer[index]
не спрацює.
ArrayBuffer
– область пам’яті. Що там зберігається? Просто послідовність байтів, що можна інтерпретувати як нам потрібно.
Для роботи з ArrayBuffer
нам потрібен спеціальний об’єкт “представлення”.
Власне об’єкт представлення не зберігає ніяких даних. Це “вікно”, що надає певну інтерпретацію “сирих” байтів всередині ArrayBuffer
.
Наприклад:
Uint8Array
– представляє кожен байт вArrayBuffer
окремим числом із областю значень від 0 до 255 (байт складається з 8 біт, тому тільки такі значення можливі). Такі числа називаються “8-бітові беззнакові цілі числа”.Uint16Array
– представляє кожні 2 байти цілим числом з областю значень від 0 до 65535. Має назву “16-бітові беззнакові цілі числа”.Uint32Array
– представляє кожні 4 байти цілим числом з областю значень від 0 до 4294967295. Має назву “32-бітові беззнакові цілі числа”.Float64Array
– представляє кожні 8 байт числом з плаваючою комою з областю значень від5.0x10-324
до1.8x10308
.
Отже, бінарні дані в 16 байтному ArrayBuffer
можна представити як 16 “коротких чисел” або 8 більших чисел (2 байти кожне), або 4 ще більших (4 байти кожне), або 2 числа з плаваючою комою високої точності (8 байти кожне).
ArrayBuffer
– головний об’єкт представлення даних, що є простою послідовністю байтів.
Якщо нам знадобиться щось туди записати, перебрати їх або для будь-якої іншої операції – нам знадобиться об’єкт представлення:
let buffer = new ArrayBuffer(16); // створення буферу з довжиною 16
let view = new Uint32Array(buffer); // представлення буферу послідовністю 32-бітових цілих чисел
alert(Uint32Array.BYTES_PER_ELEMENT); // 4 байти на кожне число
alert(view.length); // 4, стільки чисел вміщує буфер
alert(view.byteLength); // 16, розмір в байтах
// запишемо туди значення
view[0] = 123456;
// переберемо всі значення
for(let num of view) {
alert(num); // 123456, потім 0, 0, 0 (всього 4 значення)
}
TypedArray
Спільним терміном для опису об’єктів представлень (Uint8Array
, Uint32Array
тощо) є TypedArray. Всі вони мають однаковий набір методів та властивостей.
Зверніть увагу, не існує конструктору з іменем TypedArray
, це просто термін, що використовується для опису представлень ArrayBuffer
: Int8Array
, Uint8Array
і так далі.
Коли ви бачите щось на кшталт new TypedArray
– це означає будь-що з new Int8Array
, new Uint8Array
тощо.
Типізовані масиви поводять себе як звичайні масиви: мають індекси та можуть перебиратися.
Конструктори типізованих масивів (Int8Array
чи Float64Array
, це неважливо) поводять себе по-різному в залежності від типу аргументів.
Існує 5 варіантів сигнатур конструктору:
new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
-
Якщо передається аргумент типу
ArrayBuffer
, то об’єкт представлення створюється для нього. Ми вже використовувати такий синтаксис.Необов’язкові аргументи:
byteOffset
вибору зміщення від початку (типове значення 0) таlength
(типове значення відповідає кінцю) – дозволяють працювати з частиною даних зbuffer
. -
Якщо передати
Array
чи будь-який об’єкт схожий на масив – це створить типізований масив такої ж довжини і з копією вмісту.Ми можемо використовувати це для заповнення масиву даними:
let arr = new Uint8Array([0, 1, 2, 3]); alert( arr.length ); // 4, створюється бінарний масив такої ж довжини alert( arr[1] ); // 1, складається з 4 байтів (8-бітові беззнакові цілі числа) із заданими значеннями
-
Якщо передано інший
TypedArray
– це спрацює таким же чином: буде створено новий типізований масив такої ж довжини та копією значень. Значення конвертуються в новий тип в процесі, якщо необхідно.let arr16 = new Uint16Array([1, 1000]); let arr8 = new Uint8Array(arr16); alert( arr8[0] ); // 1 alert( arr8[1] ); // 232, спроба скопіювати 1000, але 8 біт не можуть вмістити число 1000 (пояснення нижче)
-
З числовим аргументом
length
буде створено типізований масив із відповідним числом елементів. Його довжиною в байтах будеlength
помноженим на кількість байтів в одному елементіTypedArray.BYTES_PER_ELEMENT
:let arr = new Uint16Array(4); // створить типізований масив для 4 цілих чисел alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 байт на число alert( arr.byteLength ); // 8 (розмір в байтах)
-
Без аргументів буде створено типізований масив нульової довжини.
TypedArray
можна створити безпосередньо, без використання ArrayBuffer
. Але об’єкт представлення не може існувати без базового ArrayBuffer
, тому його буде створено автоматично в усіх випадках окрім першого (коли ArrayBuffer
передано безпосередньо).
Для доступу до внутрішнього ArrayBuffer
TypedArray
має властивості:
buffer
– посилання наArrayBuffer
.byteLength
– довжинаArrayBuffer
.
Отже, ми можемо завжди змінити об’єкт представлення на інший:
let arr8 = new Uint8Array([0, 1, 2, 3]);
// інше представлення однакових даних
let arr16 = new Uint16Array(arr8.buffer);
Список типізованих масивів:
Uint8Array
,Uint16Array
,Uint32Array
– для цілих беззнакових чисел з довжиною 8, 16 або 32 біт.Uint8ClampedArray
– для 8-бітових беззнакових цілих чисел, що “обрізаються” при присвоєнні (пояснення нижче).
Int8Array
,Int16Array
,Int32Array
– для цілих чисел зі знаком (можуть мати від’ємні значення).Float32Array
,Float64Array
– для чисел з плаваючою комою зі знаком довжиною 32 або 64 біти.
int8
або подібних типів для значеньЗверніть увагу, попри імена на кшталт Int8Array
, в JavaScript не існує значень з типами int
або int8
.
Це логічно, оскільки, Int8Array
не є масивом окремих значення, а всього-на-всього представленням ArrayBuffer
.
Вихід за область допустимих значень
Що буде у випадку спроби записати значення, що не вміщується в область допустимих значень? Це не призведе до помилки, але зайві біти значення буде відкинуто.
Наприклад, запишемо 256 в Uint8Array
. В бінарному форматі 256 це 100000000
(9 біт), але Uint8Array
дозволяє тільки 8 біт для одного значення, тобто значення від 0 до 255.
Для більших чисел тільки праві (найменш значущі) 8 біт буде збережено, а решту буде обрізано.
Тому ми отримаємо нуль.
257 в бінарному форматі буде 100000001
(9 біт), праві 8 біт буде збережено, тому значення в масиві буде 1:
Інакше кажучи, буде збережено тільки число за модулем 28.
Приклад:
let uint8array = new Uint8Array(16);
let num = 256;
alert(num.toString(2)); // 100000000 (бінарна форма)
uint8array[0] = 256;
uint8array[1] = 257;
alert(uint8array[0]); // 0
alert(uint8array[1]); // 1
Uint8ClampedArray
є особливим в цьому сенсі, його поведінка відрізняється. Буде збережено 255 для усіх чисел, що більше ніж 255 та 0 для від’ємних чисел. Така поведінка буде в нагоді при обробці зображень.
Методи TypedArray
Методи TypedArray
в цілому збігаються з методами звичайного Array
, але є деякі відмінності.
Ми можемо його перебирати з використанням map
, slice
, find
, reduce
тощо.
Але є речі, що ми не можемо зробити:
- Немає методу
splice
– ми не можемо “видалити” значення. В основі типізованих масивів лежить буфер, що є неперервною областю пам’яті фіксованої довжини, а типізовані масиви є всього-на-всього їх представленням. Ми можемо тільки присвоїти нульове значення. - Немає методу
concat
.
Існує 2 додаткових методи:
arr.set(fromArr, [offset])
копіює всі елементи, що починаються зoffset
(типово з 0) зfromArr
вarr
.arr.subarray([begin, end])
створює нове представлення такого ж типу починаючи зbegin
доend
(не включно). Це схоже на методslice
(що також підтримується), але значення не буде скопійовано – тільки створюється нове представлення для роботи з тими самими даними.
Ці методи дають змогу копіювати типізовані масиви, змішувати їх, створювати нові ґрунтуючись на попередніх і так далі.
DataView
DataView спеціальне надзвичайно гнучке “не типізоване” представлення ArrayBuffer
. Це дозволяє отримати доступ до даних з будь-яким зміщенням та в будь-якому форматі.
- Для типізованого масиву конструктор визначає формат даних. Увесь масив повинен складатися зі значень одного типу. Доступ до i-го елементу виконується за допомогою
arr[i]
. - З
DataView
доступ до даних відбувається за допомогою методів на кшталт.getUint8(i)
чи.getUint16(i)
. Тепер можна обирати формат даних під час виклику методу, а не конструктора.
Синтаксис:
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
alert( dataView.getUint8(0) ); // 255
// тепер отримати 16-бітове число за зміщенням 0, воно складається з 2 байт, що разом представляється як 65535
alert( dataView.getUint16(0) ); // 65535 (найбільше 16-бітове беззнакове ціле число)
// отримати 32-бітове число за зміщенням 0
alert( dataView.getUint32(0) ); // 4294967295 (найбільше 32-бітове беззнакове ціле число)
dataView.setUint32(0, 0); // встановити 4-байтове число в нуль, тобто запити всі байти як 0
DataView
зручне для використання, коли ми зберігаємо дані різного формату в одному буфері. Наприклад, коли ми зберігаємо послідовність пар (16-бітове ціле число, 32-бітове число з плаваючою комою), DataView
дозволяє легко отримати до них доступ.
Підсумки
ArrayBuffer
– головний об’єкт, що є посиланням на неперервну область пам’яті фіксованої довжини.
Для більшості операцій з ArrayBuffer
нам потрібне представлення.
- Це може бути
TypedArray
:Uint8Array
,Uint16Array
,Uint32Array
– для беззнакових цілих чисел довжиною 8, 16 та 32 біти.Uint8ClampedArray
– для 8-бітових цілих чисел, при присвоєнні відбувається “обрізання” значень.Int8Array
,Int16Array
,Int32Array
– для цілих чисел зі знаком (можуть бути від’ємними).Float32Array
,Float64Array
– для чисел з плаваючою комою зі знаком довжиною 32 та 64 біти.
- Чи
DataView
– представлення, яке дозволяє вибрати формат даних за допомогою методів якgetUint8(offset)
.
У більшості випадків ми маємо справу безпосередньо з типізованими масивами, а ArrayBuffer
залишається прихованим. Якщо необхідно, можливо отримати доступ до буферу за допомогою .buffer
та створити нове представлення.
Також для опису методів, що дозволяють працювати з бінарними даними існує два додаткових терміни:
ArrayBufferView
– загальна назва представлень всіх типів.BufferSource
– термін, що означаєArrayBuffer
абоArrayBufferView
.
Ці терміни також будуть використані в наступній частині. BufferSource
часто використовується для позначення “будь-яких бінарних даних” – ArrayBuffer
чи його представлення.
Ось підказка: