ArrayBuffer, binary arrays

Most of the time you meet binary data in the browser when files are involved: uploading, downloading, saving, generating. Image processing is another big one — every pixel is a handful of bytes, and you sometimes need to touch them directly.

JavaScript can do all of this, and it does it fast. The raw byte operations run close to the metal.

The part that trips people up is the sheer number of classes involved. ArrayBuffer, Uint8Array, DataView, Blob, File, and a few more. They interlock in a way that feels unusual if you’re coming from a language with a plain byte[]. Once you see how they split responsibilities, though, the model is small and consistent.

Here’s the one sentence to anchor everything: ArrayBuffer is a reference to a fixed-length, contiguous block of memory. Nothing more. It’s the raw bytes.

You create one by asking for a size:

let buffer = new ArrayBuffer(16); // a buffer of 16 bytes
alert(buffer.byteLength); // 16

That reserves 16 bytes of contiguous memory and zeroes them out.

So an ArrayBuffer is a region of memory, and it has no opinion about what those bytes mean. Are they numbers? Text? A pixel? The buffer doesn’t know and doesn’t care. It’s a flat sequence of bytes.

To actually do anything with a buffer, you lay a view over it.

A view stores no data of its own. Think of it as a pair of glasses: put on the Uint8Array glasses and the same 16 bytes look like 16 small numbers; swap to Uint32Array glasses and they look like 4 larger numbers. The bytes never change — only the interpretation does.

Here are four common views and how each one carves up the bytes:

  • Uint8Array — reads each single byte as its own number, 0 to 255. A byte is 8 bits, so 256 distinct values. This is an “8-bit unsigned integer”.
  • Uint16Array — groups every 2 bytes into one number, 0 to 65535. A “16-bit unsigned integer”.
  • Uint32Array — groups every 4 bytes into one number, 0 to 4294967295. A “32-bit unsigned integer”.
  • Float64Array — groups every 8 bytes into one floating-point number, ranging from about 5.0×10-324 up to 1.8×10308.

That means a single 16-byte buffer can be seen as 16 tiny numbers, or 8 medium ones (2 bytes each), or 4 larger ones (4 bytes each), or 2 high-precision floats (8 bytes each). Same memory, different grouping.

ArrayBuffer(16) — 16 raw bytes
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
Uint8Array — 16 numbers, 1 byte each
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
Uint16Array — 8 numbers, 2 bytes each
··
··
··
··
··
··
··
··
Uint32Array — 4 numbers, 4 bytes each
····
····
····
····
Float64Array — 2 numbers, 8 bytes each
········
········
One 16-byte ArrayBuffer, viewed through four different typed arrays. The bytes are identical; only the grouping changes.

ArrayBuffer is the root object — the raw binary data itself. But to write into it, read from it, or loop over it, you go through a view:

let buffer = new ArrayBuffer(16); // a buffer of 16 bytes

let view = new Uint32Array(buffer); // read the buffer as 32-bit integers

alert(Uint32Array.BYTES_PER_ELEMENT); // 4 bytes per integer

alert(view.length); // 4, it holds that many integers
alert(view.byteLength); // 16, the size in bytes

// write a value
view[0] = 500000;

// iterate over the values
for(let num of view) {
  alert(num); // 500000, then 0, 0, 0 (4 values total)
}

TypedArray

The umbrella name for all these views — Uint8Array, Uint32Array, and the rest — is TypedArray. They all share one set of methods and properties.

There’s a subtlety here: TypedArray is not something you can call. There’s no new TypedArray(). It’s just a shared term for the family. When docs say new TypedArray(...), read it as “any one of new Int8Array(...), new Uint8Array(...), and so on.” The full roster comes shortly.

In day-to-day use, typed arrays feel like ordinary arrays: numeric indexes, iterable with for..of, and most of the usual methods.

The constructor is where it gets interesting. A typed array constructor — pick any, Int8Array or Float64Array, the rule is the same — behaves differently depending on what you pass it. There are five forms:

