JavaScript Typed Arrays

Chapter 27 37 mins

Learning outcomes:

  1. What are buffers
  2. The ArrayBuffer interface
  3. What are views
  4. The DataView interface
  5. Quick example

Introduction

In the previous chapter, we unraveled the DataView API from crust-to-core, and saw how it's a means of interacting with a buffer.

Now in this chapter, we shall go over a big improvement to that methodical interface that eases the whole syntax to put stuff into and get stuff out of a buffer.

That is, typed arrays.

What are typed arrays?

To start off technical:

Typed arrays are array-like objects that can store numbers only of a given type.

Internally, they are simply syntactic sugar over the DataView interface.

Let's understand all this...

Typed arrays are essentially a simplified version of views.

Say you want to store the two numbers 30 and 60422 in uint16 format. With the generic DataView interface, the code to do so will look something like the following:

var buffer = new ArrayBuffer(4);
var view = new DataView(buffer);

view.setUint16(0, 30);
view.setUint16(2, 60422);

We have to first call ArrayBuffer() with an exact value for the byteLength argument to hold two uint16 numbers. This requires us to know the byte length of each uint16 number which is 2 - constituting a total of 4 bytes.

Then we have to create a view over this buffer and finally put the numbers in the right positions. The first number takes 2 bytes so it fills up byte offsets 0 and 1. The second number would therefore go at byte offset 2, and continue till byte offset 3.

Even the slightest mistake in this code can lead to gibberish in our buffer!

However, with typed arrays - Uint16Array in this case - the code is much simpler:

var typedArr = new Uint16Array();
typedArr[0] = 30;
typedArr[1] = 60422;

See how this resembles the syntax to create a normal JavaScript array, and that how easy it is to work with buffers in this way.

We don't have to worry about the byteLength argument of the ArrayBuffer() constructor or the byteOffset arguments of DataView's set methods - just put the individual numbers in increasing indexes of the typed array.

That's it!

What does 'array' mean?

The word 'arrays' in 'typed arrays' comes from the fact that they behave very similar to normal JavaScript arrays, in that:

  1. The individual numbers they store are referred to as elements of the array.
  2. The positions where the numbers are stored are referred to as indexes of the array.
  3. They also have a length property to indicate the total number of elements.

In addition to having indexed positions, typed arrays also have numerous methods available which resemble the ones defined for Array objects.

Examples include map(), sort(), forEach(), reduce(), reverse(), filter(), slice() and much more.

Not only this, but the syntax to create a typed array also resembles the one to create a normal Array object.

Altogether it sounds very sensible why we refer to these objects by the name 'array'.

What does 'typed' signify?

The term 'typed' in 'typed arrays' signifies the fact that these array-like objects are restricted to hold only a given type of a number.

If a typed array is meant to hold uint8 numbers, it can then only store uint8 numbers; if it's meant to store float64 numbers then it can only store float64 numbers; and so on and so forth.

In short, typed arrays are array-like objects that can accept only a given type of numeric data.

In the previous chapter on the DataView interface we saw all the types of numeric data in JavaScript i.e uint8, uint16, uint32, uint64, int8, int16, int32, int64, float32 and float64.
One crucial thing to remember here is that typed arrays don't introduce any new data types into JavaScript. The types just indicate what the typed array can store internally.

Native numbers in JavaScript are always IEEE-754 double-precision floating point numbers or big integers (using the BigInt class). Internally, each typed array class coerces these floating numbers into its respective numeric data type.

Creating typed arrays

Before creating a typed array, we first need to decide on the type of the numbers that we want to store within it.

For example, if we want to store byte values, we would go with the uint8 format; if we want to store floating-point numbers to the max degree of precision we would go with the double-precision float64 format; and so on.

