Skip to content

How to Work with ArrayBuffer and Binary Arrays in JavaScript

How to use JavaScript's ArrayBuffer, typed arrays (Uint8Array, Int32Array, etc.), and DataView for high-performance binary data manipulation, file processing, and network protocols.

JavaScript was originally designed for manipulating text, DOM elements, and simple data structures. But modern web applications often need to work with raw binary data: downloading and uploading files, processing images and audio, communicating with hardware, parsing binary file formats, working with WebSockets, and interfacing with WebAssembly. For all of these, JavaScript provides a set of low-level binary data APIs built around ArrayBuffer and typed arrays.

These APIs give you direct access to memory at the byte level, something that is natural in languages like C or Rust but was historically impossible in JavaScript. Understanding how they work opens the door to high-performance data processing, file manipulation, and network communication in the browser and Node.js.

This guide walks you through the entire binary data system: from ArrayBuffer (the raw memory container) through typed arrays (the views that let you read and write specific data types) to DataView (the flexible tool for mixed-format binary data), and finally how to convert between binary data and text.

ArrayBuffer: Raw Binary Data

An ArrayBuffer is a fixed-length block of raw binary data. Think of it as a chunk of memory with a specific number of bytes. You cannot read or write individual bytes directly on an ArrayBuffer. Instead, it serves as the underlying storage that views (typed arrays and DataView) operate on.

Creating an ArrayBuffer

// Create a buffer of 16 bytes (all initialized to zero)
let buffer = new ArrayBuffer(16);

console.log(buffer.byteLength); // 16
console.log(buffer);            // ArrayBuffer(16)

Every byte in a new ArrayBuffer is initialized to 0. The size is fixed at creation and cannot be changed later.

// The size is specified in bytes
let smallBuffer = new ArrayBuffer(4);    // 4 bytes
let mediumBuffer = new ArrayBuffer(256); // 256 bytes
let largeBuffer = new ArrayBuffer(1024); // 1 KB

console.log(smallBuffer.byteLength);     // 4
console.log(mediumBuffer.byteLength);    // 256
console.log(largeBuffer.byteLength);     // 1024

You Cannot Access Bytes Directly

This is a critical concept. ArrayBuffer itself has no methods for reading or writing data. It is just a container:

let buffer = new ArrayBuffer(8);

// ❌ These do NOT work
// buffer[0] = 42;           // Does nothing meaningful
// console.log(buffer[0]);   // undefined

To actually work with the data inside an ArrayBuffer, you need a view: either a typed array or a DataView. The ArrayBuffer provides the memory; the view provides the interpretation.

┌──────────────────────────────────────────┐
│              ArrayBuffer                 │
│  (raw bytes: 00 00 00 00 00 00 00 00)    │
│                                          │
│  ┌───────────────────┐                   │
│  │  Uint8Array view  │  → sees 8 bytes   │
│  └───────────────────┘                   │
│                                          │
│  ┌───────────────────┐                   │
│  │  Uint32Array view │  → sees 2 ints    │
│  └───────────────────┘                   │
│                                          │
│  ┌───────────────────┐                   │
│  │ Float64Array view │  → sees 1 float   │
│  └───────────────────┘                   │
└──────────────────────────────────────────┘

Multiple views can share the same underlying ArrayBuffer, and each view interprets the same bytes differently.

Slicing an ArrayBuffer

The slice method creates a new ArrayBuffer containing a copy of a portion of the original:

let buffer = new ArrayBuffer(16);

// Fill with some data via a view
let view = new Uint8Array(buffer);
for (let i = 0; i < 16; i++) {
  view[i] = i * 10;       // 0, 10, 20, 30, ... 150
}

// Slice bytes 4 through 7 (exclusive end)
let sliced = buffer.slice(4, 8);
console.log(sliced.byteLength); // 4

let slicedView = new Uint8Array(sliced);
console.log(slicedView);  // Uint8Array [40, 50, 60, 70]

// The slice is a COPY: modifying it does not affect the original
slicedView[0] = 255;
console.log(view[4]);     // 40 (original unchanged)

ArrayBuffer Is Not Resizable (by Default)

Once created, an ArrayBuffer cannot grow or shrink:

let buffer = new ArrayBuffer(8);
// buffer.byteLength = 16; // ❌ Cannot change

// To "resize," you must create a new buffer and copy the data
let newBuffer = new ArrayBuffer(16);
new Uint8Array(newBuffer).set(new Uint8Array(buffer));

Typed Arrays: Uint8Array, Int32Array, Float64Array, and More

Typed arrays are views over an ArrayBuffer that let you read and write data as specific numeric types. Each typed array treats the underlying bytes as a sequence of numbers of a particular format (unsigned 8-bit integers, 32-bit floats, etc.).

The Complete Typed Array Family

Typed Array Bytes per Element Value Range Description
Int8Array 1 -128 to 127 Signed 8-bit integer
Uint8Array 1 0 to 255 Unsigned 8-bit integer
Uint8ClampedArray 1 0 to 255 Clamped (no overflow wrapping)
Int16Array 2 -32,768 to 32,767 Signed 16-bit integer
Uint16Array 2 0 to 65,535 Unsigned 16-bit integer
Int32Array 4 -2,147,483,648 to 2,147,483,647 Signed 32-bit integer
Uint32Array 4 0 to 4,294,967,295 Unsigned 32-bit integer
Float32Array 4 ~1.2×10⁻³⁸ to ~3.4×10³⁸ 32-bit floating point
Float64Array 8 ~5×10⁻³²⁴ to ~1.8×10³⁰⁸ 64-bit floating point (same as JS number)
BigInt64Array 8 -2⁶³ to 2⁶³-1 Signed 64-bit BigInt
BigUint64Array 8 0 to 2⁶⁴-1 Unsigned 64-bit BigInt

Creating Typed Arrays

There are several ways to create a typed array:

From length (creates a new buffer automatically):

// Create a Uint8Array with 4 elements (and a new 4-byte ArrayBuffer underneath)
let bytes = new Uint8Array(4);
console.log(bytes.length);            // 4
console.log(bytes.byteLength);        // 4 (4 elements × 1 byte each)
console.log(bytes.buffer.byteLength); // 4 (the underlying ArrayBuffer)
console.log(bytes);                   // Uint8Array [0, 0, 0, 0]

// Create a Float64Array with 3 elements (needs 3 × 8 = 24 bytes)
let floats = new Float64Array(3);
console.log(floats.length);           // 3
console.log(floats.byteLength);       // 24 (3 elements × 8 bytes each)
console.log(floats);                  // Float64Array [0, 0, 0]

From an array or iterable:

let bytes = new Uint8Array([10, 20, 30, 40]);
console.log(bytes);   // Uint8Array [10, 20, 30, 40]

let ints = new Int32Array([100, -200, 300]);
console.log(ints);    // Int32Array [100, -200, 300]

let floats = new Float64Array([1.5, 2.7, 3.14159]);
console.log(floats);  // Float64Array [1.5, 2.7, 3.14159]

From an existing ArrayBuffer:

let buffer = new ArrayBuffer(8);

// View the 8 bytes as 8 individual bytes
let uint8 = new Uint8Array(buffer);
console.log(uint8.length);                     // 8

// View the same 8 bytes as 2 thirty-two-bit integers
let uint32 = new Uint32Array(buffer);
console.log(uint32.length);                    // 2

// View the same 8 bytes as 1 sixty-four-bit float
let float64 = new Float64Array(buffer);
console.log(float64.length);                   // 1

// All three views share the SAME underlying buffer
console.log(uint8.buffer === uint32.buffer);   // true
console.log(uint32.buffer === float64.buffer); // true

From a buffer with offset and length:

let buffer = new ArrayBuffer(16);

// View bytes 4-11 (8 bytes) as Uint8Array
let partial = new Uint8Array(buffer, 4, 8); // (buffer, byteOffset, length)
console.log(partial.length);                // 8
console.log(partial.byteOffset);            // 4
console.log(partial.byteLength);            // 8

Reading and Writing Data

Typed arrays support index-based access like regular arrays:

let bytes = new Uint8Array(4);

// Write values
bytes[0] = 72;         // 'H' in ASCII
bytes[1] = 101;        // 'e'
bytes[2] = 108;        // 'l'
bytes[3] = 108;        // 'l'