new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
new TypedArray(buffer, offset, length)
→ view over an existing buffer
new TypedArray(array-like)
→ new buffer, copies the values in
new TypedArray(typedArray)
→ new buffer, copies + converts values
new TypedArray(length)
→ new zeroed buffer for N elements
new TypedArray()
→ empty, zero-length
Five ways to construct a typed array. Only the first one reuses an existing buffer; the rest allocate a fresh one.
  1. Pass an ArrayBuffer and you get a view over it — the form we’ve already used. You can add byteOffset to start partway in (0 by default) and length to stop early (runs to the end of the buffer by default), so the view can cover just a slice of the buffer.

  2. Pass an Array or any array-like object, and you get a typed array of the same length with the values copied in. Handy for pre-filling:

    let arr = new Uint8Array([0, 1, 2, 3]);
    alert( arr.length ); // 4, a binary array of the same length
    alert( arr[1] ); // 1, filled with those four 8-bit values
  3. Pass another TypedArray and it behaves the same way — new array, same length, values copied. If the element types differ, the values get converted as they’re copied:

    let arr16 = new Uint16Array([7, 900]);
    let arr8 = new Uint8Array(arr16);
    alert( arr8[0] ); // 7
    alert( arr8[1] ); // 132 — 900 doesn't fit in 8 bits (explained below)
  4. Pass a numeric length and you get a typed array with room for that many elements. Its byte size is length times the per-element size, TypedArray.BYTES_PER_ELEMENT:

    let arr = new Uint16Array(4); // room for 4 integers
    alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 bytes per integer
    alert( arr.byteLength ); // 8 (size in bytes)
  5. Pass nothing and you get an empty, zero-length typed array.

Notice that only the first form mentions an ArrayBuffer. In every other case a view still needs a buffer underneath — a view can’t exist without one — so the buffer is created for you automatically.

Whenever you have a typed array, you can reach the buffer beneath it through two properties:

  • buffer — the underlying ArrayBuffer.
  • byteLength — its length in bytes.

Because the buffer is exposed, you can hop from one view to another over the same bytes:

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

// a second view onto the same underlying data
let arr16 = new Uint16Array(arr8.buffer);

Edit the four raw bytes below. A Uint8Array sees them as four separate numbers; a Uint32Array laid over the same buffer folds all four into one. Change any byte and the combined value updates instantly — because there is only one block of memory underneath.

interactiveTwo views over one buffer

The full set of typed arrays:

  • Uint8Array, Uint16Array, Uint32Array — unsigned integers of 8, 16, and 32 bits.
    • Uint8ClampedArray — 8-bit integers that “clamp” on assignment (see below).
  • Int8Array, Int16Array, Int32Array — signed integers, so they can be negative.
  • Float32Array, Float64Array — signed floating-point numbers of 32 and 64 bits.

What happens when a value doesn’t fit

Store a number that’s too big for the slot, and there’s no error thrown. The extra high bits are simply dropped.

Take Uint8Array, which gives each value 8 bits (range 0–255). Try to store 256. In binary that’s 100000000 — nine bits. Only the low 8 bits are kept, the leading 1 falls off, and you’re left with 00000000, which is 0.

256 = 1 00000000 (9 bits)
1
→ dropped
kept — the low 8 bits:
0
0
0
0
0
0
0
0
result: 0
Storing 256 in a Uint8Array: the 9th bit has nowhere to go, so it's discarded and you get 0.

For 257 the binary is 100000001. Again the leading bit drops, leaving 00000001 — so you get 1:

257 = 1 00000001 (9 bits)
1
→ dropped
kept — the low 8 bits:
0
0
0
0
0
0
0
1
result: 1
Storing 257: same discard, but the low bits are 00000001, giving 1.

Put formally: the stored value is the original number modulo 28. A quick demo:

let uint8array = new Uint8Array(16);

let num = 256;
alert(num.toString(2)); // 100000000 (binary form)

uint8array[0] = 256;
uint8array[1] = 257;

alert(uint8array[0]); // 0
alert(uint8array[1]); // 1

Uint8ClampedArray is the exception to this wraparound rule. Instead of chopping bits, it clamps: anything above 255 becomes 255, anything below 0 becomes 0, and fractional values round to the nearest integer. That matters for image processing — when you brighten a pixel past pure white, you want it pinned at white, not wrapped around to black.

Try it yourself. Type any number and watch the same assignment land in two different slots — one that wraps around modulo 256, one that clamps to the 0–255 range.

interactiveWrap vs. clamp on assignment

TypedArray methods

A typed array carries the usual Array methods, with a couple of gaps.

You can iterate it, and call map, slice, find, reduce, and friends — they work as you’d expect.