Each numeric type has its own dedicated class. Following is the complete list of all typed array classes in JavaScript:

  1. Uint8Array - interface for unsigned 8-bit integers
  2. Uint16Array - interface for unsigned 16-bit integers
  3. Uint32Array - interface for unsigned 32-bit integers
  4. BigUint64Array - interface for unsigned 64-bit integers
  5. Uint8ClampedArray - interface for unsigned 8-bit integers, where overflowing values are coerced differently than in Uint8Array.
  6. Int8Array - interface for signed 8-bit integers
  7. Int16Array - interface for signed 16-bit integers
  8. Int32Array - interface for signed 32-bit integers
  9. BigInt64Array - interface for signed 64-bit integers
  10. Float32Array - interface for single-precision 32-bit floating point numbers
  11. Float64Array - interface for double-precision 64-bit floating point numbers

Apart from Uint8ClampedArray, all other classes might sound quite familiar to you given that you've read the previous chapter on DataView.

For a given interface, the term preceding the word 'Array' indicates its numeric type.

For example, Uint8Array with the word 'Array' trimmed off is Uint8, which means that it operates on uint8 numbers.
The beginning letters of all the classes above are uppercased - this is because they are constructors and conventionally constructors have uppercase beginning alphabets! Remember this?

The syntax to create a typed array is the same across all these classes. Likewise following we go with a generalised name TypedArray to refer to any one of these classes.

Shown below are a couple of ways to create a typed array:

var t = new TypedArray(arrayLike);

We can either pass an array-like object such as an Array, Set or a Map. All its individual elements will be stored as individual numbers in the typed array.

var t = new TypedArray(iterable);

Or we can even pass an iterable object, where once again, all the individual elements will be stored in the typed array.

var t = new TypedArray(length);

Or simply, we can just pass a number, which will be taken as the length of the typed array. The length specifies the number of elements stored in a typed array.

Following we demonstrate all these three ways one by one.

Say we want to store the three numbers 10, 78 and 122 in uint8 format i.e using the Uint8Array class.

In the first way, we'll simply pass an array of the numbers to the constructor, shown as follows:

var u = new Uint8Array([10, 78, 122]);

In the second way, we'll put all the numbers in an iterable object (an object that implements the iterable protocol). For simplicity, we'll use a generator function to create an iterable object:

function* gen() {
    yield 10;
    yield 78;
    yield 122;
}

var u = new Uint8Array(gen());

In the third and last way, we'll first pass in the number of elements to be stored in the typed array. After that we'll store the numbers one by one using bracket notation (just as we do with normal JavaScript arrays):

var u = new Uint8Array(3);
u[0] = 10;
u[1] = 78;
u[2] = 122; 

Regardless of the method used to create a typed array, retrieving data from it is the same - use bracket notation.

Consider the code below:

console.log(u[0]); // 10
console.log(u[1]); // 78
console.log(u[2]); // 122

We retrieve each number stored in the Uint8Array object u, by referring to its index.

Working with typed arrays is 99.9% similar to working with actual arrays in JavaScript.

Unsigned integers

As we've seen above, the five typed array classes that operate on unsigned integers are Uint8Array, Uint8ClampedArray Uint16Array, Uint32Array, BigUint64Array.

Uint8ClampedArray is a new kind of a view specifically made to aid in canvas processing. We'll see it very shortly below.

Let's start from the very first Uint8Array class.

Uint8Array

The Uint8Array typed array class, as the name implies works on uint8 numbers, each of which occupy one byte.

Thereby each element of a Uint8Array object is one byte long which further implies that:

The length and byteLength properties of a Uint8Array are equal to one another.

In terms of code, this can be represented as follows:

Uint8ArrayObject.length === Uint8ArrayObject.byteLength

Uint8ArrayObject is assumed to be a Uint8Array object.

To further clarify this, if we have four elements in a Uint8Array object, its length will be 4 as we have four elements, and similarly its byteLength will also be 4 since altogether the numbers occupy four bytes.

Consider the following illustration:

var uint8Array = new Uint8Array(3);

