JavaScript ArrayBuffer: Working with Raw Binary Data
Master JavaScript ArrayBuffer for handling raw binary data. Learn about creating buffers, using views, and practical applications for binary data manipulation.
ArrayBuffer is a JavaScript object that represents a generic, fixed-length raw binary data buffer. It's the foundation for working with binary data in JavaScript, providing a way to directly manipulate memory at the byte level.
Understanding ArrayBuffer
ArrayBuffer holds a reference to a fixed-length contiguous memory area but doesn't provide direct access to its contents. You need "views" to read and write data.
Creating ArrayBuffers
// Create an ArrayBuffer with 16 bytes
const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16
// ArrayBuffer is just raw memory
console.log(buffer); // ArrayBuffer { byteLength: 16 }
// Check if object is an ArrayBuffer
console.log(buffer instanceof ArrayBuffer); // true
console.log(ArrayBuffer.isView(buffer)); // false (it's not a view)
// Create larger buffers
const kilobyte = new ArrayBuffer(1024); // 1 KB
const megabyte = new ArrayBuffer(1024 * 1024); // 1 MB
// Maximum size depends on the platform
try {
const hugeBuffer = new ArrayBuffer(Number.MAX_SAFE_INTEGER);
} catch (e) {
console.error('Buffer too large:', e.message);
}
ArrayBuffer Properties and Methods
const buffer = new ArrayBuffer(32);
// byteLength property
console.log(buffer.byteLength); // 32
// slice() method - creates a copy
const sliced = buffer.slice(8, 16);
console.log(sliced.byteLength); // 8
console.log(sliced === buffer); // false (it's a new buffer)
// Slice with negative indices
const lastBytes = buffer.slice(-8);
console.log(lastBytes.byteLength); // 8
// Slice beyond bounds
const partial = buffer.slice(24, 40);
console.log(partial.byteLength); // 8 (only 32-24=8 bytes available)
// Check if it's a view
const uint8View = new Uint8Array(buffer);
console.log(ArrayBuffer.isView(uint8View)); // true
console.log(ArrayBuffer.isView(buffer)); // false
console.log(ArrayBuffer.isView({})); // false
Working with Views
ArrayBuffer needs views to access and manipulate its data. There are two types of views: TypedArrays and DataView.
TypedArray Views
const buffer = new ArrayBuffer(16);
// Different typed array views on the same buffer
const uint8 = new Uint8Array(buffer);
const uint16 = new Uint16Array(buffer);
const float32 = new Float32Array(buffer);
// Modifying through one view affects others
uint8[0] = 255;
uint8[1] = 255;
console.log(uint16[0]); // 65535 (0xFFFF)
// Different views see different number of elements
console.log(uint8.length); // 16 (16 bytes / 1 byte per element)
console.log(uint16.length); // 8 (16 bytes / 2 bytes per element)
console.log(float32.length); // 4 (16 bytes / 4 bytes per element)
// Views can start at an offset
const offsetView = new Uint32Array(buffer, 4, 3); // Start at byte 4, 3 elements
console.log(offsetView.byteOffset); // 4
console.log(offsetView.length); // 3
console.log(offsetView.byteLength); // 12 (3 * 4 bytes)
// Views share the same buffer
console.log(uint8.buffer === uint16.buffer); // true
console.log(uint8.buffer === buffer); // true
DataView for Heterogeneous Data
const buffer = new ArrayBuffer(24);
const view = new DataView(buffer);
// Write different types at specific byte offsets
view.setUint8(0, 0xff); // 1 byte at offset 0
view.setUint16(1, 0xffff); // 2 bytes at offset 1
view.setUint32(3, 0xffffffff); // 4 bytes at offset 3
view.setFloat32(7, Math.PI); // 4 bytes at offset 7
view.setFloat64(11, Math.E); // 8 bytes at offset 11
view.setBigUint64(19, 123n); // 8 bytes at offset 19
// Read values back
console.log(view.getUint8(0)); // 255
console.log(view.getUint16(1)); // 65535
console.log(view.getUint32(3)); // 4294967295
console.log(view.getFloat32(7)); // 3.141592...
console.log(view.getFloat64(11)); // 2.718281...
console.log(view.getBigUint64(19)); // 123n
// DataView with offset and length
const partialView = new DataView(buffer, 8, 8);
console.log(partialView.byteOffset); // 8
console.log(partialView.byteLength); // 8
Memory Management and Copying
Efficient Buffer Operations
// Copying buffers
function copyBuffer(source) {
const copy = new ArrayBuffer(source.byteLength);
new Uint8Array(copy).set(new Uint8Array(source));
return copy;
}
// Concatenating buffers
function concatenateBuffers(...buffers) {
const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0);
const result = new ArrayBuffer(totalLength);
const uint8Result = new Uint8Array(result);
let offset = 0;
for (const buffer of buffers) {
uint8Result.set(new Uint8Array(buffer), offset);
offset += buffer.byteLength;
}
return result;
}
// Comparing buffers
function compareBuffers(a, b) {
if (a.byteLength !== b.byteLength) return false;
const viewA = new Uint8Array(a);
const viewB = new Uint8Array(b);
for (let i = 0; i < viewA.length; i++) {
if (viewA[i] !== viewB[i]) return false;
}
return true;
}
// Usage
const buffer1 = new ArrayBuffer(8);
const buffer2 = new ArrayBuffer(8);
new Uint8Array(buffer1).set([1, 2, 3, 4, 5, 6, 7, 8]);
new Uint8Array(buffer2).set([9, 10, 11, 12, 13, 14, 15, 16]);
const concatenated = concatenateBuffers(buffer1, buffer2);
console.log(concatenated.byteLength); // 16
const copied = copyBuffer(buffer1);
console.log(compareBuffers(buffer1, copied)); // true
Memory Views and Aliasing
const buffer = new ArrayBuffer(16);
// Multiple views can alias the same memory
const bytes = new Uint8Array(buffer);
const words = new Uint16Array(buffer);
const floats = new Float32Array(buffer);
// Demonstrate aliasing
floats[0] = 1.0;
console.log('Float value:', floats[0]);
console.log('As bytes:', Array.from(bytes.slice(0, 4)));
console.log('As 16-bit words:', words[0], words[1]);
// Modifying through one view
bytes[0] = 0x00;
bytes[1] = 0x00;
bytes[2] = 0x80;
bytes[3] = 0x3f;
console.log('Modified float:', floats[0]); // 1.0
// Overlapping views
const view1 = new Uint32Array(buffer, 0, 2); // First 8 bytes
const view2 = new Uint32Array(buffer, 4, 2); // Bytes 4-11
const view3 = new Uint32Array(buffer, 8, 2); // Last 8 bytes
view1[1] = 0x12345678;
console.log(view2[0].toString(16)); // 12345678 (overlapping)
Endianness and Byte Order
Understanding Endianness
// Platform endianness detection
function getPlatformEndianness() {
const buffer = new ArrayBuffer(2);
const uint16 = new Uint16Array(buffer);
const uint8 = new Uint8Array(buffer);
uint16[0] = 0x1234;
if (uint8[0] === 0x34 && uint8[1] === 0x12) {
return 'little-endian';
} else if (uint8[0] === 0x12 && uint8[1] === 0x34) {
return 'big-endian';
} else {
return 'unknown';
}
}
console.log('Platform endianness:', getPlatformEndianness());
// DataView allows explicit endianness control
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
// Write in different endianness
view.setUint32(0, 0x12345678, true); // little-endian
console.log('Little-endian bytes:', Array.from(new Uint8Array(buffer)));
// [0x78, 0x56, 0x34, 0x12]
view.setUint32(0, 0x12345678, false); // big-endian (default)
console.log('Big-endian bytes:', Array.from(new Uint8Array(buffer)));
// [0x12, 0x34, 0x56, 0x78]
// Endianness conversion
function swapEndianness32(value) {
return (
((value & 0xff000000) >>> 24) |
((value & 0x00ff0000) >>> 8) |
((value & 0x0000ff00) << 8) |
((value & 0x000000ff) << 24)
);
}
console.log(swapEndianness32(0x12345678).toString(16)); // 78563412
Practical Applications
Binary File Format Parsing
// WAV file parser
class WAVParser {
constructor(arrayBuffer) {
this.buffer = arrayBuffer;
this.view = new DataView(arrayBuffer);
this.offset = 0;
}
readString(length) {
const bytes = new Uint8Array(this.buffer, this.offset, length);
this.offset += length;
return String.fromCharCode(...bytes);
}
readUint32() {
const value = this.view.getUint32(this.offset, true); // little-endian
this.offset += 4;
return value;
}
readUint16() {
const value = this.view.getUint16(this.offset, true);
this.offset += 2;
return value;
}
parse() {
// RIFF header
const riffId = this.readString(4);
if (riffId !== 'RIFF') throw new Error('Not a valid WAV file');
const fileSize = this.readUint32();
const waveId = this.readString(4);
if (waveId !== 'WAVE') throw new Error('Not a valid WAV file');
// fmt chunk
const fmtId = this.readString(4);
if (fmtId !== 'fmt ') throw new Error('fmt chunk not found');
const fmtSize = this.readUint32();
const audioFormat = this.readUint16();
const numChannels = this.readUint16();
const sampleRate = this.readUint32();
const byteRate = this.readUint32();
const blockAlign = this.readUint16();
const bitsPerSample = this.readUint16();
// Skip to data chunk
this.offset = 36 + fmtSize - 16;
// data chunk
const dataId = this.readString(4);
if (dataId !== 'data') throw new Error('data chunk not found');
const dataSize = this.readUint32();
return {
format: {
audioFormat,
numChannels,
sampleRate,
byteRate,
blockAlign,
bitsPerSample,
},
dataOffset: this.offset,
dataSize,
duration: dataSize / byteRate,
};
}
getSamples() {
const info = this.parse();
const samples = new Float32Array(
this.buffer,
info.dataOffset,
info.dataSize / (info.format.bitsPerSample / 8)
);
return samples;
}
}
Network Protocol Implementation
// Custom binary protocol
class BinaryProtocol {
static MAGIC = 0xdeadbeef;
static VERSION = 1;
static createPacket(type, payload) {
const payloadBuffer = new TextEncoder().encode(JSON.stringify(payload));
const buffer = new ArrayBuffer(16 + payloadBuffer.length);
const view = new DataView(buffer);
const uint8View = new Uint8Array(buffer);
// Header
view.setUint32(0, this.MAGIC, false); // Magic number (4 bytes)
view.setUint8(4, this.VERSION); // Version (1 byte)
view.setUint8(5, type); // Packet type (1 byte)
view.setUint16(6, 0); // Reserved (2 bytes)
view.setUint32(8, payloadBuffer.length, false); // Payload length (4 bytes)
view.setUint32(12, this.calculateCRC(payloadBuffer), false); // CRC32 (4 bytes)
// Payload
uint8View.set(payloadBuffer, 16);
return buffer;
}
static parsePacket(buffer) {
if (buffer.byteLength < 16) {
throw new Error('Packet too small');
}
const view = new DataView(buffer);
// Verify magic number
const magic = view.getUint32(0, false);
if (magic !== this.MAGIC) {
throw new Error('Invalid magic number');
}
// Parse header
const version = view.getUint8(4);
const type = view.getUint8(5);
const payloadLength = view.getUint32(8, false);
const crc = view.getUint32(12, false);
// Verify buffer size
if (buffer.byteLength < 16 + payloadLength) {
throw new Error('Incomplete packet');
}
// Extract and verify payload
const payloadBuffer = new Uint8Array(buffer, 16, payloadLength);
if (this.calculateCRC(payloadBuffer) !== crc) {
throw new Error('CRC mismatch');
}
const payload = JSON.parse(new TextDecoder().decode(payloadBuffer));
return {
version,
type,
payload,
};
}
static calculateCRC(data) {
// Simple CRC32 implementation (simplified for example)
let crc = 0xffffffff;
for (const byte of data) {
crc ^= byte;
for (let i = 0; i < 8; i++) {
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
}
}
return crc ^ 0xffffffff;
}
}
// Usage
const packet = BinaryProtocol.createPacket(1, {
message: 'Hello',
timestamp: Date.now(),
});
const parsed = BinaryProtocol.parsePacket(packet);
console.log(parsed);
Image Processing
// BMP file creator
class BMPCreator {
static createBMP(width, height, pixels) {
// BMP header is 54 bytes
const headerSize = 54;
const dataSize = width * height * 4; // 32-bit pixels
const fileSize = headerSize + dataSize;
const buffer = new ArrayBuffer(fileSize);
const view = new DataView(buffer);
const uint8View = new Uint8Array(buffer);
// BMP Header
view.setUint16(0, 0x4d42, false); // 'BM'
view.setUint32(2, fileSize, true); // File size
view.setUint32(6, 0, true); // Reserved
view.setUint32(10, headerSize, true); // Data offset
// DIB Header
view.setUint32(14, 40, true); // Header size
view.setInt32(18, width, true); // Width
view.setInt32(22, -height, true); // Height (negative for top-down)
view.setUint16(26, 1, true); // Planes
view.setUint16(28, 32, true); // Bits per pixel
view.setUint32(30, 0, true); // Compression (none)
view.setUint32(34, dataSize, true); // Image size
view.setInt32(38, 2835, true); // X pixels per meter
view.setInt32(42, 2835, true); // Y pixels per meter
view.setUint32(46, 0, true); // Colors used
view.setUint32(50, 0, true); // Important colors
// Pixel data (BGRA format)
let offset = headerSize;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
uint8View[offset++] = pixels[idx + 2]; // B
uint8View[offset++] = pixels[idx + 1]; // G
uint8View[offset++] = pixels[idx]; // R
uint8View[offset++] = pixels[idx + 3]; // A
}
}
return buffer;
}
static createGradient(width, height) {
const pixels = new Uint8Array(width * height * 4);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
pixels[idx] = (x / width) * 255; // R
pixels[idx + 1] = (y / height) * 255; // G
pixels[idx + 2] = 128; // B
pixels[idx + 3] = 255; // A
}
}
return this.createBMP(width, height, pixels);
}
}
// Create and save BMP
const bmpBuffer = BMPCreator.createGradient(256, 256);
const blob = new Blob([bmpBuffer], { type: 'image/bmp' });
const url = URL.createObjectURL(blob);
// Can now use 'url' as image source
Crypto and Hashing
// Simple hash function using ArrayBuffer
class SimpleHash {
static async sha256(data) {
// Convert string to ArrayBuffer if needed
const buffer =
typeof data === 'string' ? new TextEncoder().encode(data).buffer : data;
// Use Web Crypto API
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
// Convert to hex string
const hashArray = new Uint8Array(hashBuffer);
const hashHex = Array.from(hashArray)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return hashHex;
}
static async hmac(key, data) {
const keyBuffer =
typeof key === 'string' ? new TextEncoder().encode(key).buffer : key;
const dataBuffer =
typeof data === 'string' ? new TextEncoder().encode(data).buffer : data;
// Import key
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
// Sign data
const signature = await crypto.subtle.sign('HMAC', cryptoKey, dataBuffer);
return new Uint8Array(signature);
}
}
// XOR encryption (for demonstration)
class XORCipher {
static encrypt(data, key) {
const dataView = new Uint8Array(data);
const keyView = new Uint8Array(key);
const result = new ArrayBuffer(data.byteLength);
const resultView = new Uint8Array(result);
for (let i = 0; i < dataView.length; i++) {
resultView[i] = dataView[i] ^ keyView[i % keyView.length];
}
return result;
}
static decrypt(data, key) {
// XOR is symmetric
return this.encrypt(data, key);
}
}
SharedArrayBuffer
SharedArrayBuffer allows sharing memory between workers and the main thread.
// Check if SharedArrayBuffer is available
if (typeof SharedArrayBuffer !== 'undefined') {
// Create shared memory
const shared = new SharedArrayBuffer(1024);
// Create views on shared memory
const sharedInts = new Int32Array(shared);
const sharedBytes = new Uint8Array(shared);
// Atomic operations on shared memory
Atomics.store(sharedInts, 0, 42);
console.log(Atomics.load(sharedInts, 0)); // 42
// Atomic add
const oldValue = Atomics.add(sharedInts, 0, 10);
console.log(oldValue); // 42
console.log(Atomics.load(sharedInts, 0)); // 52
// Compare and exchange
const success = Atomics.compareExchange(sharedInts, 0, 52, 100);
console.log(success); // 52 (old value)
console.log(Atomics.load(sharedInts, 0)); // 100
// Worker example
class SharedMemoryCoordinator {
constructor(buffer) {
this.buffer = buffer;
this.view = new Int32Array(buffer);
}
async runWorker(workerScript) {
const worker = new Worker(workerScript);
worker.postMessage({ cmd: 'init', buffer: this.buffer });
return new Promise((resolve, reject) => {
worker.onmessage = (e) => {
if (e.data.status === 'done') {
resolve(e.data.result);
} else if (e.data.status === 'error') {
reject(new Error(e.data.message));
}
};
});
}
wait(index, value, timeout) {
return Atomics.wait(this.view, index, value, timeout);
}
notify(index, count = 1) {
return Atomics.notify(this.view, index, count);
}
}
} else {
console.log('SharedArrayBuffer not available');
}
Performance Considerations
Memory Alignment
// Ensure proper alignment for performance
class AlignedBuffer {
static create(size, alignment = 8) {
// Round up size to alignment boundary
const alignedSize = Math.ceil(size / alignment) * alignment;
return new ArrayBuffer(alignedSize);
}
static isAligned(buffer, alignment) {
// Check if buffer size is aligned
return buffer.byteLength % alignment === 0;
}
static getAlignedView(buffer, Type, offset = 0) {
const bytesPerElement = Type.BYTES_PER_ELEMENT;
const alignedOffset = Math.ceil(offset / bytesPerElement) * bytesPerElement;
return new Type(buffer, alignedOffset);
}
}
// Buffer pooling for performance
class BufferPool {
constructor(bufferSize, poolSize) {
this.bufferSize = bufferSize;
this.available = [];
this.inUse = new Set();
// Pre-allocate buffers
for (let i = 0; i < poolSize; i++) {
this.available.push(new ArrayBuffer(bufferSize));
}
}
acquire() {
let buffer = this.available.pop();
if (!buffer) {
// Pool exhausted, create new buffer
buffer = new ArrayBuffer(this.bufferSize);
}
this.inUse.add(buffer);
return buffer;
}
release(buffer) {
if (this.inUse.has(buffer)) {
this.inUse.delete(buffer);
// Clear buffer before returning to pool
new Uint8Array(buffer).fill(0);
this.available.push(buffer);
}
}
get stats() {
return {
available: this.available.length,
inUse: this.inUse.size,
total: this.available.length + this.inUse.size,
};
}
}
Error Handling and Validation
// Safe buffer operations
class SafeBuffer {
constructor(size) {
if (!Number.isInteger(size) || size < 0) {
throw new TypeError('Size must be a non-negative integer');
}
try {
this.buffer = new ArrayBuffer(size);
} catch (e) {
throw new RangeError(
`Cannot allocate buffer of size ${size}: ${e.message}`
);
}
}
static validateView(view) {
if (!ArrayBuffer.isView(view)) {
throw new TypeError('Argument must be an ArrayBuffer view');
}
}
static validateOffset(buffer, offset, length) {
if (offset < 0 || offset + length > buffer.byteLength) {
throw new RangeError('Offset and length out of bounds');
}
}
static safeSlice(buffer, start, end) {
const actualStart = Math.max(0, Math.min(start, buffer.byteLength));
const actualEnd = Math.max(actualStart, Math.min(end, buffer.byteLength));
return buffer.slice(actualStart, actualEnd);
}
}
// Buffer utilities
class BufferUtils {
static toHexString(buffer) {
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join(' ');
}
static fromHexString(hexString) {
const hex = hexString.replace(/\s/g, '');
const buffer = new ArrayBuffer(hex.length / 2);
const view = new Uint8Array(buffer);
for (let i = 0; i < hex.length; i += 2) {
view[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return buffer;
}
static toBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
}
static fromBase64(base64) {
const binary = atob(base64);
const buffer = new ArrayBuffer(binary.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < binary.length; i++) {
view[i] = binary.charCodeAt(i);
}
return buffer;
}
}
Best Practices
- Always check buffer bounds
function safeAccess(buffer, offset, length) {
if (offset + length > buffer.byteLength) {
throw new RangeError('Buffer access out of bounds');
}
return new Uint8Array(buffer, offset, length);
}
- Use appropriate view types
// For byte manipulation
const bytes = new Uint8Array(buffer);
// For numeric data
const floats = new Float32Array(buffer);
// For mixed types
const view = new DataView(buffer);
- Consider endianness for portability
// Always specify endianness in DataView
view.setUint32(0, value, true); // little-endian
view.getUint32(0, true); // little-endian
- Reuse buffers when possible
// Instead of creating new buffers repeatedly
const pool = new BufferPool(1024, 10);
const buffer = pool.acquire();
// Use buffer...
pool.release(buffer);
Conclusion
ArrayBuffer is the foundation for binary data manipulation in JavaScript. It provides low-level access to raw memory, enabling efficient processing of binary formats, network protocols, and multimedia data. Combined with TypedArrays and DataView, ArrayBuffer offers powerful capabilities for applications requiring direct memory manipulation. Understanding ArrayBuffer is essential for performance-critical applications and working with binary data formats in modern JavaScript.