Advanced JavaScript (ES6+)

Typed Arrays and ArrayBuffer - Binary Data Handling

13 min Lesson 25 of 40

Typed Arrays and ArrayBuffer - Binary Data Handling

Typed Arrays provide a mechanism for accessing raw binary data in JavaScript. They are essential for working with files, network protocols, WebGL graphics, audio processing, and other performance-critical operations that require direct memory manipulation.

What are Typed Arrays?

Typed Arrays are array-like views that provide a mechanism for accessing raw binary data. Unlike regular arrays, Typed Arrays:

  • Store data in a fixed-size binary buffer
  • Can only contain numbers of a specific type
  • Provide better performance for numeric operations
  • Are used in WebGL, Canvas, Web Audio, and File APIs
  • Cannot be resized after creation
Key Fact: Typed Arrays were initially created for WebGL to efficiently handle 3D graphics data, but they're now used in many Web APIs.

ArrayBuffer - The Foundation

ArrayBuffer is a generic, fixed-length raw binary data buffer:

// Create a 16-byte buffer const buffer = new ArrayBuffer(16); console.log(buffer.byteLength); // 16 console.log(buffer); // ArrayBuffer { byteLength: 16 } // ArrayBuffer itself cannot be directly manipulated // You need a "view" to read/write data // Check if ArrayBuffer is supported console.log(typeof ArrayBuffer); // 'function' // You cannot resize an ArrayBuffer // To "resize", you must create a new one and copy data
Important: ArrayBuffer is just raw memory. You cannot read or write to it directly. You need a Typed Array view or DataView to access the data.

Typed Array Types

JavaScript provides several Typed Array constructors for different numeric types:

Integer Types: Int8Array - 8-bit signed integer (-128 to 127) Uint8Array - 8-bit unsigned integer (0 to 255) Uint8ClampedArray- 8-bit unsigned integer, clamped (0 to 255) Int16Array - 16-bit signed integer (-32768 to 32767) Uint16Array - 16-bit unsigned integer (0 to 65535) Int32Array - 32-bit signed integer (-2^31 to 2^31-1) Uint32Array - 32-bit unsigned integer (0 to 2^32-1) Floating Point Types: Float32Array - 32-bit IEEE floating point Float64Array - 64-bit IEEE floating point BigInt Types (ES2020+): BigInt64Array - 64-bit signed integer BigUint64Array - 64-bit unsigned integer

Creating Typed Arrays

You can create Typed Arrays in several ways:

// Method 1: Create with length const int8 = new Int8Array(4); console.log(int8); // Int8Array [0, 0, 0, 0] console.log(int8.length); // 4 console.log(int8.byteLength); // 4 bytes // Method 2: Create from array const uint8 = new Uint8Array([10, 20, 30, 40]); console.log(uint8); // Uint8Array [10, 20, 30, 40] // Method 3: Create from ArrayBuffer const buffer = new ArrayBuffer(8); const int16 = new Int16Array(buffer); console.log(int16.length); // 4 (8 bytes / 2 bytes per element) // Method 4: Create from another Typed Array const float32 = new Float32Array(uint8); console.log(float32); // Float32Array [10, 20, 30, 40] // Method 5: Using .of() static method const arr = Uint8Array.of(1, 2, 3, 4); console.log(arr); // Uint8Array [1, 2, 3, 4] // Method 6: Using .from() static method const arr2 = Uint8Array.from([5, 6, 7, 8]); console.log(arr2); // Uint8Array [5, 6, 7, 8]

Working with Typed Arrays

Typed Arrays behave similarly to regular arrays but with some differences:

const numbers = new Uint8Array([1, 2, 3, 4, 5]); // Accessing elements console.log(numbers[0]); // 1 console.log(numbers[2]); // 3 // Setting elements numbers[0] = 10; numbers[1] = 20; console.log(numbers); // Uint8Array [10, 20, 3, 4, 5] // Array methods work console.log(numbers.length); // 5 console.log(numbers.map(x => x * 2)); // Uint8Array [20, 40, 6, 8, 10] console.log(numbers.filter(x => x > 3)); // Uint8Array [10, 20, 4, 5] console.log(numbers.reduce((sum, x) => sum + x, 0)); // 42 // Slicing creates a new Typed Array const slice = numbers.slice(1, 3); console.log(slice); // Uint8Array [20, 3] // Iteration for (const num of numbers) { console.log(num); } // Spread operator works console.log([...numbers]); // [10, 20, 3, 4, 5]

Value Clamping and Overflow

Different Typed Arrays handle out-of-range values differently:

// Uint8Array wraps around (0-255) const uint8 = new Uint8Array(3); uint8[0] = 255; uint8[1] = 256; // Wraps to 0 uint8[2] = -1; // Wraps to 255 console.log(uint8); // Uint8Array [255, 0, 255] // Uint8ClampedArray clamps values (0-255) const clamped = new Uint8ClampedArray(3); clamped[0] = 255; clamped[1] = 256; // Clamped to 255 clamped[2] = -1; // Clamped to 0 console.log(clamped); // Uint8ClampedArray [255, 255, 0] // Int8Array wraps with sign (-128 to 127) const int8 = new Int8Array(3); int8[0] = 127; int8[1] = 128; // Wraps to -128 int8[2] = -129; // Wraps to 127 console.log(int8); // Int8Array [127, -128, 127] // Float arrays don't wrap const float = new Float32Array(2); float[0] = 1.5; float[1] = 3.14159; console.log(float); // Float32Array [1.5, 3.14159]
Use Case: Uint8ClampedArray is specifically designed for Canvas pixel manipulation where values must be clamped to 0-255.

DataView - Flexible Binary Access

DataView provides a low-level interface for reading and writing multiple number types in an ArrayBuffer:

const buffer = new ArrayBuffer(8); const view = new DataView(buffer); // Write different types to the same buffer view.setInt8(0, 127); // 1 byte at offset 0 view.setInt16(1, 32767); // 2 bytes at offset 1 view.setFloat32(4, 3.14); // 4 bytes at offset 4 // Read the values back console.log(view.getInt8(0)); // 127 console.log(view.getInt16(1)); // 32767 console.log(view.getFloat32(4)); // 3.14 // Endianness control (default is big-endian) view.setInt16(0, 256, true); // Little-endian console.log(view.getInt16(0, true)); // 256 view.setInt16(2, 256, false); // Big-endian console.log(view.getInt16(2, false)); // 256

Buffer Views - Multiple Views on Same Data

Multiple Typed Arrays can view the same ArrayBuffer:

// Create a 4-byte buffer const buffer = new ArrayBuffer(4); // Create different views const view8 = new Uint8Array(buffer); const view16 = new Uint16Array(buffer); const view32 = new Uint32Array(buffer); // Modify through 8-bit view view8[0] = 255; view8[1] = 255; // Read through 16-bit view (combines two 8-bit values) console.log(view16[0]); // 65535 (255 * 256 + 255) // Modify through 32-bit view view32[0] = 0x12345678; // Read through 8-bit view (splits into bytes) console.log(view8); // Uint8Array [120, 86, 52, 18] (little-endian) // Practical example: Color conversion const colorBuffer = new ArrayBuffer(4); const rgba = new Uint8Array(colorBuffer); const color32 = new Uint32Array(colorBuffer); // Set RGBA values rgba[0] = 255; // Red rgba[1] = 128; // Green rgba[2] = 64; // Blue rgba[3] = 255; // Alpha // Get as single 32-bit value console.log(color32[0].toString(16)); // RGBA as hex

Practical Use Cases