uint8Array[0] = 15;
uint8Array[1] = 27;
uint8Array[2] = 199;

First, we instantiate an object out of the Uint8Array() constructor. Then we fill it just like we would be filling up a normal array - use the bracket notation and then assign given values to individual indexes.

Now to get all the stored values, the syntax would once again match the array syntax:

console.log(uint8Array[0]); // 15
console.log(uint8Array[1]); // 27
console.log(uint8Array[2]); // 199

And in this way you can clearly see how easy it's to fill up data in a buffer using typed arrays.

Uint16Array

The Uint16Array class operates on uint16 numbers, each of which occupies two bytes.

This implies that each element of a Uint16Array object consumes two bytes, which further means that:

The byteLength of a Uint16Array is twice its length.

For example, if a Uint16Array object has three elements, its length will be 3, while its byteLength will be 6.

In terms of code:

Uint16ArrayObject.byteLength === (Uint16ArrayObject.length * 2)

Let's consider an example.

Below we store the two numbers 309 and 2078 in uint16 format using the Uint16Array typed array class:

var uint16Array = new Uint16Array(2);
                                
uint16Array[0] = 309;
uint16Array[1] = 2078;

Let's inspect some properties of this uint16Array object and also retrieve both its elements:

console.log(uint16Array.length); // 2
console.log(uint16Array.byteLength); // 4

console.log(uint16Array[0]); // 309
console.log(uint16Array[1]); // 2078

As expected, length is 2 and byteLength is 4, and obviously the elements are the same we put in!

Simple and concise - isn't it?

Uint32Array

The Uint32Array class operates on uint32 numbers, each of which occupies four bytes.

This implies that each element of a Uint32Array object take up four bytes, which simply means that:

The byteLength of a Uint32Array is four times its length.

For example, if a Uint32Array object has five elements, its length will be 5, while its byteLength will be 20.

In terms of code:

Uint32ArrayObject.byteLength === (Uint32ArrayObject.length * 4)

An example is worth the discussion.

Say you want to store the two numbers 75600 and 968550 in uint32 format. Using the respective typed array class, the code to accomplish this will look something like the following:

var uint32Array = new Uint32Array(2);
                                
uint32Array[0] = 75600;
uint32Array[1] = 968550;

Below we inspect a couple of properties of this uint32Array object:

console.log(uint32Array.length); // 2
console.log(uint32Array.byteLength); // 8

console.log(uint32Array[0]); // 75600
console.log(uint32Array[1]); // 968550

As we'd expected, length is 2 and byteLength is 8, and obviously the elements are the same we put in!

BigUint64Array

The BigUint64Array class operates on uint64 numbers, each of which occupies eight bytes.

In simple words, this translates to the fact that:

The byteLength of a BigUint64Array is eight times its length.

For example, if a BigUint64Array object has six elements, its length will be 6, while its byteLength will be 48.

In terms of code:

BigUint64ArrayObject.byteLength === (BigUint64ArrayObject.length * 8)

Say you want to store the numbers 958668545033 and 34359738368 in uint64 format. The code to do so will resemble the snippet shown as follows:

var uint64Array = new BigUint64Array(2);
                                
uint64Array[0] = 958668545033n;
uint64Array[1] = 34359738368n;

Remember that, as the name implies, BigUint64Array expects you to input BigInt numbers, which are denoted with an ending n in literal form.

As before, below we inspect a couple of properties of this uint64Array typed array:

console.log(uint64Array.length); // 2
console.log(uint64Array.byteLength); // 16

console.log(uint64Array[0]); // 958668545033n
console.log(uint64Array[1]); // 34359738368n

The property length is 2 and byteLength is 16.

Uint8ClampedArray

Apart from these quite familiar classes, there yet exists one strange typed array class in JavaScript under this unsigned category - Uint8ClampedArray.

As can be understood from the name, this class also operates on uint8 numbers akin to the Uint8Array class.