// Read values
console.log(bytes[0]); // 72
console.log(bytes[1]); // 101

// Iterate
for (let byte of bytes) {
  console.log(byte);   // 72, 101, 108, 108
}

Overflow and Clamping Behavior

Values that exceed the range of a typed array's element type are handled differently depending on the array type:

Regular typed arrays wrap around (modular arithmetic):

let uint8 = new Uint8Array(1);

uint8[0] = 256;         // 256 % 256 = 0
console.log(uint8[0]);  // 0

uint8[0] = 257;         // 257 % 256 = 1
console.log(uint8[0]);  // 1

uint8[0] = -1;          // Wraps to 255
console.log(uint8[0]);  // 255

let int8 = new Int8Array(1);
int8[0] = 128;          // Wraps to -128
console.log(int8[0]);   // -128

int8[0] = 200;          // Wraps to -56
console.log(int8[0]);   // -56

Uint8ClampedArray clamps to the range (no wrapping):

let clamped = new Uint8ClampedArray(1);

clamped[0] = 300;
console.log(clamped[0]); // 255 (clamped to max)

clamped[0] = -50;
console.log(clamped[0]); // 0 (clamped to min)

clamped[0] = 128;
console.log(clamped[0]); // 128 (within range, no change)

Uint8ClampedArray is specifically designed for image pixel data, where clamping is the correct behavior (pixel values must be 0-255).

Multiple Views on the Same Buffer

Different typed arrays can provide different interpretations of the same bytes:

let buffer = new ArrayBuffer(4);
let uint8 = new Uint8Array(buffer);
let uint32 = new Uint32Array(buffer);

// Write through the Uint32 view
uint32[0] = 0x44434241; // Hex value

// Read through the Uint8 view: see individual bytes
console.log(uint8[0]); // 65 (0x41 = 'A')
console.log(uint8[1]); // 66 (0x42 = 'B')
console.log(uint8[2]); // 67 (0x43 = 'C')
console.log(uint8[3]); // 68 (0x44 = 'D')

// The byte order depends on system endianness (typically little-endian on x86)
// 0x44434241 is stored as bytes: 41 42 43 44 (little-endian)

Typed Array Methods

Typed arrays share many methods with regular arrays, but not all:

let arr = new Uint8Array([50, 10, 40, 20, 30]);

// Familiar array methods that work:
arr.sort();
console.log(arr);               // Uint8Array [10, 20, 30, 40, 50]

let found = arr.find(x => x > 25);
console.log(found);             // 30

let filtered = arr.filter(x => x >= 30);
console.log(filtered);          // Uint8Array [30, 40, 50]

let mapped = arr.map(x => x * 2);
console.log(mapped);            // Uint8Array [20, 40, 60, 80, 100]

console.log(arr.includes(30));  // true
console.log(arr.indexOf(40));   // 3

arr.forEach((val, i) => {
  console.log(`${i}: ${val}`);
});

let sum = arr.reduce((acc, val) => acc + val, 0);
console.log(sum);               // 150

// slice returns a NEW typed array (with its own buffer)
let sliced = arr.slice(1, 4);
console.log(sliced);            // Uint8Array [20, 30, 40]

Methods that do not exist on typed arrays:

let arr = new Uint8Array([10, 20, 30]);

// ❌ These do NOT exist on typed arrays:
// arr.push(40);      // TypeError: arr.push is not a function
// arr.pop();         // TypeError
// arr.shift();       // TypeError
// arr.unshift(5);    // TypeError
// arr.splice(1, 1);  // TypeError
// arr.concat(other); // TypeError

Typed arrays have a fixed length, so methods that would change the length (push, pop, splice, concat) are not available.

The set Method: Copying Data Between Typed Arrays

The set method copies data from one array (or typed array) into another:

let dest = new Uint8Array(8);

// Copy from a regular array
dest.set([10, 20, 30]);
console.log(dest);    // Uint8Array [10, 20, 30, 0, 0, 0, 0, 0]

// Copy from another typed array with an offset
let source = new Uint8Array([40, 50, 60]);
dest.set(source, 3);  // Start copying at position 3
console.log(dest);    // Uint8Array [10, 20, 30, 40, 50, 60, 0, 0]

