|
| 1 | +# ArrayBuffer, binary arrays |
| 2 | + |
| 3 | +In web-development we meet binary data mostly while dealing with files (create, upload, download). Another typical use case is image processing. |
| 4 | + |
| 5 | +That's all possible in JavaScript, and binary operations are high-performant. |
| 6 | + |
| 7 | +Although, there's a bit of confusion, because there are many classes. To name a few: |
| 8 | +- `ArrayBuffer`, `Uint8Array`, `DataView`, `Blob`, `File`, etc. |
| 9 | + |
| 10 | +Binary data in JavaScript is implemented in a non-standard way, compared to other languages. But when we sort things out, everything becomes fairly simple. |
| 11 | + |
| 12 | +**The basic binary object is `ArrayBuffer` -- a reference to a fixed-length contiguous memory area.** |
| 13 | + |
| 14 | +We create it like this: |
| 15 | +```js run |
| 16 | +let buffer = new ArrayBuffer(16); // create a buffer of length 16 |
| 17 | +alert(buffer.byteLength); // 16 |
| 18 | +``` |
| 19 | + |
| 20 | +This allocates a contiguous memory area of 16 bytes and pre-fills it with zeroes. |
| 21 | + |
| 22 | +```warn header="`ArrayBuffer` is not an array of something" |
| 23 | +Let's eliminate a possible source of confusion. `ArrayBuffer` has nothing in common with `Array`: |
| 24 | +- It has a fixed length, we can't increase or decrease it. |
| 25 | +- It takes exactly that much space in the memory. |
| 26 | +- To access individual bytes, another "view" object is needed, not `buffer[index]`. |
| 27 | +``` |
| 28 | +
|
| 29 | +`ArrayBuffer` is a memory area. What's stored in it? It has no clue. Just a raw sequence of bytes. |
| 30 | +
|
| 31 | +**To manipulate an `ArrayBuffer`, we need to use a "view" object.** |
| 32 | +
|
| 33 | +A view object does not store anything on it's own. It's the "eyeglasses" that give an interpretation of the bytes stored in the `ArrayBuffer`. |
| 34 | +
|
| 35 | +For instance: |
| 36 | +
|
| 37 | +- **`Uint8Array`** -- treats each byte in `ArrayBuffer` as a separate number, with possible values are from 0 to 255 (a byte is 8-bit, so it can hold only that much). Such value is called a "8-bit unsigned integer". |
| 38 | +- **`Uint16Array`** -- treats every 2 bytes as an integer, with possible values from 0 to 65535. That's called a "16-bit unsigned integer". |
| 39 | +- **`Uint32Array`** -- treats every 4 bytes as an integer, with possible values from 0 to 4294967295. That's called a "32-bit unsigned integer". |
| 40 | +- **`Float64Array`** -- treats every 8 bytes as a floating point number with possible values from <code>5.0x10<sup>-324</sup></code> to <code>1.8x10<sup>308</sup></code>. |
| 41 | +
|
| 42 | +So, the binary data in an `ArrayBuffer` of 16 bytes can be interpreted as 16 "tiny numbers", or 8 bigger numbers (2 bytes each), or 4 even bigger (4 bytes each), or 2 floating-point values with high precision (8 bytes each). |
| 43 | +
|
| 44 | + |
| 45 | +
|
| 46 | +`ArrayBuffer` is the core object, the root of everything, the raw binary data. |
| 47 | +
|
| 48 | +But if we're going to write into it, or iterate over it, basically for almost any operation – we must use a view, e.g: |
| 49 | +
|
| 50 | +```js run |
| 51 | +let buffer = new ArrayBuffer(16); // create a buffer of length 16 |
| 52 | +
|
| 53 | +*!* |
| 54 | +let view = new Uint32Array(buffer); // treat buffer as a sequence of 32-bit integers |
| 55 | +
|
| 56 | +alert(Uint32Array.BYTES_PER_ELEMENT); // 4 bytes per integer |
| 57 | +*/!* |
| 58 | +
|
| 59 | +alert(view.length); // 4, it stores that many integers |
| 60 | +alert(view.byteLength); // 16, the size in bytes |
| 61 | +
|
| 62 | +// let's write a value |
| 63 | +view[0] = 123456; |
| 64 | +
|
| 65 | +// iterate over values |
| 66 | +for(let num of view) { |
| 67 | + alert(num); // 123456, then 0, 0, 0 (4 values total) |
| 68 | +} |
| 69 | +
|
| 70 | +``` |
| 71 | + |
| 72 | +## TypedArray |
| 73 | + |
| 74 | +The common term for all these views (`Uint8Array`, `Uint32Array`, etc) is [TypedArray](https://tc39.github.io/ecma262/#sec-typedarray-objects). They share the same set of methods and properities. |
| 75 | + |
| 76 | +They are much more like regular arrays: have indexes and iterable. |
| 77 | + |
| 78 | + |
| 79 | +A typed array constructor (be it `Int8Array` or `Float64Array`, doesn't matter) behaves differently depending on argument types. |
| 80 | + |
| 81 | +There are 5 variants of arguments: |
| 82 | + |
| 83 | +```js |
| 84 | +new TypedArray(buffer, [byteOffset], [length]); |
| 85 | +new TypedArray(object); |
| 86 | +new TypedArray(typedArray); |
| 87 | +new TypedArray(length); |
| 88 | +new TypedArray(); |
| 89 | +``` |
| 90 | + |
| 91 | +1. If an `ArrayBuffer` argument is supplied, the view is created over it. We used that syntax already. |
| 92 | + |
| 93 | + Optionally we can provide `byteOffset` to start from (0 by default) and the `length` (till the end of the buffer by default), then the view will cover only a part of the `buffer`. |
| 94 | + |
| 95 | +2. If an `Array`, or any array-like object is given, it creates a typed array of the same length and copies the content. |
| 96 | + |
| 97 | + We can use it to pre-fill the array with the data: |
| 98 | + ```js run |
| 99 | + *!* |
| 100 | + let arr = new Uint8Array([0, 1, 2, 3]); |
| 101 | + */!* |
| 102 | + alert( arr.length ); // 4 |
| 103 | + alert( arr[1] ); // 1 |
| 104 | + ``` |
| 105 | +3. If another `TypedArray` is supplied, it does the same: creates a typed array of the same length and copies values. Values are converted to the new type in the process. |
| 106 | + ```js run |
| 107 | + let arr16 = new Uint16Array([1, 1000]); |
| 108 | + *!* |
| 109 | + let arr8 = new Uint8Array(arr16); |
| 110 | + */!* |
| 111 | + alert( arr8[0] ); // 1 |
| 112 | + alert( arr8[1] ); // 232 (tried to copy 1000, but can't fit 1000 into 8 bits) |
| 113 | + ``` |
| 114 | + |
| 115 | +4. For a numeric argument `length` -- creates the typed array to contain that many elements. Its byte length will be `length` multiplied by the number of bytes in a single item `TypedArray.BYTES_PER_ELEMENT`: |
| 116 | + ```js run |
| 117 | + let arr = new Uint16Array(4); // create typed array for 4 integers |
| 118 | + alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 bytes per integer |
| 119 | + alert( arr.byteLength ); // 8 (size in bytes) |
| 120 | + ``` |
| 121 | + |
| 122 | +5. Without arguments, creates an zero-length typed array. |
| 123 | + |
| 124 | +We can create a `TypedArray` directly, without mentioning `ArrayBuffer`. But a view cannot exist without an underlying `ArrayBuffer`, so gets created automatically in all these cases except the first one (when provided). |
| 125 | + |
| 126 | +To access the `ArrayBuffer`, there are properties: |
| 127 | +- `arr.buffer` -- references the `ArrayBuffer`. |
| 128 | +- `arr.byteLength` -- the length of the `ArrayBuffer`. |
| 129 | + |
| 130 | +So, we can always move from one view to another: |
| 131 | +```js |
| 132 | +let arr8 = new Uint8Array([0, 1, 2, 3]); |
| 133 | +
|
| 134 | +// another view on the same data |
| 135 | +let arr16 = new Uint16Array(arr8.buffer); |
| 136 | +``` |
| 137 | + |
| 138 | + |
| 139 | +Here's the list of typed arrays: |
| 140 | +
|
| 141 | +- `Uint8Array`, `Uint16Array`, `Uint32Array` -- for integer numbers of 8, 16 and 32 bits. |
| 142 | + - `Uint8ClampedArray` -- for 8-bit integers, "clamps" them on assignment (see below). |
| 143 | +- `Int8Array`, `Int16Array`, `Int32Array` -- for signed integer numbers (can be negative). |
| 144 | +- `Float32Array`, `Float64Array` -- for signed floating-point numbers of 32 and 64 bits. |
| 145 | +
|
| 146 | +```warn header="No `int8` or similar single-valued types" |
| 147 | +Please note, despite of the names like `Int8Array`, there's no single-value type like `int`, or `int8` in JavaScript. |
| 148 | + |
| 149 | +That's logical, as `Int8Array` is not an array of these individual values, but rather a view on `ArrayBuffer`. |
| 150 | +``` |
| 151 | +
|
| 152 | +### Out-of-bounds behavior |
| 153 | +
|
| 154 | +What if we attempt to write an out-of-bounds value into a typed array? There will be no error. But extra bits are cut-off. |
| 155 | +
|
| 156 | +For instance, let's try to put 256 into `Uint8Array`. In binary form, 256 is `100000000` (9 bits), but `Uint8Array` only provides 8 bits per value, that makes the available range from 0 to 255. |
| 157 | + |
| 158 | +For bigger numbers, only the rightmost (less significant) 8 bits are stored, and the rest is cut off: |
| 159 | + |
| 160 | + |
| 161 | + |
| 162 | +So we'll get zero. |
| 163 | +
|
| 164 | +For 257, the binary form is `100000001` (9 bits), the rightmost 8 get stored, so we'll have `1` in the array: |
| 165 | + |
| 166 | + |
| 167 | + |
| 168 | +In other words, the number modulo 2<sup>8</sup> is saved. |
| 169 | + |
| 170 | +Here's the demo: |
| 171 | +
|
| 172 | +```js run |
| 173 | +let uint8array = new Uint8Array(16); |
| 174 | +
|
| 175 | +let num = 256; |
| 176 | +alert(num.toString(2)); // 100000000 (binary representation) |
| 177 | +
|
| 178 | +uint8array[0] = 256; |
| 179 | +uint8array[1] = 257; |
| 180 | +
|
| 181 | +alert(uint8array[0]); // 0 |
| 182 | +alert(uint8array[1]); // 1 |
| 183 | +``` |
| 184 | +
|
| 185 | +`Uint8ClampedArray` is special in this aspect, its behavior is different. It saves 255 for any number that is greater than 255, and 0 for any negative number. That behavior is useful for image processing. |
| 186 | +
|
| 187 | +## TypedArray methods |
| 188 | +
|
| 189 | +`TypedArray` has regular `Array` methods, with notable exceptions. |
| 190 | +
|
| 191 | +We can iterate, `map`, `slice`, `find`, `reduce` etc. |
| 192 | +
|
| 193 | +There are few things we can't do though: |
| 194 | + |
| 195 | +- No `splice` -- we can't "delete" a value, because typed arrays are views on a buffer, and these are fixed, contiguous areas of memory. All we can do is to assign a zero. |
| 196 | +- No `concat` method. |
| 197 | +
|
| 198 | +There are two additional methods: |
| 199 | +
|
| 200 | +- `arr.set(fromArr, [offset])` copies all elements from `fromArr` to the `arr`, starting at position `offset` (0 by default). |
| 201 | +- `arr.subarray([begin, end])` creates a new view of the same type from `begin` to `end` (exclusive). That's similar to `slice` method (that's also supported), but doesn't copy anything -- just creates a new view, to operate on the given piece of data. |
| 202 | + |
| 203 | +These methods allow us to copy typed arrays, mix them, create new arrays from existing ones, and so on. |
| 204 | + |
| 205 | + |
| 206 | + |
| 207 | +## DataView |
| 208 | + |
| 209 | +[DataView](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) is a special super-flexible "untyped" view over `ArrayBuffer`. It allows to access the data on any offset in any format. |
| 210 | + |
| 211 | +- For typed arrays, the constructor dictates what the format is. The whole array is supposed to be uniform. The i-th number is `arr[i]`. |
| 212 | +- With `DataView` we access the data with methods like `.getUint8(i)` or `.getUint16(i)`. We choose the format at method call time instead of the construction time. |
| 213 | + |
| 214 | +The syntax: |
| 215 | + |
| 216 | +```js |
| 217 | +new DataView(buffer, [byteOffset], [byteLength]) |
| 218 | +``` |
| 219 | + |
| 220 | +- **`buffer`** -- the underlying `ArrayBuffer`. Unlike typed arrays, `DataView` doesn't create a buffer on its own. We need to have it ready. |
| 221 | +- **`byteOffset`** -- the starting byte position of the view (by default 0). |
| 222 | +- **`byteLength`** -- the byte length of the view (by default till the end of `buffer`). |
| 223 | + |
| 224 | +For instance, here we extract numbers in different formats from the same buffer: |
| 225 | + |
| 226 | +```js run |
| 227 | +let buffer = new Uint8Array([255, 255, 255, 255]).buffer; |
| 228 | + |
| 229 | +let dataView = new DataView(buffer); |
| 230 | + |
| 231 | +// get 8-bit number at offset 0 |
| 232 | +alert( dataView.getUint8(0) ); // 255 |
| 233 | + |
| 234 | +// now get 16-bit number at offset 0, that's 2 bytes, both with max value |
| 235 | +alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int) |
| 236 | + |
| 237 | +// get 32-bit number at offset 0 |
| 238 | +alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int) |
| 239 | + |
| 240 | +dataView.setUint32(0, 0); // set 4-byte number to zero |
| 241 | +``` |
| 242 | + |
| 243 | +`DataView` is great when we store mixed-format data in the same buffer. E.g we store a sequence of pairs (16-bit integer, 32-bit float). Then `DataView` allows to access them easily. |
| 244 | + |
| 245 | +## Summary |
| 246 | + |
| 247 | +`ArrayBuffer` is the core object, a reference to the fixed-length contiguous memory area. |
| 248 | + |
| 249 | +To do almost any operation on `ArrayBuffer`, we need a view. |
| 250 | + |
| 251 | +- It can be a `TypedArray`: |
| 252 | + - `Uint8Array`, `Uint16Array`, `Uint32Array` -- for unsigned integers of 8, 16, and 32 bits. |
| 253 | + - `Uint8ClampedArray` -- for 8-bit integers, "clamps" them on assignment. |
| 254 | + - `Int8Array`, `Int16Array`, `Int32Array` -- for signed integer numbers (can be negative). |
| 255 | + - `Float32Array`, `Float64Array` -- for signed floating-point numbers of 32 and 64 bits. |
| 256 | +- Or a `DataView` -- the view that uses methods to specify a format, e.g. `getUint8(offset)`. |
| 257 | + |
| 258 | +In most cases we create and operate directly on typed arrays, leaving `ArrayBuffer` under cover, as a "common discriminator". We can access it as `.buffer` and make another view if needed. |
| 259 | + |
| 260 | +There are also two additional terms: |
| 261 | +- `ArrayBufferView` is an umbrella term for all these kinds of views. |
| 262 | +- `BufferSource` is an umbrella term for `ArrayBuffer` or `ArrayBufferView`. |
| 263 | + |
| 264 | +These are used in descriptions of methods that operate on binary data. `BufferSource` is one of the most common terms, as it means "any kind of binary data" -- an `ArrayBuffer` or a view over it. |
| 265 | + |
| 266 | + |
| 267 | +Here's a cheatsheet: |
| 268 | + |
| 269 | + |
0 commit comments