// Example 1: Reading binary file data async function readBinaryFile(url) { const response = await fetch(url); const buffer = await response.arrayBuffer(); const uint8View = new Uint8Array(buffer); console.log(`File size: ${buffer.byteLength} bytes`); console.log(`First 10 bytes:`, uint8View.slice(0, 10)); return uint8View; } // Example 2: Image data manipulation function invertImageColors(imageData) { const pixels = new Uint8ClampedArray(imageData.data.buffer); // Iterate through RGBA pixels 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 // Alpha (i + 3) remains unchanged } return imageData; } // Example 3: Audio processing function generateSineWave(frequency, duration, sampleRate = 44100) { const numSamples = duration * sampleRate; const buffer = new Float32Array(numSamples); for (let i = 0; i < numSamples; i++) { const time = i / sampleRate; buffer[i] = Math.sin(2 * Math.PI * frequency * time); } return buffer; } const audioData = generateSineWave(440, 1); // 440 Hz A note, 1 second console.log(`Generated ${audioData.length} samples`); // Example 4: Network protocol parsing function parseHeader(buffer) { const view = new DataView(buffer); return { version: view.getUint8(0), type: view.getUint8(1), length: view.getUint16(2), timestamp: view.getUint32(4), checksum: view.getUint16(8) }; } // Example 5: WebGL vertex buffer function createVertexBuffer(gl, vertices) { const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // Convert to Float32Array for WebGL const vertexData = new Float32Array(vertices); gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW); return buffer; }

Converting Between Types

// Typed Array to regular Array const typed = new Uint8Array([1, 2, 3]); const regular = Array.from(typed); const regular2 = [...typed]; // Regular Array to Typed Array const arr = [10, 20, 30]; const typed2 = new Uint8Array(arr); const typed3 = Uint8Array.from(arr); // Typed Array to another Typed Array const uint8 = new Uint8Array([1, 2, 3]); const int16 = new Int16Array(uint8); const float = new Float32Array(uint8); // ArrayBuffer to Base64 string function bufferToBase64(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } // Base64 string to ArrayBuffer function base64ToBuffer(base64) { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; }

Performance Considerations

When to use Typed Arrays: ✓ Processing large amounts of numeric data ✓ Working with binary file formats ✓ WebGL graphics programming ✓ Audio/video processing ✓ Network protocol implementation ✓ High-performance mathematical computations Performance Benefits: - Faster access than regular arrays for numeric data - Less memory overhead - Better for CPU cache utilization - Required by many Web APIs (WebGL, Canvas, etc.) Limitations: - Fixed size (cannot grow/shrink) - Only numeric values - No holes (sparse arrays not possible) - Less flexible than regular arrays

Practice Exercise:

Challenge: Create a function that converts RGB color values to grayscale.

function rgbToGrayscale(imageData) { const pixels = new Uint8ClampedArray(imageData.data.buffer); // Process each pixel (4 bytes: RGBA) for (let i = 0; i < pixels.length; i += 4) { const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; // Calculate grayscale using luminosity method const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Set RGB to the same grayscale value pixels[i] = gray; pixels[i + 1] = gray; pixels[i + 2] = gray; // Alpha (i + 3) remains unchanged } return imageData; } // Test with sample pixel data const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(2, 2); // Set red pixel imageData.data[0] = 255; // R imageData.data[1] = 0; // G imageData.data[2] = 0; // B imageData.data[3] = 255; // A const grayscale = rgbToGrayscale(imageData); console.log(grayscale.data[0]); // ~76 (grayscale red)

Try it yourself: Create a function to adjust brightness or apply a sepia filter.

Summary

In this lesson, you learned:

  • ArrayBuffer is a fixed-size binary data buffer
  • Typed Arrays provide typed views into ArrayBuffers
  • Multiple Typed Array types exist for different numeric types
  • DataView enables flexible reading/writing of mixed data types
  • Typed Arrays are essential for WebGL, Canvas, and File APIs
  • Value clamping and overflow behavior varies by type
  • Multiple views can share the same underlying ArrayBuffer
  • Typed Arrays provide better performance for numeric operations
Module Complete! You've completed Module 4: ES6+ Data Structures. Next, we'll dive into Module 5: Object-Oriented JavaScript with ES6 Classes!