JavaScript Advanced

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.

By JavaScript Document Team
arraybufferbinary-datamemoryadvancedbuffers

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

  1. 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);
}
  1. 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);
  1. Consider endianness for portability
// Always specify endianness in DataView
view.setUint32(0, value, true); // little-endian
view.getUint32(0, true); // little-endian
  1. 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.