Typed Arrays

Typed arrays are array-like objects that provide a way to work with binary data in a more controlled manner. They are particularly useful when dealing with scenarios where you need to handle raw binary data, such as networking, file I/O, or WebGL.

Unlike regular JavaScript arrays, typed arrays have fixed-length and contain elements of a single data type, which is specified when the typed array is created. This allows for more efficient memory usage and faster manipulation of the data.

The types with names Int[bits]Array and Uint[bits]Array hold signed and unsigned integers of 8, 16, or 32 bits. The BigInt64Array and BigUint64Array types hold 64-bit integers represented as BigInt values. The types with names Float32Array and Float64Array hold floating point numbers of 32 or 64 bits. The elements of Float64Array are the same type as regular JavaScript numbers. The type Uint8ClampedArray clamps values in the range 0-255 instead of overflowing or underflowing like other types arrays.

let bytes = new Uint8Array(1024); // 1024 bytes
let matrix = new Float64Array(9); // 3x3 matrix
let point = new Int16Array(3); // Point in 3D space
let rgba = new Uint8ClampedArray(4); // 4-byte RGBA pixel value
let sudoku = new Int8Array(81); // 9x9 sudoku board

The arrays are initialized by default with 0, 0n, or 0.0. You can specify the values in the typed array with the static of() factory method. The from() method expects an array-like iterable object as its first argument to copy its elements into the new typed array with automatic value truncation.

let white = Uint8ClampedArray.of(255, 255, 255, 0); // RGBA opaque white
let ints = Uint32Array.from(white); // Same numbers but 32-bit integers
Uint8Array.of(1.23, 2.98, 32000); // => new Uint8Array([1, 2, 0])

The ArrayBuffer type is a reference to a chunk of memory. The reason to work with ArrayBuffer objects is that sometimes you may want to have multiple typed array views of a single buffer.

To use the buffer, call the typed array constructor with an ArrayBuffer as the first argument, a byte offset (multiple of the size of the chosen type) within the array buffer as the second optional argument, and the array length (in elements) as the third optional argument. If you omit both optional arguments, the array will use all of the memory in the array buffer. If you omit the array length argument, then the array will use all of the available memory between the offset position and the end of the buffer.

let buffer = new ArrayBuffer(1024*1024); // Allocate 1MB of memory
let asbytes = new Uint8Array(buffer); // View as bytes
let asints = new Int32Array(buffer); // View as 32-bit signed integers
let lastK = new Uint8Array(buffer, 1023*1024); // View last kilobyte as bytes
let ints256 = new Int32Array(buffer, 1024, 256); // View 2nd kilobyte as 256 integers

The typed arrays have fixed length, and methods that change the length of the array, such as push(), pop(), unshift(), shift(), and splice() are not implemented. Methods that alter contents of an array without changing the length, such as sort(), reverse(), and fill() are implemented. Methods that return new arrays, such as map() and slice(), return a typed array of the same type.

let ints10 = new Int16Array(10);
ints10.fill(3).map(x => x*x).slice(7).join(''); // => '999'

The set() method copies the array specified at its first argument into the array it is invoked upon with an offset specified at its second optional argument, which defaults to 0.

let kb = new Uint8Array(1024); // 1024 bytes array
let pattern = new Uint8Array([0,1,2,3]); // 4 bytes array
kb.set(pattern); // Copy pattern to the start of kb
kb.set(pattern, 4); // Copy pattern again to kb with an offset
kb.set([0,1,2,3], 8); // Copy values directly from a refular array
kb.slice(0, 12); // => new Uint8Array([0,1,2,3,0,1,2,3,0,1,2,3])

The subarray() method returns a new view of the array on which it was called on. Every typed array has a buffer property which is the ArrayBuffer of the array. The byteOffset property is the starting position of the array's data within the underlying buffer. And byteLength is the length of the array's data in bytes.

let ints8 = new Uint8Array([0,1,2,3,4,5,6,7]);
let last3 = ints8.subarray(ints8.length-3, ints8.length); // => View of the same values
last3[0] = 8; // Change the array first element
ints8[5]; // => 8: ints8 array has changed too
last3.buffer; // ArrayBuffer object of a typed array
last3.buffer === ints8.buffer; // => true
last3.byteOffset; // => 5
last3.byteLength; // => 3
last3.buffer.byteLength; // => 8

You can create an initial typed array and then use its buffer to create other views.

let bytes4 = new Uint8Array([0,1,2,3]); // 4 bytes
// 00000000, 00000001, 00000010, 00000011 in binary
let ints2 = new Uint16Array(bytes4.buffer); // 2 Uint16 integers
// 00000001-00000000, 00000011-00000010 in binary
let ints1 = new Uint32Array(bytes4.buffer); // 1 Uint32 integers
// 00000011-00000010-00000001-00000000 in binary

Typed arrays allow you to view the same sequence of bytes in chunks of 8, 16, 32, or 64 bits. This exposes the endianness, or the order in which bytes are arranged into longer words.

In big-endian byte order, the most significant byte (the byte containing the highest-order bits) is stored at the lowest memory address, while the least significant byte (containing the lowest-order bits) is stored at the highest memory address. In little-endian byte order, the least significant byte is stored at the lowest memory address, while the most significant byte is stored at the highest memory address.

For example, if we want to store a 4-byte integer 0x12345678 (hexadecimal) into memory, a big-endian system will have the memory layout 12 34 56 78 with address allocation 0x12->0x1000, 0x34->0x1001, 0x56->0x1002, and 0x78->0x1003, while a little-endian system will arrange the memory as 78 56 34 12 with address allocation 0x78->0x1000, 0x56->0x1001, 0x34->0x1002, and 0x12->0x1003.

For efficiency, typed arrays in JavaScript use the native endianness of the underlying hardware. You can find out the endianness of your system with the following code.

let littleEndian = new Int8Array(new Int32Array([1]).buffer)[0] === 1;

Most common CPU architectures are little-endian, however, many network protocols, and some binary file formats are in big-endian format. When working with external data, you can use Int8Array and Uint8Array to view data as individual bytes, but not other multibyte types. Instead, use DataView class, which defines methods for reading and writing values from an ArrayBuffer with explicitly specified byte ordering.

DataView class defines 10 get methods for each of 10 typed arrays, such as getInt16(), getUint32(), and getFloat64(). These methods have first argument specifying the offset within ArrayBuffer at which the value begins and an optional boolean value as their second argument specifying whether little-endian ordering should be used. The corresponding set methods are used to write values into the underlying ArrayBuffer with the first argument as the offset, the second argument as the value to write, and the third optional argument specifying whether little-ending byte ordering should be used.

let bytes12 = new Uint8Array([0,1,2,3,4,5,6,7,8,9,10,11]);
let view = new DataView(bytes12.buffer, bytes12.byteOffset, bytes12.byteLength);

// Read 4 bytes from offset 0 and interpret as Int32 in big-endian format
let viewInt = view.getInt32(0);
// 00000000-00000001-00000010-00000011 in binary

// Read 4 bytes from offset 4 and interpret as Int32 in big-endian format
viewInt = view.getInt32(4, false);
// 00000100-00000101-00000110-00000111 in binary

// Read 4 bytes from offset 8 and interpret as Uint32 in little-endian format
viewInt = view.getUint32(8, true);
// 00001011-00001010-00001001-00001000 in binary

// Write viewInt as Uint32 in 4 bytes starting at offset 8 in big-endian format
view.setUint32(8, viewInt, false);