One Block of Memory, Multiple Interpretations: JavaScript Binary Arrays Explained
Binary arrays are JavaScript's interface for operating on binary data, consisting of the ArrayBuffer object, TypedArray views, and DataView views. They all handle binary data using array syntax.
Note that binary arrays are not true arrays, but array-like objects.
ArrayBuffer Object
The ArrayBuffer object represents a block of binary data in memory, which can be manipulated through "views." Views implement the array interface, allowing you to operate on memory using array methods.
It is itself just a memory region and cannot directly read or write memory data; it requires a TypedArray view or a DataView view.
Views
The ArrayBuffer object, as a memory region, can hold multiple types of data. Different interpretations of the same memory segment are called "views." ArrayBuffer has two types of views: TypedArray views and DataView views.
TypedArray View
A TypedArray view is an array view for reading and writing ArrayBuffer binary data using a fixed data type (e.g., Int8, Uint16, Float32, etc.).
There are 12 types in the current standard, with Uint8Array, Int16Array, and Float32Array being the most common in front-end development.
| Type | Element Size | Description | Range |
|---|---|---|---|
Int8Array |
1 byte | 8-bit signed integer | -128 ~ 127 |
Uint8Array |
1 byte | 8-bit unsigned integer | 0 ~ 255 |
Uint8ClampedArray |
1 byte | 8-bit unsigned int (clamped) | 0 ~ 255 |
Int16Array |
2 bytes | 16-bit signed integer | -32768 ~ 32767 |
Uint16Array |
2 bytes | 16-bit unsigned integer | 0 ~ 65535 |
Int32Array |
4 bytes | 32-bit signed integer | -2147483648 ~ 2147483647 |
Uint32Array |
4 bytes | 32-bit unsigned integer | 0 ~ 4294967295 |
Float16Array |
2 bytes | 16-bit floating point | -65504 ~ 65504 |
Float32Array |
4 bytes | 32-bit floating point | IEEE 754 |
Float64Array |
8 bytes | 64-bit floating point | IEEE 754 |
BigInt64Array |
8 bytes | 64-bit signed BigInt | -2^63 ~ 2^63-1 |
BigUint64Array |
8 bytes | 64-bit unsigned BigInt | 0 ~ 2^64-1 |
Float16Array, Float32Array, and Float64Array are all signed; there are no corresponding unsigned types.
Creating a TypedArray
A TypedArray can be created by specifying a length, from a regular array, from an ArrayBuffer, or from another TypedArray. Creating one based on an ArrayBuffer is the most common and efficient way to handle binary data in front-end development.
Specifying a length to create a TypedArray syntax:
new TypedArray(length);
For example:
const arr = new Uint8Array(5);
console.log(arr);
Output:
Uint8Array(5)[(0, 0, 0, 0, 0)];
Using a regular array to create a TypedArray syntax:
new TypedArray(array);
For example:
const arr = new Uint8Array([10, 20, 30]);
console.log(arr);
Output:
Uint8Array(3)[(10, 20, 30)];
Based on an ArrayBuffer to create a TypedArray syntax:
new TypedArray(buffer, byteOffset, length);
For example:
const buffer = new ArrayBuffer(8);
const arr = new Uint8Array(buffer);
console.log(arr.length); // 8
console.log(arr.byteLength); // 8
If you need to start reading from a specific position:
const buffer = new ArrayBuffer(16);
// Start from the 4th byte, read 4 elements
const arr = new Uint8Array(buffer, 4, 4);
Where:
buffer: underlying memory4: starting offset (unit: bytes)4: number of elements (not bytes)
This is the most common creation method when parsing binary files (like WAV, PNG).
Using another TypedArray to create a TypedArray syntax:
new TypedArray(typedArray);
For example:
const arr1 = new Uint8Array([1, 2, 3]);
const arr2 = new Uint8Array(arr1);
console.log(arr2);
Output:
Uint8Array(3)[(1, 2, 3)];
Note:
This copies the data; arr1 and arr2 do not share memory.
Comparison of the 4 ways to create a TypedArray:
| Creation Method | Example | Creates a new ArrayBuffer? |
|---|---|---|
| Specify length | new Uint8Array(8) |
Yes |
| Regular array | new Uint8Array([1,2,3]) |
Yes |
| ArrayBuffer | new Uint8Array(buffer) |
No, shares existing memory |
| TypedArray | new Uint8Array(other) |
Yes (copies data) |
Common TypedArray APIs
The TypedArray API is very similar to a regular Array, but it is specifically designed for operating on binary data.
TypedArray provides a set of APIs for creating, reading, modifying, iterating, copying, and finding binary data.
Properties
| Property | Purpose |
|---|---|
buffer |
The corresponding ArrayBuffer |
byteLength |
The number of bytes occupied by the current view |
byteOffset |
The starting offset (in bytes) of the current view |
length |
Number of elements |
BYTES_PER_ELEMENT |
Number of bytes per element (static property) |
Example:
const arr = new Uint16Array(10); // Has 10 elements, so length is 10
console.log(arr.length); // 10
console.log(arr.byteLength); // 20
console.log(arr.byteOffset); // 0
console.log(arr.buffer); // ArrayBuffer
console.log(Uint16Array.BYTES_PER_ELEMENT); // 2
In the code above, Uint16Array is a 16-bit unsigned integer, so 1 element occupies 2 bytes (1 byte = 8 bits).
Therefore, BYTES_PER_ELEMENT is 2.
Because there are 10 elements, byteLength is 10 times 2, which is 20 bytes.
Reading and Writing Elements
Like regular arrays, elements can be accessed via subscript.
const arr = new Int16Array(3);
arr[0] = 100;
arr[1] = -50;
console.log(arr[0]); // 100
Creation and Copying
set()copies another set of data, i.e., completely copies a segment of content to another memory segment.
const arr = new Uint8Array(5);
arr.set([1, 2, 3]);
console.log(arr);
Output:
Uint8Array(5)[(1, 2, 3, 0, 0)];
You can also specify a starting position:
arr.set([8, 9], 3);
Result:
[1, 2, 3, 8, 9];
subarray()creates a new view sharing the same block of memory.
const arr = new Uint8Array([1, 2, 3, 4]);
const sub = arr.subarray(1, 3);
console.log(sub);
Output:
Uint8Array[(2, 3)];
subarray()does not copy data.
slice()copies data and returns a newTypedArray.
const copy = arr.slice(1, 3);
Result:
Uint8Array[(2, 3)];
The difference from subarray():
slice(): copies datasubarray(): shares memory
Iteration
for...of iteration
for (const value of arr) {
console.log(value);
}
forEach() iteration
arr.forEach((value, index) => {
console.log(index, value);
});
entries()
for (const [i, v] of arr.entries()) {
console.log(i, v);
}
keys()
for (const key of arr.keys()) {
console.log(key);
}
values()
for (const value of arr.values()) {
console.log(value);
}
Searching
Search methods from regular arrays can all be used on TypedArray.
arr.includes(10);
arr.indexOf(10);
arr.lastIndexOf(10);
arr.find((v) => v > 100);
arr.findIndex((v) => v > 100);
Conversion
Conversion methods from regular arrays also apply to TypedArray.
arr.join(",");
arr.toString();
Array.from(arr);
[...arr];
For example:
const arr = new Uint8Array([1, 2, 3]);
console.log(Array.from(arr));
Output:
[1, 2, 3];
Iteration Methods
Iteration methods from regular arrays also apply to TypedArray.
arr.map(...)
arr.filter(...)
arr.reduce(...)
arr.reduceRight(...)
arr.some(...)
arr.every(...)
For example:
const arr = new Uint8Array([1, 2, 3]);
const result = arr.map((x) => x * 2);
console.log(result);
Output:
Uint8Array[(2, 4, 6)];
Sorting
The sorting methods for TypedArray are the same as those for regular arrays.
arr.sort();
arr.reverse();
For example:
const arr = new Uint8Array([3, 1, 2]);
arr.sort();
console.log(arr);
Output:
Uint8Array[(1, 2, 3)];
Filling
Filling methods from regular arrays also apply to TypedArray.
arr.fill(100);
Output:
[100, 100, 100];
Checking
Checking methods from regular arrays also apply to TypedArray.
arr.includes(10);
Returns:
true;
false;
Differences from Regular Array
TypedArray supports many of the same methods as Array, but has some limitations:
- Element type is fixed, e.g.,
Uint8Arraycan only store integers from 0 to 255. - Length is fixed, methods that change length like
push(),pop(),shift(),unshift(),splice()cannot be used after creation.
For example:
const arr = new Uint8Array([1, 2, 3]);
arr.push(4); // ❌ TypeError
This is because the underlying ArrayBuffer of a TypedArray has a fixed size and cannot dynamically expand like a regular array.
Composite Views
Before introducing the DataView view, let's introduce composite views.
A composite view is when multiple different views (TypedArray or DataView) simultaneously view and operate on the same ArrayBuffer.
First, a Regular View
Suppose there is an 8-byte memory block:
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
At this point, the buffer memory has only one Uint8Array view.
Now, a Composite View
The same memory block:
const buffer = new ArrayBuffer(8);
const uint8 = new Uint8Array(buffer);
const uint16 = new Uint16Array(buffer);
const dataView = new DataView(buffer);
Now, the buffer memory has three views: Uint8Array, Uint16Array, and DataView. This is a composite view.
Multiple views share the same block of memory.
The Most Intuitive Example
const buffer = new ArrayBuffer(4);
const uint8 = new Uint8Array(buffer);
const uint16 = new Uint16Array(buffer);
uint8[0] = 1;
uint8[1] = 0;
console.log(uint16[0]);
Output:
1;
Because uint8 and uint16 share the same memory, modifications in uint8 also affect the data in uint16.
Analogy: The Same Book
Think of ArrayBuffer as a book.
Reading by character:
T
o
d
a
y
Equivalent to:
Uint8Array;
Reading by word:
Today
Weather
Good
Equivalent to:
Uint16Array;
Reading from any position:
Start reading from the 3rd character
Equivalent to:
DataView;
These three people are reading:
The same book
Just in different ways.
This is the essence of composite views.
Why Are Composite Views Needed?
They are very common when parsing binary files.
For example, a WAV file consists of a file header, metadata, and PCM data.
function readString(view, offset, length) {
let str = "";
for (let i = 0; i < length; i++) {
str += String.fromCharCode(view.getUint8(offset + i));
}
return str;
}
async function main() {
const response = await fetch("/5s_16k_mono_16bit_440hz.wav");
const buffer = await response.arrayBuffer();
const view = new DataView(buffer);
const chunkID = readString(view, 0, 4); // "RIFF"
const pcm = new Int16Array(buffer, 44); // PCM data
}
main();
In the code above, a DataView reads the file header, and an Int16Array reads the PCM data. They share the same ArrayBuffer.
This is a typical use of composite views in real-world development.
DataView View
DataView is a view that can flexibly read and write an ArrayBuffer using any data type and at any byte offset.
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setInt16(0, 1000);
console.log(view.getInt16(0)); // 1000
The difference from TypedArray:
TypedArray = View the entire memory block as a single type
DataView = View any position in memory as any type
DataView can be called a "universal binary reader." It can read or write data from any position in an ArrayBuffer using any data type, making it ideal for parsing binary formats like WAV, PNG, and network protocols.
Creating a DataView
DataView must be created based on an ArrayBuffer. The syntax is:
new DataView(buffer, byteOffset?, byteLength?)
buffer: TheArrayBufferto operate onbyteOffset: Starting offset (in bytes), default 0byteLength: View length (in bytes), default to the end of the buffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
const view2 = new DataView(buffer, 2, 4); // Start at byte 2, read 4 bytes
Common DataView APIs
The DataView API is very regular: getXxx() methods read data, setXxx() methods write data, where Xxx represents the data type (e.g., Uint16, Float32). Combined with the buffer, byteOffset, and byteLength properties, you can flexibly operate on any binary data.
Properties
| Property | Purpose |
|---|---|
buffer |
The corresponding ArrayBuffer |
byteLength |
Number of bytes occupied by the view |
byteOffset |
Starting offset of the view in the buffer |
Read Methods
Read data from a specified offset as a specified type. Syntax: view.get<Type>(byteOffset, littleEndian?):
| Method | Purpose | Bytes |
|---|---|---|
getInt8() |
Read 8-bit signed integer | 1 |
getUint8() |
Read 8-bit unsigned integer | 1 |
getInt16() |
Read 16-bit signed integer | 2 |
getUint16() |
Read 16-bit unsigned integer | 2 |
getInt32() |
Read 32-bit signed integer | 4 |
getUint32() |
Read 32-bit unsigned integer | 4 |
getFloat16() |
Read 16-bit floating point | 2 |
getFloat32() |
Read 32-bit floating point | 4 |
getFloat64() |
Read 64-bit floating point | 8 |
getBigInt64() |
Read 64-bit integer (BigInt) | 8 |
getBigUint64() |
Read 64-bit unsigned int (BigInt) | 8 |
littleEndian defaults to false (Big Endian). Set to true for Little Endian.
Write Methods
Write data at a specified offset as a specified type. Syntax: view.set<Type>(byteOffset, value, littleEndian?):
| Method | Purpose | Bytes |
|---|---|---|
setInt8() |
Write 8-bit signed integer | 1 |
setUint8() |
Write 8-bit unsigned integer | 1 |
setInt16() |
Write 16-bit signed integer | 2 |
setUint16() |
Write 16-bit unsigned integer | 2 |
setInt32() |
Write 32-bit signed integer | 4 |
setUint32() |
Write 32-bit unsigned integer | 4 |
setFloat16() |
Write 16-bit floating point | 2 |
setFloat32() |
Write 32-bit floating point | 4 |
setFloat64() |
Write 64-bit floating point | 8 |
setBigInt64() |
Write 64-bit integer (BigInt) | 8 |
setBigUint64() |
Write 64-bit unsigned int (BigInt) | 8 |
Example:
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
// Write
view.setUint8(0, 82); // Write 'R' (ASCII 82) at byte 0
view.setUint16(2, 44100, true); // Write 44100 at byte 2 (Little Endian)
// Read
console.log(view.getUint8(0)); // 82
console.log(view.getUint16(2, true)); // 44100
About littleEndian
Except for getInt8(), getUint8(), setInt8(), and setUint8() (which are only 1 byte and don't involve byte order), all other multi-byte methods have an optional parameter:
view.getUint32(offset, true);
Where:
true
means:
Read or write in Little Endian byte order.
Endianness refers to the storage order of multi-byte data in memory. Take the 4-byte hexadecimal number 0x12345678 as an example:
Big Endian: High-order byte first
Address: [0] [1] [2] [3]
Data: 0x12 0x34 0x56 0x78 → Matches human reading habits
Little Endian: Low-order byte first
Address: [0] [1] [2] [3]
Data: 0x78 0x56 0x34 0x12 → More efficient for CPU computation
- Big Endian: High byte at low address, like reading a number "left-to-right." Common in network protocols.
- Little Endian: Low byte at low address, the default byte order for mainstream CPUs like x86/ARM.
The following function can be used to determine whether the current platform is Little Endian or Big Endian.
const BIG_ENDIAN = Symbol("BIG_ENDIAN");
const LITTLE_ENDIAN = Symbol("LITTLE_ENDIAN");
function getPlatformEndianness() {
let arr32 = Uint32Array.of(0x12345678);
let arr8 = new Uint8Array(arr32.buffer);
switch (arr8[0] * 0x1000000 + arr8[1] * 0x10000 + arr8[2] * 0x100 + arr8[3]) {
case 0x12345678:
return BIG_ENDIAN;
case 0x78563412:
return LITTLE_ENDIAN;
default:
throw new Error("Unknown endianness");
}
}
Summary
ArrayBuffer— Raw binary memory, cannot be read/written directly; requires views for operation.TypedArray— Fixed-type array view, 4 creation methods (based onArrayBufferis most efficient), supports rich array APIs, but length and type are fixed.- Composite Views — Multiple views sharing the same
ArrayBuffer, a core technique for parsing binary files. Analogous to "the same book, read by character, by word, or from any position." DataView— Universal binary reader, can read/write at any offset with any type.getXxx()/setXxx()methods cover all data types.- Endianness — Big Endian (high byte first, common in network protocols) vs. Little Endian (low byte first, default for mainstream CPUs).
DataView's multi-byte methods are controlled via thelittleEndianparameter; platform endianness can be detected using a composite view ofUint32Array+Uint8Array. getPlatformEndianness()— Reassembles 4 bytes using Big Endian weights (×256³, ×256², ×256, ×1) and compares the result to determine the platform's endianness.
Core idea: One block of memory, multiple interpretations — this is the essence of binary arrays.