The subarray Method: A View Without Copying

Unlike slice (which copies data), subarray creates a new typed array view over the same buffer. Changes through the subarray affect the original:

let original = new Uint8Array([10, 20, 30, 40, 50]);

let sub = original.subarray(1, 4);            // Elements 1, 2, 3 (NO COPY)
console.log(sub);                             // Uint8Array [20, 30, 40]

// Modifying the subarray modifies the original
sub[0] = 99;
console.log(original);                        // Uint8Array [10, 99, 30, 40, 50]

// They share the same buffer
console.log(sub.buffer === original.buffer);  // true

Practical Example: Working with Image Pixel Data

One of the most common uses of typed arrays in the browser is manipulating image pixels via the Canvas API:

let canvas = document.createElement("canvas");
canvas.width = 200;
canvas.height = 100;
let ctx = canvas.getContext("2d");

// Draw something
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 200, 100);

// Get the pixel data: returns a Uint8ClampedArray
let imageData = ctx.getImageData(0, 0, 200, 100);
let pixels = imageData.data;            // Uint8ClampedArray

console.log(pixels.length);             // 80000 (200 × 100 × 4 channels: RGBA)
console.log(pixels[0]);                 // 0 (Red channel of first pixel)
console.log(pixels[1]);                 // 0 (Green channel)
console.log(pixels[2]);                 // 255 (Blue channel)
console.log(pixels[3]);                 // 255 (Alpha channel)

// Invert all colors
for (let i = 0; i < pixels.length; i += 4) {
  pixels[i]     = 255 - pixels[i];      // Red
  pixels[i + 1] = 255 - pixels[i + 1];  // Green
  pixels[i + 2] = 255 - pixels[i + 2];  // Blue
  // pixels[i + 3] stays the same (Alpha)
}

// Put the modified pixels back
ctx.putImageData(imageData, 0, 0);
document.body.append(canvas);

DataView: Flexible Binary Access

While typed arrays are efficient for working with uniform data (arrays of the same type), DataView is designed for reading and writing mixed-format binary data where different bytes represent different types. It gives you explicit control over byte order (endianness) and lets you read any type from any byte offset.

DataView is the right tool for parsing binary file formats (PNG, ZIP, MP3, PDF), network protocol headers, and any binary structure where fields of different sizes and types are packed together.

Creating a DataView

let buffer = new ArrayBuffer(16);
let view = new DataView(buffer);

// Or with offset and length
let partialView = new DataView(buffer, 4, 8); // Start at byte 4, span 8 bytes

Reading Data with Explicit Type and Offset

DataView provides getter methods for every numeric type. Each method takes a byte offset and an optional endianness flag:

let buffer = new ArrayBuffer(16);
let view = new DataView(buffer);

// Write some values
view.setUint8(0, 255);                  // 1 byte at offset 0
view.setInt16(1, -1000, true);          // 2 bytes at offset 1 (little-endian)
view.setUint32(3, 123456, true);        // 4 bytes at offset 3 (little-endian)
view.setFloat64(7, 3.14159, true);      // 8 bytes at offset 7 (little-endian)

// Read them back
console.log(view.getUint8(0));          // 255
console.log(view.getInt16(1, true));    // -1000
console.log(view.getUint32(3, true));   // 123456
console.log(view.getFloat64(7, true));  // 3.14159

DataView Methods Reference

Getter methods (reading):

Method Bytes Type
getInt8(offset) 1 Signed 8-bit integer
getUint8(offset) 1 Unsigned 8-bit integer
getInt16(offset, littleEndian?) 2 Signed 16-bit integer
getUint16(offset, littleEndian?) 2 Unsigned 16-bit integer
getInt32(offset, littleEndian?) 4 Signed 32-bit integer
getUint32(offset, littleEndian?) 4 Unsigned 32-bit integer
getFloat32(offset, littleEndian?) 4 32-bit float
getFloat64(offset, littleEndian?) 8 64-bit float
getBigInt64(offset, littleEndian?) 8 Signed 64-bit BigInt
getBigUint64(offset, littleEndian?) 8 Unsigned 64-bit BigInt