What you can’t do comes straight from the fixed-length nature of the underlying buffer:

  • No splice. You can’t remove an element, because the array is a window onto a fixed, contiguous block of memory. The most you can do is overwrite a slot with zero.
  • No concat.

In their place, typed arrays add two methods of their own:

  • arr.set(fromArr, [offset]) — copies every element of fromArr into arr, starting at index offset (0 by default).
  • arr.subarray([begin, end]) — returns a new typed array of the same type covering the range [begin, end). It looks like slice (which is also available), but with a key difference: it copies nothing. It’s just a fresh view onto the same bytes, letting you work with a slice of the data in place.

Between set and subarray, you have what you need to copy typed arrays, splice pieces together into a new buffer, and carve existing data into windows.

DataView

DataView is a deliberately “untyped”, maximally flexible view over an ArrayBuffer. It lets you read or write at any byte offset in any format you name at that moment.

The contrast with typed arrays is the whole point:

  • A typed array fixes the format in its constructor. The array is uniform — every element is the same type — and element i lives at arr[i].
  • A DataView fixes nothing up front. You call .getUint8(i) or .getUint16(i) and choose the format per call, not per view.

The syntax:

new DataView(buffer, [byteOffset], [byteLength])
  • buffer — the underlying ArrayBuffer. Unlike a typed array, DataView never allocates a buffer for you; you must supply one.
  • byteOffset — where the view starts (0 by default).
  • byteLength — how many bytes it covers (to the end of buffer by default).

Here’s the same four bytes read at three different widths:

// 4 bytes, each set to the maximum 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;

let dataView = new DataView(buffer);

// one 8-bit number at offset 0
alert( dataView.getUint8(0) ); // 255

// one 16-bit number at offset 0 — 2 bytes together read as 65535
alert( dataView.getUint16(0) ); // 65535 (largest 16-bit unsigned int)

// one 32-bit number at offset 0
alert( dataView.getUint32(0) ); // 4294967295 (largest 32-bit unsigned int)

dataView.setUint32(0, 0); // write a 4-byte zero, clearing all four bytes
buffer: [255, 255, 255, 255]
255
255
255
255
getUint8(0)
→ 255
getUint16(0)
→ 65535
getUint32(0)
→ 4294967295
One 4-byte buffer, read three ways through a DataView. Each getter chooses how many bytes to fold together.

DataView shines when a single buffer holds mixed formats. Say the buffer is a stream of pairs — a 16-bit integer followed by a 32-bit float, over and over. A typed array can’t express that (it insists on one uniform type), but a DataView reads each field at its own offset with the right getter.

The demo below makes both ideas concrete. Edit the four bytes, then read them at three widths through a single DataView. Flip the little-endian switch and watch the multi-byte reads flip their byte order — while getUint8 stays put, because a single byte has no order to speak of.

interactiveOne DataView, three widths, either endianness

Summary

ArrayBuffer is the core object: a reference to a fixed-length, contiguous block of memory.

To do almost anything with an ArrayBuffer, you need a view over it.

  • It can be a TypedArray:
    • Uint8Array, Uint16Array, Uint32Array — unsigned integers of 8, 16, and 32 bits.
    • Uint8ClampedArray — 8-bit integers that clamp on assignment.
    • Int8Array, Int16Array, Int32Array — signed integers, negatives allowed.
    • Float32Array, Float64Array — signed floats of 32 and 64 bits.
  • Or a DataView — a view whose methods name the format per call, like getUint8(offset).

Most of the time you create and work directly on typed arrays, and the ArrayBuffer stays out of sight as the shared foundation. When you need it, it’s there as .buffer, ready for you to lay another view on top.

Two more terms show up in the documentation of methods that take binary data:

  • ArrayBufferView — an umbrella for all the view kinds (any typed array or a DataView).
  • BufferSource — an umbrella for an ArrayBuffer or an ArrayBufferView.

You’ll meet these in later chapters. BufferSource is one of the most common, because it’s shorthand for “any kind of binary data” — a raw buffer or any view over one.

BufferSource = ArrayBuffer or any view
ArrayBuffer
ArrayBufferView
TypedArray
Uint8Array, Int16Array,
Float64Array, …
DataView
The vocabulary, at a glance: ArrayBuffer at the root, views on top, and the two umbrella terms that group them.