JavaScript Typed Arrays
Learning outcomes:
- What are buffers
- The
ArrayBuffer
interface - What are views
- The
DataView
interface - 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:
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:
- The individual numbers they store are referred to as elements of the array.
- The positions where the numbers are stored are referred to as indexes of the array.
- 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.
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.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:
Uint8Array
- interface for unsigned 8-bit integersUint16Array
- interface for unsigned 16-bit integersUint32Array
- interface for unsigned 32-bit integersBigUint64Array
- interface for unsigned 64-bit integersUint8ClampedArray
- interface for unsigned 8-bit integers, where overflowing values are coerced differently than inUint8Array
.Int8Array
- interface for signed 8-bit integersInt16Array
- interface for signed 16-bit integersInt32Array
- interface for signed 32-bit integersBigInt64Array
- interface for signed 64-bit integersFloat32Array
- interface for single-precision 32-bit floating point numbersFloat64Array
- 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
.
'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 syntax to create a typed array is the same across all these classes. Likewise following we go with a generalized 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.
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:
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:
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:
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:
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
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
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 does the following code log?
var u = new Uint8ClampedArray(1);
u[0] = -150;
console.log(u[0]);
-150
0
106
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:
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:
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:
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:
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:
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.
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:
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
.
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.
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);
reverse()
As the name suggests, reverse()
reverses the order of numbers in a given typed array.
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);
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:
start
specifies the index where to start the slicing. Default is0
.end
specifies the position where to end the slicing (exclusive). Default is thelength
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);
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!
Spread the word
Think that the content was awesome? Share it with your friends!
Join the community
Can't understand something related to the content? Get help from the community.