JavaScript Data Structures - Typed Arrays I
Written by Ian Elliot   
Thursday, 28 February 2019
Article Index
JavaScript Data Structures - Typed Arrays I
Operators
Block Copy

Operators

If you create an array of a given type then the data stored is of that type. However, the arithmetic and other operators that you might apply to an array element perform the usual JavaScript operations after type conversion.

This is very reasonable, but it can also be confusing. For example, what would you expect the result of 

bytes[0]=0xFF; 
console.log(~bytes[0]);

to be?

The "~" is a bitwise not (see JavaScript Bit Manipulation is you have forgotten how it works) and so the byte 11111111 should be converted into 00000000 and the result should be zero. However it is actually -256. 

How can this be? 

The answer is that all JavaScript bitwise operations work with a 16-bit integer and so the value that the not operates on is 

0000000011111111

and when you apply the bitwise not you get

1111111100000000

which is then treated as a 16-bit signed integer and converted to a standard 64-bit float before being printed, which gives the -256. 

Notice that in this case the high order bits are lost if you assign the operation back to an array element of type Uint8Array. That is:

bytes[0]=0xFF;
bytes[0]=~bytes[0] 
console.log(bytes[0]);

does give the expected result of zero, but only because the 16-bit result is truncated to 8-bits by the type conversion. 

The same is true of arithmetic when the value stored in the array will be converted into a 64-bit float. Notice that none of the integer nor floating point array types will lose precision in this conversion. What non-JavaScript frameworks that you pass a typed array to do with the values is another matter and you just have to investigate on a case-by-case basis.

datastruct

The ArrayBuffer

In many cases you can simply use typed arrays as described above but sometimes you need to do more sophisticated things. Every typed array makes use of an ArrayBuffer object to store its data. This is simply the block of memory that the typed array allocates to store its data and it doesn't have methods that allow you to access the data. To access the data you need a typed array which provides a view into the ArrayBuffer object. 

The reason for this two level approach is so that you can acquire data and only later determine how you want to treat it. It is even possible to use multiple views with a single ArrayBuffer so providing alternative interpretations of the data. Some API calls return an ArrayBuffer leaving it up to you how to set up a view to process the data.  

Think of the ArrayBuffer as just being the data storage and the view as being how to interpret the data. 

When you crate a typed array an ArrayBuffer object is automatically created big enough to store the array. You can retrieve a reference to the ArrayBuffer via the buffer property. 

For example

var buffer=bytes.buffer;

To associate a new view with an existing ArrayBuffer all you have to do is specify it within the constructor. For example:

var buffer= bytes.buffer;
var uint16=new Uint16Array(buffer); console.log(uint16.length);

In this case a 10 byte buffer is now viewed via unit16 as five 16-bit unsigned integers. You can see the advantages of this approach in that you can get to the individual high and low bytes of the 16-bit integer via the bytes array and the entire 16-bit integer via the unit16 array. This saves a lot of additional work that would be necessary if you needed to combine the high and low bytes as a special operation. 

If you want to do a lot of this sort of view swapping then you can create an instance of the ArrayBuffer directly. For example,

var buffer= new ArrayBuffer(10);

creates a buffer with ten bytes initialized to zero and 

var bytes=new Uint8Array(buffer);

creates an array of unsigned 8-bit integers using it.  You can also set another view into the same buffer using something like

var uint16=new Uint16Array(buffer); 

Notice that arrays that share the same ArrayBuffer really do share the same data. In our example if you store something in unit16[0] then you have modified what is stored in bytes[0] and bytes[1] which share the same location in the buffer. 

You can also specify an offset and a length for a view which determines exactly which part of the ArrayBuffer it uses. 

For example:

var uint16=new Uint16Array(buffer,5,2);

Specifies that the view starts in the ArrayBuffer at the sixth byte and creates just two 16-byte integers. In this case unint16[0] is the same storage location as bytes[5] and bytes[6] and unint16[1] is bytes[7] and bytes[8]. 

You can see that things could become very complicated with views sharing overlapping and non-overlapping portions of the ArrayBuffer but in general things are usually kept simple.



Last Updated ( Thursday, 28 February 2019 )