JavaScript Typed Arrays: Working with Binary Data
Master JavaScript Typed Arrays for efficient binary data manipulation. Learn about ArrayBuffer, different typed array views, and practical applications.
Typed Arrays in JavaScript provide a mechanism for accessing raw binary data in memory buffers through array-like views. They offer better performance and memory efficiency when working with binary data compared to regular JavaScript arrays.
Understanding Typed Arrays
Typed Arrays consist of two main components: ArrayBuffer (raw binary data) and typed array views that interpret this data.
ArrayBuffer Basics
// Creating an ArrayBuffer
const buffer = new ArrayBuffer(16); // 16 bytes
console.log(buffer.byteLength); // 16
// ArrayBuffer holds raw binary data
// Cannot directly manipulate ArrayBuffer contents
// Need a "view" to work with the data
// Check if it's an ArrayBuffer
console.log(buffer instanceof ArrayBuffer); // true
// Create buffer from existing data
const data = new Uint8Array([1, 2, 3, 4]);
const bufferFromArray = data.buffer;
console.log(bufferFromArray.byteLength); // 4
// Slice a buffer (creates a copy)
const sliced = buffer.slice(0, 8);
console.log(sliced.byteLength); // 8
Typed Array Views
// Different typed array views
const buffer = new ArrayBuffer(16);
// 8-bit integers
const int8 = new Int8Array(buffer);
const uint8 = new Uint8Array(buffer);
// 16-bit integers
const int16 = new Int16Array(buffer);
const uint16 = new Uint16Array(buffer);
// 32-bit integers
const int32 = new Int32Array(buffer);
const uint32 = new Uint32Array(buffer);
// 64-bit integers (BigInt)
const bigInt64 = new BigInt64Array(buffer);
const bigUint64 = new BigUint64Array(buffer);
// Floating point
const float32 = new Float32Array(buffer);
const float64 = new Float64Array(buffer);
// Clamped array (0-255)
const uint8Clamped = new Uint8ClampedArray(buffer);
// 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(uint32.length); // 4 (16 bytes / 4 bytes per element)
console.log(float64.length); // 2 (16 bytes / 8 bytes per element)
Creating and Manipulating Typed Arrays
Different Ways to Create Typed Arrays
// From length
const arr1 = new Uint8Array(10); // 10 elements, initialized to 0
console.log(arr1); // Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// From array-like object
const arr2 = new Uint8Array([1, 2, 3, 4, 5]);
console.log(arr2); // Uint8Array(5) [1, 2, 3, 4, 5]
// From another typed array
const arr3 = new Uint16Array(arr2);
console.log(arr3); // Uint16Array(5) [1, 2, 3, 4, 5]
// From ArrayBuffer
const buffer = new ArrayBuffer(8);
const arr4 = new Float32Array(buffer);
console.log(arr4.length); // 2
// From ArrayBuffer with offset and length
const arr5 = new Uint8Array(buffer, 2, 4); // Start at byte 2, length 4
console.log(arr5.length); // 4
// From iterable
const arr6 = new Uint8Array(new Set([1, 2, 3, 4]));
console.log(arr6); // Uint8Array(4) [1, 2, 3, 4]
// Using from() method
const arr7 = Uint8Array.from([1, 2, 3], (x) => x * 2);
console.log(arr7); // Uint8Array(3) [2, 4, 6]
// Using of() method
const arr8 = Uint8Array.of(1, 2, 3, 4, 5);
console.log(arr8); // Uint8Array(5) [1, 2, 3, 4, 5]
Accessing and Modifying Elements
const array = new Uint8Array([10, 20, 30, 40, 50]);
// Access elements like regular array
console.log(array[0]); // 10
console.log(array[2]); // 30
// Modify elements
array[1] = 25;
console.log(array[1]); // 25
// Type coercion happens automatically
array[0] = 256; // Wraps around for Uint8 (256 % 256 = 0)
console.log(array[0]); // 0
array[0] = -1; // Wraps to 255 for Uint8
console.log(array[0]); // 255
// Clamped arrays behave differently
const clamped = new Uint8ClampedArray([0, 0, 0]);
clamped[0] = 300; // Clamped to 255
clamped[1] = -100; // Clamped to 0
console.log(clamped); // Uint8ClampedArray(3) [255, 0, 0]
// Float arrays handle decimals
const floats = new Float32Array([1.5, 2.7, 3.9]);
console.log(floats[0]); // 1.5
// BigInt arrays require BigInt values
const bigints = new BigInt64Array(2);
bigints[0] = 123n;
bigints[1] = BigInt(456);
console.log(bigints); // BigInt64Array(2) [123n, 456n]
Typed Array Methods
Typed Arrays share many methods with regular arrays but with some differences.
Array-like Methods
const array = new Uint8Array([1, 2, 3, 4, 5]);
// map - returns new typed array of same type
const doubled = array.map((x) => x * 2);
console.log(doubled); // Uint8Array(5) [2, 4, 6, 8, 10]
// filter - returns new typed array
const filtered = array.filter((x) => x > 2);
console.log(filtered); // Uint8Array(3) [3, 4, 5]
// reduce
const sum = array.reduce((acc, val) => acc + val, 0);
console.log(sum); // 15
// forEach
array.forEach((val, idx) => {
console.log(`Index ${idx}: ${val}`);
});
// find and findIndex
const found = array.find((x) => x > 3);
console.log(found); // 4
const index = array.findIndex((x) => x > 3);
console.log(index); // 3
// every and some
console.log(array.every((x) => x > 0)); // true
console.log(array.some((x) => x > 4)); // true
// slice - returns new typed array
const sliced = array.slice(1, 4);
console.log(sliced); // Uint8Array(3) [2, 3, 4]
// includes (ES2016)
console.log(array.includes(3)); // true
// indexOf and lastIndexOf
console.log(array.indexOf(3)); // 2
console.log(array.lastIndexOf(3)); // 2
Modifying Methods
const array = new Uint8Array([1, 2, 3, 4, 5]);
// set - copy elements from another array
const source = new Uint8Array([10, 20, 30]);
array.set(source, 1); // Copy at index 1
console.log(array); // Uint8Array(5) [1, 10, 20, 30, 5]
// set from regular array
array.set([100, 200], 0);
console.log(array); // Uint8Array(5) [100, 200, 20, 30, 5]
// subarray - creates view, not copy
const sub = array.subarray(1, 4);
console.log(sub); // Uint8Array(3) [200, 20, 30]
// Modifying subarray affects original
sub[0] = 50;
console.log(array); // Uint8Array(5) [100, 50, 20, 30, 5]
// fill
array.fill(0, 2, 4);
console.log(array); // Uint8Array(5) [100, 50, 0, 0, 5]
// reverse
array.reverse();
console.log(array); // Uint8Array(5) [5, 0, 0, 50, 100]
// sort
array.sort();
console.log(array); // Uint8Array(5) [0, 0, 5, 50, 100]
// copyWithin
array.copyWithin(0, 3, 5);
console.log(array); // Uint8Array(5) [50, 100, 5, 50, 100]
DataView for Complex Binary Data
DataView provides a low-level interface for reading and writing multiple number types in an ArrayBuffer.
Basic DataView Operations
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
// Write different types at different offsets
view.setUint8(0, 255);
view.setUint16(1, 65535, true); // little-endian
view.setFloat32(3, 3.14159, true);
view.setInt32(7, -2147483648, true);
// Read values back
console.log(view.getUint8(0)); // 255
console.log(view.getUint16(1, true)); // 65535
console.log(view.getFloat32(3, true)); // 3.14159
console.log(view.getInt32(7, true)); // -2147483648
// Endianness matters
view.setUint16(0, 0x1234); // big-endian (default)
console.log(view.getUint8(0)); // 0x12
console.log(view.getUint8(1)); // 0x34
view.setUint16(0, 0x1234, true); // little-endian
console.log(view.getUint8(0)); // 0x34
console.log(view.getUint8(1)); // 0x12
Working with Structured Data
// Define a binary structure
class BinaryStruct {
constructor(buffer) {
this.view = new DataView(buffer);
}
// Structure:
// - id (uint32): 4 bytes
// - x (float32): 4 bytes
// - y (float32): 4 bytes
// - active (uint8): 1 byte
// Total: 13 bytes
get id() {
return this.view.getUint32(0, true);
}
set id(value) {
this.view.setUint32(0, value, true);
}
get x() {
return this.view.getFloat32(4, true);
}
set x(value) {
this.view.setFloat32(4, value, true);
}
get y() {
return this.view.getFloat32(8, true);
}
set y(value) {
this.view.setFloat32(8, value, true);
}
get active() {
return Boolean(this.view.getUint8(12));
}
set active(value) {
this.view.setUint8(12, value ? 1 : 0);
}
}
// Usage
const buffer = new ArrayBuffer(13);
const struct = new BinaryStruct(buffer);
struct.id = 12345;
struct.x = 10.5;
struct.y = 20.7;
struct.active = true;
console.log(struct.id); // 12345
console.log(struct.x); // 10.5
console.log(struct.y); // 20.7
console.log(struct.active); // true
// Array of structures
class StructArray {
constructor(count) {
this.itemSize = 13;
this.buffer = new ArrayBuffer(count * this.itemSize);
this.count = count;
}
get(index) {
const offset = index * this.itemSize;
const itemBuffer = this.buffer.slice(offset, offset + this.itemSize);
return new BinaryStruct(itemBuffer);
}
getView(index) {
const offset = index * this.itemSize;
const view = new DataView(this.buffer, offset, this.itemSize);
return new BinaryStruct(
view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength)
);
}
}
Practical Applications
Image Processing
// Working with image pixel data
function processImageData(imageData) {
// imageData.data is a Uint8ClampedArray
const pixels = imageData.data;
// Process RGBA values
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const a = pixels[i + 3];
// Convert to grayscale
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
pixels[i] = gray; // R
pixels[i + 1] = gray; // G
pixels[i + 2] = gray; // B
// Alpha unchanged
}
return imageData;
}
// Create image filter
function applyFilter(imageData, kernel) {
const pixels = imageData.data;
const width = imageData.width;
const height = imageData.height;
// Create output buffer
const output = new Uint8ClampedArray(pixels.length);
const kernelSize = Math.sqrt(kernel.length);
const half = Math.floor(kernelSize / 2);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
for (let c = 0; c < 3; c++) {
// RGB channels
let sum = 0;
for (let ky = -half; ky <= half; ky++) {
for (let kx = -half; kx <= half; kx++) {
const px = Math.min(width - 1, Math.max(0, x + kx));
const py = Math.min(height - 1, Math.max(0, y + ky));
const idx = (py * width + px) * 4 + c;
const kidx = (ky + half) * kernelSize + (kx + half);
sum += pixels[idx] * kernel[kidx];
}
}
output[(y * width + x) * 4 + c] = sum;
}
// Copy alpha
output[(y * width + x) * 4 + 3] = pixels[(y * width + x) * 4 + 3];
}
}
return new ImageData(output, width, height);
}
// Gaussian blur kernel
const gaussianKernel = [
1 / 16,
2 / 16,
1 / 16,
2 / 16,
4 / 16,
2 / 16,
1 / 16,
2 / 16,
1 / 16,
];
Audio Processing
// Process audio samples
class AudioProcessor {
constructor(sampleRate = 44100) {
this.sampleRate = sampleRate;
}
// Generate sine wave
generateSineWave(frequency, duration, amplitude = 0.5) {
const samples = Math.floor(this.sampleRate * duration);
const buffer = new Float32Array(samples);
for (let i = 0; i < samples; i++) {
buffer[i] =
amplitude * Math.sin((2 * Math.PI * frequency * i) / this.sampleRate);
}
return buffer;
}
// Mix audio buffers
mixBuffers(buffers) {
const maxLength = Math.max(...buffers.map((b) => b.length));
const mixed = new Float32Array(maxLength);
for (let i = 0; i < maxLength; i++) {
let sum = 0;
let count = 0;
for (const buffer of buffers) {
if (i < buffer.length) {
sum += buffer[i];
count++;
}
}
mixed[i] = count > 0 ? sum / count : 0;
}
return mixed;
}
// Apply gain
applyGain(buffer, gain) {
const output = new Float32Array(buffer.length);
for (let i = 0; i < buffer.length; i++) {
output[i] = Math.max(-1, Math.min(1, buffer[i] * gain));
}
return output;
}
// Simple low-pass filter
lowPassFilter(buffer, cutoffFrequency) {
const output = new Float32Array(buffer.length);
const rc = 1.0 / (cutoffFrequency * 2 * Math.PI);
const dt = 1.0 / this.sampleRate;
const alpha = dt / (rc + dt);
output[0] = buffer[0];
for (let i = 1; i < buffer.length; i++) {
output[i] = output[i - 1] + alpha * (buffer[i] - output[i - 1]);
}
return output;
}
}
// Usage
const processor = new AudioProcessor();
const sine440 = processor.generateSineWave(440, 1); // A4 note, 1 second
const sine880 = processor.generateSineWave(880, 1); // A5 note
const mixed = processor.mixBuffers([sine440, sine880]);
const filtered = processor.lowPassFilter(mixed, 600);
Network Protocol Parsing
// Parse binary network protocols
class PacketParser {
constructor(buffer) {
this.view = new DataView(buffer);
this.offset = 0;
}
readUint8() {
const value = this.view.getUint8(this.offset);
this.offset += 1;
return value;
}
readUint16(littleEndian = false) {
const value = this.view.getUint16(this.offset, littleEndian);
this.offset += 2;
return value;
}
readUint32(littleEndian = false) {
const value = this.view.getUint32(this.offset, littleEndian);
this.offset += 4;
return value;
}
readFloat32(littleEndian = false) {
const value = this.view.getFloat32(this.offset, littleEndian);
this.offset += 4;
return value;
}
readString(length) {
const bytes = new Uint8Array(this.view.buffer, this.offset, length);
this.offset += length;
return new TextDecoder().decode(bytes);
}
readBytes(length) {
const bytes = new Uint8Array(this.view.buffer, this.offset, length);
this.offset += length;
return bytes;
}
}
// Example: Parse custom protocol
class GamePacket {
static parse(buffer) {
const parser = new PacketParser(buffer);
const packet = {
type: parser.readUint8(),
timestamp: parser.readUint32(true),
playerId: parser.readUint16(true),
x: parser.readFloat32(true),
y: parser.readFloat32(true),
z: parser.readFloat32(true),
action: parser.readUint8(),
dataLength: parser.readUint16(true),
};
if (packet.dataLength > 0) {
packet.data = parser.readBytes(packet.dataLength);
}
return packet;
}
static create(data) {
const buffer = new ArrayBuffer(20 + (data.data?.length || 0));
const view = new DataView(buffer);
let offset = 0;
view.setUint8(offset, data.type);
offset += 1;
view.setUint32(offset, data.timestamp, true);
offset += 4;
view.setUint16(offset, data.playerId, true);
offset += 2;
view.setFloat32(offset, data.x, true);
offset += 4;
view.setFloat32(offset, data.y, true);
offset += 4;
view.setFloat32(offset, data.z, true);
offset += 4;
view.setUint8(offset, data.action);
offset += 1;
view.setUint16(offset, data.data?.length || 0, true);
offset += 2;
if (data.data) {
new Uint8Array(buffer, offset).set(data.data);
}
return buffer;
}
}
File Processing
// Binary file reader
class BinaryFileReader {
constructor(arrayBuffer) {
this.buffer = arrayBuffer;
this.view = new DataView(arrayBuffer);
}
// Read file signatures
getFileSignature() {
const sig = new Uint8Array(this.buffer, 0, 4);
return Array.from(sig)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
// Check file types
isPNG() {
const signature = new Uint8Array(this.buffer, 0, 8);
const pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
return signature.every((byte, i) => byte === pngSignature[i]);
}
isJPEG() {
const signature = new Uint8Array(this.buffer, 0, 3);
return (
signature[0] === 0xff && signature[1] === 0xd8 && signature[2] === 0xff
);
}
// Read EXIF data from JPEG
readEXIF() {
if (!this.isJPEG()) return null;
let offset = 2; // Skip SOI marker
while (offset < this.buffer.byteLength) {
const marker = this.view.getUint16(offset);
if (marker === 0xffe1) {
// APP1 marker (EXIF)
const length = this.view.getUint16(offset + 2);
const exifData = new Uint8Array(this.buffer, offset + 4, length - 2);
// Check for "Exif\0\0"
const exifHeader = new Uint8Array(
exifData.buffer,
exifData.byteOffset,
6
);
if (String.fromCharCode(...exifHeader) === 'Exif\0\0') {
return this.parseEXIF(exifData.slice(6));
}
}
offset += 2 + this.view.getUint16(offset + 2);
}
return null;
}
parseEXIF(data) {
// Simplified EXIF parsing
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
const isLittleEndian = view.getUint16(0) === 0x4949; // "II"
return {
byteOrder: isLittleEndian ? 'little' : 'big',
// Additional EXIF parsing would go here
};
}
}
// Usage
async function analyzeFile(file) {
const buffer = await file.arrayBuffer();
const reader = new BinaryFileReader(buffer);
console.log('File signature:', reader.getFileSignature());
console.log('Is PNG:', reader.isPNG());
console.log('Is JPEG:', reader.isJPEG());
if (reader.isJPEG()) {
const exif = reader.readEXIF();
console.log('EXIF data:', exif);
}
}
Performance Optimization
Memory-Efficient Operations
// Efficient array operations
class TypedArrayUtils {
// In-place operations are more memory efficient
static addInPlace(target, source) {
const length = Math.min(target.length, source.length);
for (let i = 0; i < length; i++) {
target[i] += source[i];
}
return target;
}
// Use SIMD-like operations when possible
static dotProduct(a, b) {
let sum = 0;
const length = Math.min(a.length, b.length);
// Unroll loop for better performance
const unrolled = Math.floor(length / 4) * 4;
for (let i = 0; i < unrolled; i += 4) {
sum +=
a[i] * b[i] +
a[i + 1] * b[i + 1] +
a[i + 2] * b[i + 2] +
a[i + 3] * b[i + 3];
}
// Handle remaining elements
for (let i = unrolled; i < length; i++) {
sum += a[i] * b[i];
}
return sum;
}
// Efficient matrix multiplication
static matrixMultiply(a, b, aRows, aCols, bCols) {
const result = new Float32Array(aRows * bCols);
for (let i = 0; i < aRows; i++) {
for (let j = 0; j < bCols; j++) {
let sum = 0;
for (let k = 0; k < aCols; k++) {
sum += a[i * aCols + k] * b[k * bCols + j];
}
result[i * bCols + j] = sum;
}
}
return result;
}
// Memory pool for typed arrays
static createPool(Type, size, count) {
const pool = [];
const used = new Set();
for (let i = 0; i < count; i++) {
pool.push(new Type(size));
}
return {
acquire() {
for (const array of pool) {
if (!used.has(array)) {
used.add(array);
return array;
}
}
// All arrays in use, create new one
const array = new Type(size);
pool.push(array);
used.add(array);
return array;
},
release(array) {
used.delete(array);
array.fill(0); // Clear for reuse
},
stats() {
return {
total: pool.length,
used: used.size,
available: pool.length - used.size,
};
},
};
}
}
// Usage
const pool = TypedArrayUtils.createPool(Float32Array, 1000, 10);
const array1 = pool.acquire();
// Use array...
pool.release(array1);
Shared Memory and Atomics
// SharedArrayBuffer for multi-threaded operations
if (typeof SharedArrayBuffer !== 'undefined') {
// Create shared memory
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);
// Atomic operations
Atomics.store(sharedArray, 0, 42);
console.log(Atomics.load(sharedArray, 0)); // 42
// Atomic add
console.log(Atomics.add(sharedArray, 0, 10)); // Returns old value: 42
console.log(Atomics.load(sharedArray, 0)); // 52
// Compare and exchange
console.log(Atomics.compareExchange(sharedArray, 0, 52, 100)); // 52
console.log(Atomics.load(sharedArray, 0)); // 100
// Atomic wait and notify (in workers)
// Worker 1: Atomics.wait(sharedArray, 0, expectedValue);
// Worker 2: Atomics.notify(sharedArray, 0, 1);
}
// Worker example with shared memory
class SharedCounter {
constructor(sharedBuffer) {
this.counter = new Int32Array(sharedBuffer);
}
increment() {
return Atomics.add(this.counter, 0, 1);
}
decrement() {
return Atomics.sub(this.counter, 0, 1);
}
get value() {
return Atomics.load(this.counter, 0);
}
waitForValue(value, timeout) {
return Atomics.wait(this.counter, 0, value, timeout);
}
notify() {
return Atomics.notify(this.counter, 0, 1);
}
}
Best Practices
- Choose the right typed array for your data
// For pixel data (0-255)
const pixels = new Uint8ClampedArray(width * height * 4);
// For audio samples (-1 to 1)
const audio = new Float32Array(sampleCount);
// For large integers
const bigNumbers = new BigInt64Array(count);
- Use views for different interpretations
const buffer = new ArrayBuffer(16);
const bytes = new Uint8Array(buffer);
const floats = new Float32Array(buffer);
// Same memory, different views
bytes[0] = 0x00;
bytes[1] = 0x00;
bytes[2] = 0x80;
bytes[3] = 0x3f;
console.log(floats[0]); // 1.0
- Be aware of endianness
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
// Always specify endianness for multi-byte values
view.setUint32(0, 0x12345678, true); // little-endian
const bytes = new Uint8Array(buffer);
console.log(Array.from(bytes).map((b) => b.toString(16)));
// ['78', '56', '34', '12']
- Reuse buffers when possible
// Create once, reuse many times
const tempBuffer = new Float32Array(1024);
function processAudioChunk(input) {
// Use tempBuffer for processing
tempBuffer.set(input.subarray(0, 1024));
// Process tempBuffer...
return tempBuffer.slice(); // Return copy if needed
}
Conclusion
Typed Arrays are essential for efficient binary data manipulation in JavaScript. They provide better performance and memory efficiency compared to regular arrays when working with numeric data. Whether you're processing images, audio, parsing binary protocols, or working with WebGL, understanding Typed Arrays is crucial for building high-performance JavaScript applications that handle binary data effectively.