However, the difference between the two is that in Uint8ClampedArray if input values exceed the range 0-255, they are rounded to either 0 or 255, which ever is closer, rather than being reduced modulo 256 as with Uint8Array.

The example below distinguishes between these two classes.

First let's see what happens when an overflowing number is stored in Uint8Array:

var u = new Uint8Array(1);

// put in an overflowing value
u[0] = 257;

// it's reduced modulo 256
console.log(u[0]); // 1
1

The range of uint8 numbers is 0-255 i.e the least number that we can store is 0, whereas the maximum is 255. Now, if we store a number that's beyond this range, it's reduced modulo 256.

In other words, the remainder of the number when divided by 256 is stored.

a mod b represents the remainder of the division a / b.

For the example above, when we store 257, the number 1 is stored internally. This is because the remainder when 257 is divided by 256 is 1.

Simple math!

Now consider the code below with a Uint8ClampedArray object:

var u = new Uint8ClampedArray(1);

// put in an overflowing value
u[0] = 257;

// it's rounded to the nearest value: 0 or 255
console.log(u[0]); // 255
255

In the clamped version, the overflowing value is simply rounded to the nearest of the two numbers 0 and 255. If the number is above 255, it's rounded down to 255; and similarly if it's less than 0, it's rounded up to 0.

For the example above, we put in the value 257 which is above 255, and therefore what gets stored internally is 255.

What will the following code log in the console?

var u = new Uint8ClampedArray(1);
u[0] = -150;

console.log(u[0]);
  • -150
  • 0
  • 106
Tie-breaking rules in Uint8ClampedArray differ from the ones used in Math.round(). The former rounds 'half to even', whereas the latter rounds 'half up'.

In other words, if we input 0.5 in Uint8ClampedArray, the value stored is 0 (which is even), whereas with Math.round() the value returned is 1.

Similarly, if we input 1.5 in Uint8ClampedArray the value stored is 2 (which is even), whereas with Math.round() the value stored is 2.

Signed integers

With the unsigned category all dealt with, it's now time to shed some light on the signed-integer category of typed arrays.

The classes we'll be discussing here are: Int8Array, Int16Array, Int32Array and finally BigInt64Array.

Int8Array

The Int8Array typed array class works on int8 numbers each of which occupies one byte.

This means that:

The length and byteLength properties of an Int8Array are equal to one another.

In general terms:

Int8ArrayObject.length === Int8ArrayObject.byteLength

Consider the code below:

var int8Array = new Int8Array(3);

int8Array[0] = -120;
int8Array[1] = 50;
int8Array[2] = 3;

console.log(int8Array.byteLength); // 3
console.log(int8Array.length); // 3

console.log(int8Array[0]); // -120
console.log(int8Array[1]); // 50
console.log(int8Array[2]); // 3

Three numbers are stored in the array, and then finally output.

Int16Array

The Int16Array class operates on int16 numbers, each of which occupies two bytes.

This means that:

The byteLength of a Int16Array is twice its length.

In simple terms:

Int16ArrayObject.byteLength === (Int16ArrayObject.length * 2)

Say we want to store the two numbers -3056 and -100 in int16 format. The following code accomplishes this:

var int16Array = new Int16Array(2);

int16Array[0] = -3056;
int16Array[1] = -100;

console.log(int16Array.length); // 2
console.log(int16Array.byteLength); // 4

console.log(int16Array[0]); // -3056
console.log(int16Array[1]); // -100

Since we've stored two numbers in the array, its length is 2 and its byteLength is 4.

Int32Array

The Int32Array class operates on int32 numbers, each of which occupies four bytes.

This means that:

The byteLength of a Uint32Array is four times its length.

In terms of code:

Int32ArrayObject.byteLength === (Int32ArrayObject.length * 4)

Say you want to store the four numbers -189645, -71643086, 68545 and -2 in int32 format. Following is a demonstration of how to do that using a typed array:

var int32Array = new Int32Array(4);

