ArrayBuffer
, Typed Arrays, DataView
new ArrayBuffer()
ArrayBuffer
ArrayBuffer.prototype
new DataView()
DataView.prototype
Much data on the web is text: JSON files, HTML files, CSS files, JavaScript code, etc. JavaScript handles such data well, via its built-in strings.
However, before 2011, it did not handle binary data well. The Typed Array Specification 1.0 was introduced on 8 February 2011 and provides tools for working with binary data. With ECMAScript 6, Typed Arrays were added to the core language and gained methods that were previously only available for normal Arrays (.map()
, .filter()
, etc.).
The main uses cases for Typed Arrays, are:
ArrayBuffer
, Typed Arrays, DataView
The Typed Array API stores binary data in instances of ArrayBuffer
:
An ArrayBuffer itself is a black box: If you want to access its data, you must wrap it in another object – a view object. Two kinds of view objects are available:
Uint8Array
: Elements are unsigned 8-bit integers. Unsigned means that their ranges start at zero.Int16Array
: Elements are signed 16-bit integers. Signed means that they have a sign and can be negative, zero, or positive.Float32Array
: Elements are 32-bit floating point numbers.Uint8
, Int16
, Float32
, etc.) that you can read and write at any byte offset.Fig. 20 shows a class diagram of the API.
Typed Arrays are used much like normal Arrays, with a few notable differences:
The following code shows three different ways of creating the same Typed Array:
// Argument: Typed Array or Array-like object
const ta1 = new Uint8Array([0, 1, 2]);
const ta2 = Uint8Array.of(0, 1, 2);
const ta3 = new Uint8Array(3); // length of Typed Array
ta3[0] = 0;
ta3[1] = 1;
ta3[2] = 2;
assert.deepEqual(ta1, ta2);
assert.deepEqual(ta1, ta3);
const typedArray = new Int16Array(2); // 2 elements
assert.equal(typedArray.length, 2);
assert.deepEqual(
typedArray.buffer, new ArrayBuffer(4)); // 4 bytes
const typedArray = new Int16Array(2);
assert.equal(typedArray[1], 0); // initialized with 0
typedArray[1] = 72;
assert.equal(typedArray[1], 72);
This is how DataViews are used:
const dataView = new DataView(new ArrayBuffer(4));
assert.equal(dataView.getInt16(0), 0);
assert.equal(dataView.getUint8(0), 0);
dataView.setUint8(0, 5);
Element | Typed Array | Bytes | Description | |
---|---|---|---|---|
Int8 |
Int8Array |
1 | 8-bit signed integer | ES6 |
Uint8 |
Uint8Array |
1 | 8-bit unsigned integer | ES6 |
Uint8C |
Uint8ClampedArray |
1 | 8-bit unsigned integer | ES6 |
(clamped conversion) | ES6 | |||
Int16 |
Int16Array |
2 | 16-bit signed integer | ES6 |
Uint16 |
Uint16Array |
2 | 16-bit unsigned integer | ES6 |
Int32 |
Int32Array |
4 | 32-bit signed integer | ES6 |
Uint32 |
Uint32Array |
4 | 32-bit unsigned integer | ES6 |
Float32 |
Float32Array |
4 | 32-bit floating point | ES6 |
Float64 |
Float64Array |
8 | 64-bit floating point | ES6 |
Tbl. 19 lists the available element types. These types (e.g. Int32
) show up in two locations:
Int32Array
have the type Int32
. The element type is the only aspect of Typed Arrays that differs..getInt32()
and .setInt32()
.The element type Uint8C
is special: it is not supported by DataView
and only exists to enable Uint8ClampedArray
. This Typed Array is used by the canvas
element (where it replaces CanvasPixelArray
) and should otherwise be avoided. The only difference between Uint8C
and Uint8
is how overflow and underflow are handled (as explained in the next subsection).
Normally, when a value is out of the range of the element type, modulo arithmetic is used to convert it to a value within range. For signed and unsigned integers that means that:
The following function helps illustrate how conversion works:
Modulo conversion for unsigned 8-bit integers:
const uint8 = new Uint8Array(1);
// Highest value of range
assert.equal(setAndGet(uint8, 255), 255);
// Overflow
assert.equal(setAndGet(uint8, 256), 0);
// Lowest value of range
assert.equal(setAndGet(uint8, 0), 0);
// Underflow
assert.equal(setAndGet(uint8, -1), 255);
Modulo conversion for signed 8-bit integers:
const int8 = new Int8Array(1);
// Highest value of range
assert.equal(setAndGet(int8, 127), 127);
// Overflow
assert.equal(setAndGet(int8, 128), -128);
// Lowest value of range
assert.equal(setAndGet(int8, -128), -128);
// Underflow
assert.equal(setAndGet(int8, -129), 127);
Clamped conversion is different:
const uint8c = new Uint8ClampedArray(1);
// Highest value of range
assert.equal(setAndGet(uint8c, 255), 255);
// Overflow
assert.equal(setAndGet(uint8c, 256), 255);
// Lowest value of range
assert.equal(setAndGet(uint8c, 0), 0);
// Underflow
assert.equal(setAndGet(uint8c, -1), 0);
Whenever a type (such as Uint16
) is stored as a sequence of multiple bytes, endianness matters:
Uint16
value 0x4321 is stored as two bytes – first 0x43, then 0x21. Uint16
value 0x4321 is stored as two bytes – first 0x21, then 0x43. Endianness tends to be fixed per CPU architecture and consistent across native APIs. Typed Arrays are used to communicate with those APIs, which is why their endianness follows the endianness of the platform and can’t be changed.
On the other hand, the endianness of protocols and binary files varies, but is fixed per format, across platforms. Therefore, we must be able to access data with either endianness. DataViews serve this use case and let you specify endianness when you get or set a value.
Quoting Wikipedia on Endianness:
Other orderings are also possible. Those are generically called middle-endian or mixed-endian.
In this section, «ElementType»Array
stands for Int8Array
, Uint8Array
, etc. ElementType
is Int8
, Uint8
, etc.
«ElementType»Array.from()
This method has the type signature:
.from<S>(
source: Iterable<S>|ArrayLike<S>,
mapfn?: S => ElementType, thisArg?: any)
: «ElementType»Array
.from()
converts source
into an instance of this
(a Typed Array).
For example, normal Arrays are iterable and can be converted with this method:
Typed Arrays are also iterable:
source
can also be an Array-like object:
The optional mapfn
lets you transform the elements of source
before they become elements of the result. Why perform the two steps mapping and conversion in one go? Compared to mapping separately via .map()
, there are two advantages:
Read on for an explanation of the second advantage.
The static method .from()
can optionally both map and convert between Typed Array types. Less can go wrong if you use that method.
To see why that is, let us first convert a Typed Array to a Typed Array with a higher precision. If we use .from()
to map, the result is automatically correct. Otherwise, you must first convert and then map.
const typedArray = Int8Array.of(127, 126, 125);
assert.deepEqual(
Int16Array.from(typedArray, x => x * 2),
Int16Array.of(254, 252, 250));
assert.deepEqual(
Int16Array.from(typedArray).map(x => x * 2),
Int16Array.of(254, 252, 250)); // OK
assert.deepEqual(
Int16Array.from(typedArray.map(x => x * 2)),
Int16Array.of(-2, -4, -6)); // wrong
If we go from a Typed Array to a Typed Array with a lower precision, mapping via .from()
produces the correct result. Otherwise, we must first map and then convert.
assert.deepEqual(
Int8Array.from(Int16Array.of(254, 252, 250), x => x / 2),
Int8Array.of(127, 126, 125));
assert.deepEqual(
Int8Array.from(Int16Array.of(254, 252, 250).map(x => x / 2)),
Int8Array.of(127, 126, 125)); // OK
assert.deepEqual(
Int8Array.from(Int16Array.of(254, 252, 250)).map(x => x / 2),
Int8Array.of(-1, -2, -3)); // wrong
The problem is that, if we map via .map()
, then input type and output type are the same. In contrast, .from()
goes from an arbitrary input type to an output type that you specify via its receiver.
Typed Arrays are iterable. That means that you can use the for-of
loop and other iteration-based mechanisms:
const ui8 = Uint8Array.of(0, 1, 2);
for (const byte of ui8) {
console.log(byte);
}
// Output:
// 0
// 1
// 2
ArrayBuffers and DataViews are not iterable.
Typed Arrays are much like normal Arrays: they have a .length
, elements can be accessed via the bracket operator []
and they have most of the standard Array methods. They differ from normal Arrays in the following ways:
Typed Arrays have buffers. The elements of a Typed Array ta
are not stored in ta
, they are stored in an associated ArrayBuffer that can be accessed via ta.buffer
:
Typed Arrays are initialized with zeros:
new Array(4)
creates a normal Array without any elements. It only has 4 holes (indices less than the .length
that have no associated elements).new Uint8Array(4)
creates a Typed Array whose 4 elements are all 0.All of the elements of a Typed Array have the same type:
Setting elements converts values to that type.
Getting elements returns numbers.
The .length
of a Typed Array is derived from its ArrayBuffer and never changes (unless you switch to a different ArrayBuffer).
Normal Arrays can have holes; Typed Arrays can’t.
To convert a normal Array to a Typed Array, you pass it to a Typed Array constructor (which accepts Array-like objects and Typed Arrays) or to «ElementType»Array.from()
(which accepts iterables and Array-like objects). For example:
const ta1 = new Uint8Array([0, 1, 2]);
const ta2 = Uint8Array.from([0, 1, 2]);
assert.deepEqual(ta1, ta2);
To convert a Typed Array to a normal Array, you can use spreading or Array.from()
(because Typed Arrays are iterable):
assert.deepEqual(
[...Uint8Array.of(0, 1, 2)], [0, 1, 2] );
assert.deepEqual(
Array.from(Uint8Array.of(0, 1, 2)), [0, 1, 2] );
Typed Arrays don’t have a method .concat()
, like normal Arrays do. The work-around is to use their overloaded method .set()
:
It copies the existing typedArray
or arrayLike
into the receiver, at index offset
. TypedArray
is a fictitious abstract superclass of all concrete Typed Array classes.
The following function uses that method to copy zero or more Typed Arrays (or Array-like objects) into an instance of resultConstructor
:
function concatenate(resultConstructor, ...arrays) {
let totalLength = 0;
for (const arr of arrays) {
totalLength += arr.length;
}
const result = new resultConstructor(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
assert.deepEqual(
concatenate(Uint8Array, Uint8Array.of(1, 2), [3, 4]),
Uint8Array.of(1, 2, 3, 4));
In preparation for the quick references on ArrayBuffers, Typed Arrays and DataViews, we need learn the differences between indices and offsets:
Indices for the bracket operator [ ]
: You can only use non-negative indices (starting at 0).
In normal Arrays, writing to negative indices creates properties:
In Typed Arrays, writing to negative indices is ignored:
Indices for methods of ArrayBuffers, Typed Arrays and DataViews: Every index can be negative. If it is, it is added to the length of the entity, to produce the actual index. Therefore, -1
refers to the last element, -2
to the second-last, etc. Methods of normal Arrays work the same way.
Offsets passed to methods of Typed Arrays and DataViews: must be non-negative. For example:
Whether a parameter is an index or an offset can only be determined by looking at documentation; there is no simple rule.
ArrayBuffers store binary data, which is meant to be accessed via Typed Arrays and DataViews.
new ArrayBuffer()
The type signature of the constructor is:
Invoking this constructor via new
creates an instance whose capacity is length
bytes. Each of those bytes is initially 0.
You can’t change the length of an ArrayBuffer, you can only create a new one with a different length.
ArrayBuffer
ArrayBuffer.isView(arg: any)
Returns true
if arg
is an object and a view for an ArrayBuffer (i.e., if it is a Typed Array or a DataView).
ArrayBuffer.prototype
get .byteLength(): number
Returns the capacity of this ArrayBuffer in bytes.
.slice(startIndex: number, endIndex=this.byteLength)
Creates a new ArrayBuffer that contains the bytes of this ArrayBuffer whose indices are greater than or equal to startIndex
and less than endIndex
. start
and endIndex
can be negative (see §29.4 “Quick references: indices vs. offsets”).
The properties of the various Typed Array objects are introduced in two steps:
TypedArray
: First, we look at the abstract superclass of all Typed Array classes (which was shown in the class diagram at the beginning of this chapter). I’m calling that superclass TypedArray
, but it is not directly accessible from JavaScript. TypedArray.prototype
houses all methods of Typed Arrays.«ElementType»Array
: The concrete Typed Array classes are called Uint8Array
, Int16Array
, Float32Array
, etc. These are the classes that you use via new
, .of
and .from()
.TypedArray<T>
Both static TypedArray
methods are inherited by its subclasses (Uint8Array
etc.). TypedArray
is abstract. Therefore, you always use these methods via the subclasses, which are concrete and can have direct instances.
.from<S>(source: Iterable<S>|ArrayLike<S>, mapfn?: S => T, thisArg?: any) : instanceof this
Converts an iterable (including Arrays and Typed Arrays) or an Array-like object to an instance of this
(instanceof this
is my invention, to express that fact).
The optional mapfn
lets you transform the elements of source
before they become elements of the result.
.of(...items: number[]): instanceof this
Creates a new instance of this
whose elements are items
(coerced to the element type).
TypedArray<T>.prototype
Indices accepted by Typed Array methods can be negative (they work like traditional Array methods that way). Offsets must be non-negative. For details, see §29.4 “Quick references: indices vs. offsets”.
The following properties are specific to Typed Arrays; normal Arrays don’t have them:
get .buffer(): ArrayBuffer
Returns the buffer backing this Typed Array.
get .length(): number
Returns the length in elements of this Typed Array’s buffer.
get .byteLength(): number
Returns the size in bytes of this Typed Array’s buffer.
get .byteOffset(): number
Returns the offset where this Typed Array “starts” inside its ArrayBuffer.
.set(typedArray: TypedArray, offset=0): void
.set(arrayLike: ArrayLike<number>, offset=0): void
Copies all elements of the first parameter to this Typed Array. The element at index 0 of the parameter is written to index offset
of this Typed Array (etc.). For more information on Array-like objects, consult §28.4 “Array-like objects”.
.subarray(startIndex=0, endIndex=this.length): TypedArray<T>
Returns a new Typed Array that has the same buffer as this Typed Array, but a (generally) smaller range. If startIndex
is non-negative then the first element of the resulting Typed Array is this[startIndex]
, the second this[startIndex+1]
(etc.). If startIndex
in negative, it is converted appropriately.
The following methods are basically the same as the methods of normal Arrays:
.copyWithin(target: number, start: number, end=this.length): this
[W, ES6].entries(): Iterable<[number, T]>
[R, ES6].every(callback: (value: T, index: number, array: TypedArray<T>) => boolean, thisArg?: any): boolean
[R, ES5].fill(value: T, start=0, end=this.length): this
[W, ES6].filter(callback: (value: T, index: number, array: TypedArray<T>) => any, thisArg?: any): T[]
[R, ES5].find(predicate: (value: T, index: number, obj: T[]) => boolean, thisArg?: any): T | undefined
[R, ES6].findIndex(predicate: (value: T, index: number, obj: T[]) => boolean, thisArg?: any): number
[R, ES6].forEach(callback: (value: T, index: number, array: TypedArray<T>) => void, thisArg?: any): void
[R, ES5].includes(searchElement: T, fromIndex=0): boolean
[R, ES2016].indexOf(searchElement: T, fromIndex=0): number
[R, ES5].join(separator = ','): string
[R, ES1].keys(): Iterable<number>
[R, ES6].lastIndexOf(searchElement: T, fromIndex=this.length-1): number
[R, ES5].map<U>(mapFunc: (value: T, index: number, array: TypedArray<T>) => U, thisArg?: any): U[]
[R, ES5].reduce<U>(callback: (accumulator: U, element: T, index: number, array: T[]) => U, init?: U): U
[R, ES5].reduceRight<U>(callback: (accumulator: U, element: T, index: number, array: T[]) => U, init?: U): U
[R, ES5].reverse(): this
[W, ES1].slice(start=0, end=this.length): T[]
[R, ES3].some(callback: (value: T, index: number, array: TypedArray<T>) => boolean, thisArg?: any): boolean
[R, ES5].sort(compareFunc?: (a: T, b: T) => number): this
[W, ES1].toString(): string
[R, ES1].values(): Iterable<number>
[R, ES6]For details on how these methods work, please consult §28.12.3 “Methods of Array<T>.prototype
”.
new «ElementType»Array()
Each Typed Array constructor has a name that follows the pattern «ElementType»Array
, where «ElementType»
is one of the element types in the table at the beginning. That means that there are 9 constructors for Typed Arrays:
Float32Array
, Float64Array
Int8Array
, Int16Array
, Int32Array
Uint8Array
, Uint8ClampedArray
, Uint16Array
, Uint32Array
Each constructor has four overloaded versions – it behaves differently depending on how many arguments it receives and what their types are:
new «ElementType»Array(buffer: ArrayBuffer, byteOffset=0, length=0)
Creates a new «ElementType»Array
whose buffer is buffer
. It starts accessing the buffer at the given byteOffset
and will have the given length
. Note that length
counts elements of the Typed Array (with 1–8 bytes each), not bytes.
new «ElementType»Array(length=0)
Creates a new «ElementType»Array
with the given length
and the appropriate buffer. The buffer’s size in bytes is:
new «ElementType»Array(source: TypedArray)
Creates a new instance of «ElementType»Array
whose elements have the same values as the elements of source
, but coerced to ElementType
.
new «ElementType»Array(source: ArrayLike<number>)
Creates a new instance of «ElementType»Array
whose elements have the same values as the elements of source
, but coerced to ElementType
. For more information on Array-like objects, consult §28.4 “Array-like objects”.
«ElementType»Array
«ElementType»Array.BYTES_PER_ELEMENT: number
Counts how many bytes are needed to store a single element:
«ElementType»Array.prototype
.BYTES_PER_ELEMENT: number
The same as «ElementType»Array.BYTES_PER_ELEMENT
.
new DataView()
new DataView(buffer: ArrayBuffer, byteOffset=0, byteLength=buffer.byteLength-byteOffset)
buffer
. By default, the new DataView can access all of buffer
. The last two parameters allow you to change that.DataView.prototype
In the remainder of this section, «ElementType»
refers to either:
Float32
, Float64
Int8
, Int16
, Int32
Uint8
, Uint16
, Uint32
These are the properties of DataView.prototype
:
get .buffer()
Returns the ArrayBuffer of this DataView.
get .byteLength()
Returns how many bytes can be accessed by this DataView.
get .byteOffset()
Returns at which offset this DataView starts accessing the bytes in its buffer.
.get«ElementType»(byteOffset: number, littleEndian=false)
Reads a value from the buffer of this DataView.
.set«ElementType»(byteOffset: number, value: number, littleEndian=false)
Writes value
to the buffer of this DataView.