In the previous article, we got familiar with JavaScript buffers. The lowest possible implementation of a buffer in JavaScript is the ArrayBuffer
class. This class is read-only, meaning we don't have any API to write data inside the buffer.
To change buffer content, we have two options: data views and typed arrays. In this article, we'll talk about data views, typed arrays, and their differences.
Data views
Data view is the abstraction that allows you to change the content of a buffer. It acts like a key to a locked door. You can't go inside without having a key, but once you have it, feel free to come in.
The same is true for the relation between buffer and data view. Data view is like a key that allows you to write data inside a buffer. Data view is represented by the DataView
class in JavaScript.
It holds the key to the underlying ArrayBuffer
instance where data is stored.
You can see example in the code snippet below.
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
console.log(view.buffer.byteLength); // Prints 8
view.buffer.resize(5);
console.log(buffer.byteLength); // Prints 5
Notice that the buffer
size changed after the buffer that view references was resized. This means that the view references the same buffer that we passed into the DataView
class constructor.
When we want to write into a buffer using data view, we call one of the methods that set different types of values.
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.setUint8(0, 0x0F);
console.log(view.getUint8(0)); // Prints 15
When we call the setUnit8(0, 0xF)
method, it writes the value of 0x0F
inside the buffer using 1 byte for it.
Typed arrays
The second option to change buffer data is typed arrays. Don't be confused by the array part. Typed arrays aren't arrays but array-like structures.
What it means is when you use Array.isArray
function and pass a typed array inside it returns false
. At the same time, typed arrays provide many array-like methods: at
, fill
, map
, reduce
, etc. That's why typed arrays are called array-like structures.
In JavaScript, we have many different typed arrays such as:
Uint8Array
Int8Array
Uint16Array
Int16Array
Float32Array
Float64Array
And others. Every typed array provides the ability to modify the underlying buffer. But what is the difference between all those typed arrays and when to use which?
To understand this topic better, I highly recommend reading the article about different types of text encoding schemes in JavaScript. Knowing text encoding schemes makes it much easier to understand the topic of typed arrays.
There are 3 main characteristics of the typed arrays you should know about:
Signed and unsigned typed arrays
Number of bytes required to store a single value inside a typed array
Type of value that a typed array can store
Let's look at each of them in more detail.
Signed and unsigned typed arrays
The meaning of the data that the buffer contains heavily depends on the context in which it is interpreted.
const signedArray = new Int8Array([0xFF, 0x75, 0x6E]);
const unsignedArray = new Uint8Array([0xFF, 0x75, 0x6E]);
// Prints Int8Array {0: -1, 1: 117, 2: 110}
console.log(signedArray);
// Prints Uint8Array {0: 255, 1: 117, 2: 110}
console.log(unsignedArray);
Quick note, if you're not comfortable with those 0xFF
and other hexadecimal values, check out the article in which we dive into the hexadecimal numeric system in JavaScript.
You see that both structures are almost identical. The only difference is that the first element with a value of OxFF
in signed arrays is treated as -1
, and in unsigned arrays, it is 255
. That's the whole point of signed vs. unsigned, the range of possible values is different.
Signed arrays can contain both negative and positive values. Unsigned array can only contain positive values. The specific range of values is dictated by how many bytes are used to store a single item.
Number of bytes required to store a value
Typed arrays store values differently. To be more precise, each typed array allocates a different number of bytes to store a single item.
For the same piece of data, different typed arrays allocate different number of bytes.
To better understand when to use which typed array, you have to understand the data you're working with.
If the data is not expected to exceed the range from 0
to 255
, then it is better to use Uint8Array
. It operates in this exact range, and it is one of the most memory-efficient type arrays.
If you expect to work with data where some elements can have a value higher than 255
it is better to use Uint16Array
or Uint32Array
. If you try to write a value higher than 255
into an 8-bit unsigned array, the value will be cut at 255
.
const u8Array = new Uint8Array([0xFFF]);
const u16Array = new Uint16Array([0xFFF]);
console.log(u8Array) // Prints Uint8Array {0: 255}
console.log(u16Array) // Prints Uint16Array {0: 4095}
Different value types
Different typed arrays are meant to store different datatypes. So far, we've talked only about integer typed arrays. The integer, in this case, is a number without any floating point numbers like 3
or 120
. But what if you want to store values with floating point numbers like 3.14
?
const u8array = new Uint8Array([3.14]);
// Prints Uint8Array {0: 3}
console.log(u8Array);
The part after the floating point gets cut, and you only see 3
. To store floating point numbers, you have to use specific typed arrays Float32Array
or Float64Array
.
const float32array = new Float32Array([3.14]);
// Prints Float32Array {0: 3.140000104904175}
console.log(float32array);
Now you can see the numbers after the floating point.
What is the difference between a data view and a typed array
It looks like we have 2 things that are doing basically the same. Well, it is partially true because both DataView
and TypedArray
are buffer views.
As you can see, using both abstractions gives you the same power to edit buffers' content. At the same time, there are differences in how they're editing buffer content.
Scope of manipulated data
When dealing with typed arrays, we always work with a specific type of data. For example, using Uint8Array
means that we only work with data range between 0
and 255
.
Typed arrays are quite convenient when we work only with a single type of data per buffer. However, this is not the case if we want to write different types of data inside a buffer. It is still possible to use typed arrays for it, but the code becomes more tedious.
const buffer = new ArrayBuffer(10);
const u8Array = new Uint8Array(buffer, 0, 3);
const u16Array = new Uint16Array(buffer, 3, 3);
u8Array.fill(0xFF);
u16Array.fill(0xFFF);
// Prints Uint8Array {0: 255, 1: 255, 2: 255}
console.log(u8Array);
// Prints Uint16Array {0: 4095, 1: 4095, 2: 4095}
console.log(u16Array);
Using DataView
is more convenient in such cases because you're not constrained by any particular type of data.
const buffer = new ArrayBuffer(10);
const view = new DataView(buffer);
view.setUint8(0, 0xFF);
view.setUint16(1, 0xFFFF);
You don't need to work with many abstractions and variables. ArrayBuffer
and DataView
are enough for this task.
The difference in how a data view and a typed array treat Endianness
If you're not familiar with Endianness, check out this section of the article on bits and bytes in JavaScript.
By default, typed arrays only work with the native Endianness of your platform. For example, if you have little-endian hardware, then typed arrays use little-endian byte order when working with typed arrays.
Most modern machines use little-endian bytes order. However, it doesn't mean there is no place for big-endian.
In this StackOverflow question user faces the problem where a big-endian WebGL file is interpreted in little-endian using typed arrays. It happens because the native system byte order is little-endian.
To solve the issues, we can use DataView
. Data views allow you to change the way how the view treats the buffer byte order.
const buffer = new ArrayBuffer(10);
const view = new DataView(buffer);
view.setUint16(0, 0xFFF, true);
When we pass true
as the third argument to DataView
instance methods, it means that we intend to store the data in little-endian byte order. By default, DataView
uses big-endian byte order to store the data.
Notice that methods which set 8-bit values like setUint8
and setInt8
don't have this flag. The reason is simple, there is only 1 byte, and the byte order is irrelevant in this case.
Conclusion
Views allow you to change the content of a buffer. There are two types of views: typed arrays and data views.
Data view is represented by the DataView
class in JavaScript. There is only one class when it comes to data view, and through this class, we can set many different values for the buffer.
On the other hand, typed arrays are represented by multiple different class that differs by:
Signed and unsigned type
Number of bytes required to store a single buffer item
Type of data stored like integers, floats, big integers
The difference between data view and typed array lies in how flexible you want to be when working with buffers. If you want to primarily work with a single type of data and ok without having much flexibility, then typed arrays are your choice.
On the other hand, if you need a more flexible solution or you want to work with different types of data inside a single buffer, then data views are the way to go.