Setter methods (writing) follow the same pattern: setUint8(offset, value), setInt16(offset, value, littleEndian?), etc.

The littleEndian Parameter

The second parameter (third for setters) controls byte order:

  • false or omitted: big-endian (most significant byte first, aka "network byte order")
  • true: little-endian (least significant byte first, native on most CPUs)
let buffer = new ArrayBuffer(4);
let view = new DataView(buffer);

// Write the number 0x01020304

// Big-endian (default): bytes stored as 01 02 03 04
view.setUint32(0, 0x01020304, false);
let bytes = new Uint8Array(buffer);
console.log(bytes); // Uint8Array [1, 2, 3, 4]

// Little-endian: bytes stored as 04 03 02 01
view.setUint32(0, 0x01020304, true);
console.log(bytes); // Uint8Array [4, 3, 2, 1]

Practical Example: Parsing a Binary File Header

Many binary file formats start with a header of mixed-type fields. Here is an example of parsing a simplified BMP-like image header:

// Simulated BMP-like header (14 bytes):
// Bytes 0-1:  Magic number (Uint16, "BM" = 0x424D)
// Bytes 2-5:  File size (Uint32, little-endian)
// Bytes 6-9:  Reserved (Uint32)
// Bytes 10-13: Pixel data offset (Uint32, little-endian)

function parseBMPHeader(buffer) {
  let view = new DataView(buffer);

  let header = {
    // Read 2-byte magic number as big-endian to preserve character order
    magic: String.fromCharCode(view.getUint8(0), view.getUint8(1)),
    // File size: 4 bytes, little-endian (BMP uses little-endian)
    fileSize: view.getUint32(2, true),
    // Reserved
    reserved: view.getUint32(6, true),
    // Pixel data offset
    pixelOffset: view.getUint32(10, true)
  };

  return header;
}

// Create a simulated BMP header
let buffer = new ArrayBuffer(14);
let view = new DataView(buffer);

view.setUint8(0, 0x42);           // 'B'
view.setUint8(1, 0x4D);           // 'M'
view.setUint32(2, 1024000, true); // File size: ~1MB
view.setUint32(6, 0, true);       // Reserved
view.setUint32(10, 54, true);     // Pixel data starts at byte 54

let header = parseBMPHeader(buffer);
console.log(header);
// { magic: "BM", fileSize: 1024000, reserved: 0, pixelOffset: 54 }

Practical Example: Building a Binary Protocol Message

// Custom protocol message:
// Byte 0:     Message type (Uint8)
// Bytes 1-2:  Sequence number (Uint16, big-endian)
// Bytes 3-6:  Timestamp (Uint32, big-endian)
// Bytes 7-14: Payload value (Float64, big-endian)

function createMessage(type, sequence, timestamp, value) {
  let buffer = new ArrayBuffer(15);
  let view = new DataView(buffer);

  view.setUint8(0, type);
  view.setUint16(1, sequence, false);   // big-endian (network byte order)
  view.setUint32(3, timestamp, false);
  view.setFloat64(7, value, false);

  return buffer;
}

function parseMessage(buffer) {
  let view = new DataView(buffer);

  return {
    type: view.getUint8(0),
    sequence: view.getUint16(1, false),
    timestamp: view.getUint32(3, false),
    value: view.getFloat64(7, false)
  };
}

// Create and parse a message
let msgBuffer = createMessage(1, 42, Date.now() / 1000 | 0, 98.6);
let parsed = parseMessage(msgBuffer);

console.log(parsed);
// { type: 1, sequence: 42, timestamp: 1710500000, value: 98.6 }

When to Use DataView vs. Typed Arrays

Scenario Best Choice Why
Uniform data (all same type) Typed Array Faster, simpler indexing
Image pixels (RGBA) Uint8ClampedArray Clamping behavior, Canvas API
Audio samples Float32Array Web Audio API uses it
Mixed-type binary structures DataView Flexible types at any offset
Network protocols DataView Explicit endianness control
Binary file parsing DataView Mixed fields, specific byte order
High-performance math Typed Array Array-like access, optimized

Converting Between Binary and Text