int32Array[0] = -189645;
int32Array[1] = -71643086;
int32Array[2] = 68545;
int32Array[3] = -2;

console.log(int32Array.length); // 4
console.log(int32Array.byteLength); // 16

console.log(int32Array[0]); // -189645
console.log(int32Array[1]); // -71643086
console.log(int32Array[2]); // 68545
console.log(int32Array[3]); // -2

As with the norms, length is 4 and byteLength is 16.

BigInt64Array

The BigInt64Array class operates on int64 numbers, each of which occupies eight bytes.

This means that:

The byteLength of a BigInt64Array is eight times its length.

In other words, we can write this as:

BigInt64ArrayObject.byteLength === (BigInt64ArrayObject.length * 8)

Say you want to store the numbers -289450006060048 and 8156792035 in int64 format:

var int64Array = new BigInt64Array(2);
                                
int64Array[0] = -289450006060048n;
int64Array[1] = 8156792035n;

console.log(int64Array.length); // 2
console.log(int64Array.byteLength); // 16

console.log(int64Array[0]); // -289450006060048n
console.log(int64Array[1]); // 8156792035n

The property length is 2 and byteLength is 16.

Over to the last category of numbers - floats.

Floating-point numbers

Floating-point numbers in JavaScript and most other programming languages and devices follow the IEE-754 format and are likewise divided into two categories: single-precision and double-precision.

Typed arrays also follow along the norms and provide facilities to work with both these numeric types.

Let's discover the details...

Float32Array

The Float32Array class operates on single-precision (32-bit) floating-point, or simply float32, numbers each of which takes up 4 bytes of memory.

And this obviously means that:

The byteLength of a Float32Array is four times its length.

In general terms:

Float32ArrayObject.byteLength === (Float32ArrayObject.length * 4)

Consider the example below.

Say you want to store the numbers -6.135 and Math.PI in float32 format. The code to do so is illustrated below:

var float32Array = new Float32Array(2);
                                
float32Array[0] = -6.135;
float32Array[1] = Math.PI;

console.log(float32Array.length); // 2
console.log(float32Array.byteLength); // 16

console.log(float32Array[0]); // -6.135000228881836
console.log(float32Array[1]); // 3.1415927410125732

Since two numbers are stored, length is 2 whereas byteLength is 8.

One thing you'll notice here is the fact that we input the numbers -6.135 and Math.PI in the typed array, but what gets stored is -6.135000228881836 and 3.1415927410125732 i.e very slightly changed numbers.

This happens due to the fact that we're using the single-precision format, and not the double-precision format which is what JavaScript uses internally to represent all numbers.

Even if we were using a double-precision typed array class, then too can strange numbers show up in comparison to the ones we input. Remember, this is floating-point arithmetic - you shouldn't be expecting perfect precision for most numbers!

Float64Array

The Float64Array class operates on double-precision (64-bit) floating-point, or simply float64, numbers each of which takes up 8 bytes of memory.

In other words:

The byteLength of a Float64Array is eight times its length.

In general terms:

Float64ArrayObject.byteLength === (Float64ArrayObject.length * 8)

Consider the following example.

Say you want to store the numbers 4 ** 201 and -0.000015652 in float64 format. The code to do so will look something as follows if done via the Float64Array class:

var float64Array = new Float64Array(2);
                                
float64Array[0] = 4 ** 201;
float64Array[1] = -0.000015652;

console.log(float64Array.length); // 2
console.log(float64Array.byteLength); // 16

console.log(float64Array[0]); // 1.0328999512347634e+121
console.log(float64Array[1]); // -0.000015652

Since two numbers are stored, length is 2 whereas byteLength is 16.

Since the format of storage, this time, is the double-precision format, whatever we store in the typed array is exactly how it'll be represented in the console. What this means is shown below:

console.log(float64Array[0] === 4 ** 201); // true
console.log(float64Array[1] === -0.000015652); // true

