跪拜 Guibai
← Back to the summary

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:

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
  1. 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];

  1. 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.


  1. slice() copies data and returns a new TypedArray.
const copy = arr.slice(1, 3);

Result:

Uint8Array[(2, 3)];

The difference from subarray():

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:

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.

pic1.png

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?)
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

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

  1. ArrayBuffer — Raw binary memory, cannot be read/written directly; requires views for operation.
  2. TypedArray — Fixed-type array view, 4 creation methods (based on ArrayBuffer is most efficient), supports rich array APIs, but length and type are fixed.
  3. 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."
  4. DataView — Universal binary reader, can read/write at any offset with any type. getXxx()/setXxx() methods cover all data types.
  5. 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 the littleEndian parameter; platform endianness can be detected using a composite view of Uint32Array + Uint8Array.
  6. 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.