A common task is converting between binary data (ArrayBuffer/typed arrays) and text strings. Text in computers is ultimately stored as bytes according to an encoding (UTF-8, UTF-16, ASCII, etc.), so converting between the two requires specifying which encoding to use.

TextEncoder: String to Binary (UTF-8)

TextEncoder converts a JavaScript string into a Uint8Array of UTF-8 bytes:

let encoder = new TextEncoder();  // Always encodes to UTF-8

let bytes = encoder.encode("Hello");
console.log(bytes);               // Uint8Array [72, 101, 108, 108, 111]
console.log(bytes.length);        // 5 (ASCII characters = 1 byte each in UTF-8)

// Multi-byte characters in UTF-8
let bytes2 = encoder.encode("Café");
console.log(bytes2);              // Uint8Array [67, 97, 102, 195, 169]
console.log(bytes2.length);       // 5 (é takes 2 bytes in UTF-8)

// Emoji take 4 bytes each in UTF-8
let bytes3 = encoder.encode("Hi 👋");
console.log(bytes3.length);       // 7 (H=1, i=1, space=1, 👋=4)

You can also encode into an existing buffer using encodeInto:

let encoder = new TextEncoder();
let buffer = new Uint8Array(20);

let result = encoder.encodeInto("Hello, World!", buffer);
console.log(result);
// { read: 13, written: 13 }

console.log(buffer.subarray(0, result.written));
// Uint8Array [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]

TextDecoder: Binary to String

TextDecoder converts binary data (an ArrayBuffer, typed array, or DataView) back into a JavaScript string. It supports many encodings, not just UTF-8:

let decoder = new TextDecoder("utf-8"); // Default is UTF-8

let bytes = new Uint8Array([72, 101, 108, 108, 111]);
let text = decoder.decode(bytes);
console.log(text);                      // "Hello"

// Works with ArrayBuffer directly
let buffer = new ArrayBuffer(5);
new Uint8Array(buffer).set([87, 111, 114, 108, 100]);
console.log(decoder.decode(buffer));    // "World"

Other encodings:

// Windows-1251 (Cyrillic)
let win1251decoder = new TextDecoder("windows-1251");
let cyrillicBytes = new Uint8Array([207, 240, 232, 226, 229, 242]);
console.log(win1251decoder.decode(cyrillicBytes));  // "Привет"

// ISO-8859-1 (Latin-1)
let latin1decoder = new TextDecoder("iso-8859-1");
let latinBytes = new Uint8Array([72, 101, 108, 108, 111, 33]);
console.log(latin1decoder.decode(latinBytes));      // "Hello!"

Handling Decoding Errors

By default, TextDecoder replaces invalid byte sequences with the Unicode replacement character (U+FFFD, �). You can make it throw an error instead:

// Default: replace invalid bytes
let decoder = new TextDecoder("utf-8");
let invalidBytes = new Uint8Array([0xFF, 0xFE]);
console.log(decoder.decode(invalidBytes));  // "��" (replacement characters)

// Strict mode: throw on invalid bytes
let strictDecoder = new TextDecoder("utf-8", { fatal: true });
try {
  strictDecoder.decode(invalidBytes);
} catch (error) {
  console.log(error.message);               // "The encoded data was not valid."
}

Streaming Decoding

When processing data in chunks (e.g., from a network stream), use the stream option to handle multi-byte characters that might be split across chunks:

let decoder = new TextDecoder("utf-8");

// "Café" in UTF-8: [67, 97, 102, 195, 169]
// If split across chunks: [67, 97, 102, 195] | [169]

let chunk1 = new Uint8Array([67, 97, 102, 195]);
let chunk2 = new Uint8Array([169]);

// With stream: true, the decoder keeps incomplete characters buffered
let part1 = decoder.decode(chunk1, { stream: true });
console.log(part1);           // "Caf" (195 is incomplete, buffered for next chunk)

let part2 = decoder.decode(chunk2);
console.log(part2);           // "é" (completes the buffered character)

console.log(part1 + part2);   // "Café"

Manual Hex and Base64 Conversions

Sometimes you need to convert binary data to and from hexadecimal or base64 strings:

Hex conversion:

// Uint8Array → Hex string
function toHex(uint8Array) {
  return Array.from(uint8Array)
    .map(byte => byte.toString(16).padStart(2, "0"))
    .join("");
}

// Hex string → Uint8Array
function fromHex(hexString) {
  let bytes = new Uint8Array(hexString.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
  }
  return bytes;
}

let data = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]);
let hex = toHex(data);
console.log(hex);       // "deadbeef"

let restored = fromHex(hex);
console.log(restored);  // Uint8Array [222, 173, 190, 239]

Base64 conversion (browser):

// Uint8Array → Base64
function toBase64(uint8Array) {
  let binaryString = Array.from(uint8Array)
    .map(byte => String.fromCharCode(byte))
    .join("");
  return btoa(binaryString);
}

// Base64 → Uint8Array
function fromBase64(base64String) {
  let binaryString = atob(base64String);
  let bytes = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes;
}

let data = new Uint8Array([72, 101, 108, 108, 111]);
let base64 = toBase64(data);
console.log(base64);                            // "SGVsbG8="

let decoded = fromBase64(base64);
console.log(decoded);                           // Uint8Array [72, 101, 108, 108, 111]
console.log(new TextDecoder().decode(decoded)); // "Hello"

Converting Between Typed Arrays and Regular Arrays

// Typed array → Regular array
let typedArr = new Uint8Array([10, 20, 30, 40]);
let regularArr = Array.from(typedArr);
// or
let regularArr2 = [...typedArr];
console.log(regularArr);                // [10, 20, 30, 40]
console.log(Array.isArray(regularArr)); // true

// Regular array → Typed array
let arr = [100, 200, 300];
let typed = new Uint16Array(arr);
console.log(typed);                     // Uint16Array [100, 200, 300]

Practical Example: Binary File Analyzer

Here is a comprehensive example that brings together ArrayBuffer, typed arrays, DataView, and text conversion to analyze binary files loaded through a file input:

<!DOCTYPE html>
<html>
<head>
  <title>Binary File Analyzer</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }
    .hex-dump { font-family: monospace; font-size: 13px; background: #1e1e1e; color: #d4d4d4;
                padding: 16px; border-radius: 8px; overflow-x: auto; white-space: pre; line-height: 1.6; }
    .offset { color: #569cd6; }
    .hex-byte { color: #ce9178; }
    .ascii { color: #6a9955; }
    .info { background: #f5f5f5; padding: 16px; border-radius: 8px; margin: 16px 0; }
    .info dt { font-weight: bold; margin-top: 8px; }
    .info dd { margin-left: 20px; font-family: monospace; }
  </style>
</head>
<body>
  <h1>Binary File Analyzer</h1>
  <input type="file" id="file-input">
  <div id="output"></div>

  <script>
    document.getElementById("file-input").addEventListener("change", async (event) => {
      let file = event.target.files[0];
      if (!file) return;

      let arrayBuffer = await file.arrayBuffer();
      let output = document.getElementById("output");
      output.innerHTML = "";

      // File info
      let info = document.createElement("dl");
      info.className = "info";
      info.innerHTML = `
        <dt>File Name</dt><dd>${file.name}</dd>
        <dt>File Size</dt><dd>${arrayBuffer.byteLength.toLocaleString()} bytes</dd>
        <dt>MIME Type</dt><dd>${file.type || "unknown"}</dd>
      `;

      // Detect file type from magic bytes
      let magicType = detectFileType(arrayBuffer);
      info.innerHTML += `<dt>Detected Type</dt><dd>${magicType}</dd>`;
      output.append(info);

      // Hex dump (first 256 bytes)
      let hexDump = createHexDump(arrayBuffer, 256);
      output.append(hexDump);
    });

    function detectFileType(buffer) {
      let bytes = new Uint8Array(buffer, 0, Math.min(8, buffer.byteLength));

      // Check magic bytes
      if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {
        return "PNG Image";
      }
      if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {
        return "JPEG Image";
      }
      if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {
        return "GIF Image";
      }
      if (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) {
        return "PDF Document";
      }
      if (bytes[0] === 0x50 && bytes[1] === 0x4B && bytes[2] === 0x03 && bytes[3] === 0x04) {
        return "ZIP Archive (or DOCX/XLSX/JAR)";
      }
      if (bytes[0] === 0x42 && bytes[1] === 0x4D) {
        return "BMP Image";
      }

      // Check if it looks like text
      let isText = bytes.every(b => (b >= 32 && b <= 126) || b === 9 || b === 10 || b === 13);
      if (isText) return "Text File (ASCII)";

      return "Unknown binary format";
    }

    function createHexDump(buffer, maxBytes) {
      let bytes = new Uint8Array(buffer, 0, Math.min(maxBytes, buffer.byteLength));
      let pre = document.createElement("pre");
      pre.className = "hex-dump";

      let lines = [];
      for (let offset = 0; offset < bytes.length; offset += 16) {
        let chunk = bytes.subarray(offset, Math.min(offset + 16, bytes.length));

        // Offset column
        let offsetStr = `<span class="offset">${offset.toString(16).padStart(8, "0")}</span>`;

        // Hex bytes
        let hexParts = [];
        for (let i = 0; i < 16; i++) {
          if (i < chunk.length) {
            hexParts.push(`<span class="hex-byte">${chunk[i].toString(16).padStart(2, "0")}</span>`);
          } else {
            hexParts.push("  ");
          }
        }
        let hexStr = hexParts.join(" ");

        // ASCII representation
        let asciiParts = [];
        for (let i = 0; i < chunk.length; i++) {
          let byte = chunk[i];
          let char = (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : ".";
          // Escape HTML special characters
          if (char === "<") char = "&lt;";
          if (char === ">") char = "&gt;";
          if (char === "&") char = "&amp;";
          asciiParts.push(char);
        }
        let asciiStr = `<span class="ascii">${asciiParts.join("")}</span>`;

        lines.push(`${offsetStr}  ${hexStr}  ${asciiStr}`);
      }

      if (buffer.byteLength > maxBytes) {
        lines.push(`\n... ${(buffer.byteLength - maxBytes).toLocaleString()} more bytes ...`);
      }

      pre.innerHTML = lines.join("\n");
      return pre;
    }
  </script>
</body>
</html>

This example demonstrates:

  • file.arrayBuffer() to get binary data from a file input
  • Uint8Array to read individual bytes for magic number detection and hex display
  • subarray for efficient byte range access without copying
  • Byte-to-hex conversion with toString(16)
  • Byte-to-ASCII conversion with String.fromCharCode
  • Practical binary format detection by reading header bytes

Summary

JavaScript's binary data system is built on three layers: the raw storage (ArrayBuffer), the views that interpret the storage (typed arrays and DataView), and the utilities that convert between binary and text.

ArrayBuffer:

  • A fixed-length block of raw bytes, initialized to zero.
  • Cannot be read or written directly. Requires a view.
  • slice() creates a copy of a portion. byteLength gives the size.

Typed Arrays (Uint8Array, Int32Array, Float64Array, etc.):

  • Views over an ArrayBuffer that interpret bytes as a uniform numeric type.
  • Support index-based access (arr[0]) and most array methods (map, filter, sort, find, reduce).
  • Do not support length-changing methods (push, pop, splice, concat).
  • Overflow wraps around (modular arithmetic), except Uint8ClampedArray which clamps.
  • set() copies data in. subarray() creates a view without copying. slice() copies.
  • Multiple typed arrays can share the same underlying buffer.
  • Use the system's native endianness for multi-byte values.

DataView:

  • A flexible view that reads/writes individual values of any type at any byte offset.
  • Explicit endianness control (littleEndian parameter on every get/set call).
  • Best for mixed-format binary data: file headers, network protocols, structured binary messages.
  • Methods: getUint8, setUint8, getInt16, setInt16, getUint32, setUint32, getFloat32, setFloat32, getFloat64, setFloat64, plus BigInt variants.

Text Conversion:

  • TextEncoder converts strings to Uint8Array (UTF-8).
  • TextDecoder converts binary data back to strings (supports UTF-8, Windows-1251, ISO-8859-1, and many more).
  • Use { stream: true } for chunked decoding of multi-byte characters.
  • Use { fatal: true } to throw on invalid byte sequences instead of replacing with �.
Navigation