The reason is simply because JavaScript's native number representation format is also the double-precision floating-point format.

And with this last class explored to the core, we are done with all the typed array classes in JavaScript.

Now what's left to discover are a handful of methods of typed arrays that further sweeten the syntactic sugar they apply over the generic DataView interface!

Typed array methods

As stated before, typed arrays share many aspects with normal JavaScript arrays, including many of their methods.

In this section we shall go over some of the most useful methods of typed arrays stated as follows: indexOf(), sort(), reverse(), slice(), subarray().

indexOf()

The indexOf() method can be used to find a given number in a typed array.

If found, it returns the index of the number in the array, or else the value -1.

Consider the code below:

var u = new Uint8Array([16, 41]);

console.log(u.indexOf(30)); // -1
console.log(u.indexOf(41)); // 1

First we define a typed array u and then search for the number 30 within it. Since there is no such number, indexOf() returns -1.

Afterwards, we search for 41 and since it does indeed exist, indexOf() returns 1.

As with arrays, typed arrays also have a lastIndexOf() method.

sort()

Sorting is a real challenge when it's left to be done manually. Fortunately, we don't have to face this challenge in typed arrays - that's because they provide us with a sort() method!

The method sort() sorts the numbers in a given typed array, in increasing order.

To shift to decreasing order, one has to pass in a callback function just like in the sort() method on normal arrays.

Remember that sort() performs the sorting in place i.e the original typed array is rearranged.

Consider the code below:

var u = new Int32Array([-35, 189, 10, 1, -986665, -1, 67700]);
u.sort();

console.log(u);
Int32Array(7) [ -986665, -35, -1, 1, 10, 189, 67700 ]

reverse()

As the name suggests, reverse() reverses the order of numbers in a given typed array.

Just like sort(), the method reverse() also changes the order of numbers in place.

Following is an example:

var u = new Uint16Array([60, 30, 1, 15, 189, 10000]);
u.reverse();

console.log(u);
Uint16Array(6) [ 10000, 189, 15, 1, 30, 60 ]

slice()

The method slice() is really handy if we only want to work with a given portion of a typed array.

TypedArray.slice([start [, end]])

It takes in two optional arguments:

  1. start specifies the index where to start the slicing. Default is 0.
  2. end specifies the position where to end the slicing (exclusive). Default is the length of the array.

One important thing to remember about the slice() method of typed arrays is that it creates a new buffer to hold the sliced array's contents.

Consider the code below:

var u = new Uint8Array([1, 6, 8, 166, 100, 60, 37]);
var u2 = u.slice(2, 5);

console.log(u2);
Uint8Array(3) [ 8, 166, 100 ]

First we create a typed array u with five elements, and then another typed array u2 by slicing u from index 2 to 5 (exclusive).

u2 is not a limited view over u's buffer, but is rather a view over a totally new buffer. This can be confirmed by the following statement:

console.log(u.buffer === u2.buffer); // false

Both u and u2 have their own separate buffers.

If you want to make a slice that refers to the same buffer as the one for the original typed array, then you ought to use the subarray(). method

subarray()

The subarray() method syntactically works exactly like slice(), but internally has its own purpose.

It creates a subarray out of a given typed array, whose buffer is the same as that of the typed array.

Let's review the same example above using subarray():

var u = new Uint8Array([1, 6, 8, 166, 100, 60, 37]);
var u2 = u.subarray(2, 5);

console.log(u2);

Uptil this point there doesn't seem any difference between subarray() and slice(), but after the following statement it all becomes clear:

console.log(u.buffer === u2.buffer); // true

The buffers of both u and u2 are the same.

In short:

slice() returns a sliced portion of a typed array with an entirely separate buffer.

subarray() returns a sliced portion of a typed array with the same buffer as for the original typed array.

Even more methods exist, and it's recommended that you experiment around with them to get yourself more familiar with typed arrays. After all, doing so will also make you more familiar with the powerful Array interface!