How to Work with ArrayBuffer and Binary Arrays in JavaScript
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));
The ArrayBuffer with the maxByteLength option and the resize() method were introduced in recent ECMAScript proposals. However, for most practical work, you treat ArrayBuffer as a fixed-size container and use typed arrays and DataView to interact with its contents.
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)
Endianness (byte order) matters when interpreting multi-byte values. Most modern computers use little-endian format, where the least significant byte is stored first. This means 0x0102 is stored as bytes 02 01. Typed arrays use the system's native endianness. If you need to control byte order explicitly (e.g., when reading binary file formats or network protocols), use DataView instead (covered later).
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:
falseor 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]
Binary file formats typically specify their endianness. Network protocols (TCP/IP) use big-endian ("network byte order"). Most CPU architectures (x86, ARM in standard mode) are little-endian. Always check the specification for the format you are working with and pass the correct littleEndian flag to DataView methods.
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 = `
`;
// Detect file type from magic bytes
let magicType = detectFileType(arrayBuffer);
info.innerHTML += ``;
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 = "<";
if (char === ">") char = ">";
if (char === "&") char = "&";
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 inputUint8Arrayto read individual bytes for magic number detection and hex displaysubarrayfor 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.byteLengthgives the size.
Typed Arrays (Uint8Array, Int32Array, Float64Array, etc.):
- Views over an
ArrayBufferthat 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
Uint8ClampedArraywhich 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 (
littleEndianparameter 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:
TextEncoderconverts strings toUint8Array(UTF-8).TextDecoderconverts 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οΏ½.