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 about5.0×10-324up to1.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 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();
-
Pass an
ArrayBufferand you get a view over it — the form we’ve already used. You can addbyteOffsetto start partway in (0 by default) andlengthto stop early (runs to the end of the buffer by default), so the view can cover just a slice of the buffer. -
Pass an
Arrayor 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 -
Pass another
TypedArrayand 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) -
Pass a numeric
lengthand you get a typed array with room for that many elements. Its byte size islengthtimes 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) -
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 underlyingArrayBuffer.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.
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.
For 257 the binary is 100000001. Again the leading bit drops, leaving 00000001 — so you get 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.
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 offromArrintoarr, starting at indexoffset(0 by default).arr.subarray([begin, end])— returns a new typed array of the same type covering the range[begin, end). It looks likeslice(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
ilives atarr[i]. - A
DataViewfixes 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 underlyingArrayBuffer. Unlike a typed array,DataViewnever 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 ofbufferby 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
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.
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, likegetUint8(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 aDataView).BufferSource— an umbrella for anArrayBufferor anArrayBufferView.
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.
Float64Array, …