Collection types

An array and an object have been the two main types that JavaScript developers have used for quite some time. But, we now have two other collection types that help us do some things that we used to use these other types for. These are set and map. A set is an unordered collection of unique items. What this means is that if we try to put something in the set that is already there, we will notice that we only have a single item. We can easily simulate a set with an array like so:

const set = function(...items) {
this._arr = [...items];
this.add = function(item) {
if( this._arr.includes(item) ) return false;
this._arr.push(item);
return true;
}
this.has = function(item) {
return this._arr.includes(item);
}
this.values = function() {
return this._arr;
}
this.clear = function() {
this._arr = [];
}
}

Since we now have the set system, we can just use that API. We also get access to the for of loop since the set is an iterable item (we can also use the next syntax if we get the iterator attached to the set). Sets also have an advantage of faster read access over arrays when we get into large datasets. The following example illustrates this point:

const data = new Array(10000000);
for(let i = 0; i < data.length; i++) {
data[i] = i;
}
const setData = new Set();
for(let i = 0; i < data.length; i++) {
setData.add(i);
}
data.includes(5000000);
setData.has(5000000);

While the set takes a bit longer to create, when it comes to looking for items or even grabbing them, the set will perform nearly 100 times faster than an array. This is mostly due to the way the array has to look items up. Since an array is purely linear, it has to go through each element to check, whereas the set is a simple constant time check.

A set can be implemented in different ways depending on the engine. A set in the V8 engine is built utilizing hash dictionaries for the lookup. We will not go over the internals of this, but essentially, the lookup time is considered constant, or O(1), for computer science folk, whereas the array lookup time is linear, or O(n).

On top of the set, we also have maps. We can look at these and see them as just objects, but they have a couple of nice properties:

  • First, we can use any value as the key, even an object. This can be great for adding other data that we do not want to tie directly to the object (private values come to mind).
  • In addition to this, maps are also iterable, so we can utilize the for of loop just like a set.
  • Finally, a map can give us performance benefits over a plain old object in the cases of large datasets and when the keys are the same types and so are the values.

The following example highlights many of the areas where maps are generally better than plain objects and where objects used to be used:

const map = new Map();
for(let i = 0; i < 10000; i++) {
map.set(`${i}item`, i);
}
map.forEach((val, key) => console.log(val));
map.size();
map.has('0item');
map.clear();

On top of these two items, we also have weak versions of them. The weak versions have one major limitation: the values have to be objects. This makes sense once we understand what WeakSet and WeakMap do. They weakly store the reference to the items. This means that while the items they have stored are around, we can perform the methods that these interfaces give us. Once the garbage collector decides to collect them, the references will be removed from the weak versions. We may be wondering, why would we use these?

For a WeakMap, there are a few use cases:

  • First, if we do not have private variables, we can utilize WeakMap to store values on the object without actually attaching the property to them. Now, when the object is eventually garbage collected, so is this private reference.
  • We can also utilize weak maps to attach properties or data to the DOM without actually having to add attributes to the DOM. We get all of the benefits of data attributes without cluttering up the DOM.
  • Finally, if we wanted to store reference data off to the side, but have it disappear when the data does, this is another use case.

All in all, WeakMap is used when we want to tie some sort of data to that object without having a tight coupling. We will be able to see this as follows:

const items = new WeakMap();
const container = document.getElementById('content');
for(let i = 0; i < 50000; i++) {
const el = document.createElement('li');
el.textContent = `we are element ${i}`;
el.onclick = function(ev) {
console.log(items.get(el));
}
items.set(el, i);
container.appendChild(el);
}
const removeHalf = function() {
const amount = Math.floor(container.children.length / 2);
for(let i = 0; i < amount; i++) {
container.removeChild(container.firstChild);
}
}

First, we create a WeakMap to store the data we want against the DOM elements that we are creating. Next, we grab our unordered list and add a list element to each iteration. We then tie the number that we are on to the DOM element through the WeakMap. That way, the onclick handler can get the item and get back the data we stored against it.

With this, we can click on any of the elements and get the data back. This is cool since we used to add data attributes to the HTML elements directly in the DOM. Now we can just use WeakMap. But, we also get one more benefit that has been talked about. If we run the removeHalf function in the command line and garbage collect, we can take a look at how many items are in the WeakMap. If we do this and we check how many elements are in the WeakMap, we will notice the number of elements it has stored can range from 25,000 to the full 50,000 elements we started with. This is for the reason stated above; once a reference has been garbage collected, the WeakMap will no longer store it. It has a weak reference.

The amount to be collected by the garbage collector is going to be up to the system that we are running. On some systems, the garbage collector may decide to not collect anything from the list. This all depends on how the V8 garbage collection has been set up for Chrome or Node.js.

We can easily see this if we replace WeakMap with a regular one. Let's go ahead and make this minor change. With this change, observe the same preceding steps. We will notice that the map still has all 50,000 items inside it. This is what we mean when we say something either has a strong reference or a weak reference. A weak reference will allow the item to be cleaned up by the garbage collector, whereas a strong reference will not. WeakMaps are great for these types of linking in terms of data to another data source. If we want the item decoration or the link to be cleaned up when the primary object is cleaned, a WeakMap is a go-to item.

A WeakSet has a more limited use case. One great use case is for checking for infinite loops in object properties or graphs. If we store all of the visited nodes in a WeakSet, we are able to check whether we have the items, but we also don't have to clear the set once we are done with the check. This means, once the data gets collected, so will all of the references that were stored in the WeakSet. Overall, a WeakSet should be used when we need to tag an object or a reference. This means that if we need to see whether we have it or whether it has been visited, a WeakSet is a likely candidate for this job.

We can utilize the deep copy example from the previous chapter. With it, we still run into one more use case that we did not think of. What happens if an item points to another item in the object and that same item decides to point back at the original item? This can be seen in the following code:

const a = {item1 : b};
const b = {item1 : a};

With each of these items pointing at one another, we would run into circular reference issues. A way to get around this is with a WeakSet. We could hold all the visited nodes, and if we come to a node that is already visited, we just return from the function. This can be seen in the modified version of the code:

const state = {};
(function(scope) {
const _state = {},
_held = new WeakSet(),
checkPrimitives = function(item) {
return item === null || typeof item === 'string' || typeof
item === 'number' || typeof item === 'boolean' ||
typeof item === 'undefined';
},
cloneFunction = function(fun, scope=null) {
return fun.bind(scope);
},
cloneObject = function(obj) {
const newObj = {},
const keys = Object.keys(obj);
for(let i = 0; i < keys.length; i++) {
const key = keys[i];
const item = obj[key];
newObj[key] = runUpdate(item);
}
return newObj;
},
cloneArray = function(arr) {
const newArr = new Array(arr.length);
for(let i = 0; i < arr.length; i++) {
newArr[i] = runUpdate(arr[i]);
}
return newArr;
},
runUpdate = function(item) {
if( checkPrimitives(item) ) {
return item;
}
if( typeof item === 'function' ) {
return cloneFunction(item);
}
if(!_held.has(item) ) {
_held.add(item);
if( item instanceof Array ) {
return cloneArray(item);
} else {
return cloneObject(item);
}
}
};
scope.update = function(obj) {
const x = Object.keys(obj);
for(let i = 0; i < x.length; i++) {
_state[x[i]] = runUpdate(obj[x[i]]);
}
_held = new WeakSet();
}
})(state);
Object.freeze(state);

As we can see, we have added a new _held variable that will hold all of our references. Then, the runUpdate function has been modified to make sure that when an item is not a primitive type or a function, we check whether we already have it in our held list. If we do, then we skip the item, otherwise, we will just keep going. Finally, we replace the _held variable with a new WeakSet since the clear method is no longer available on WeakSets.

This does not keep the circular reference, which may be a problem, but it does solve our issue of the system going into an infinite loop because of objects referencing one another. Other than this use case, and maybe some more advanced ideas, there are not many other needs for a WeakSet. The main thing is if we need to track the existence of something. If we need to do this, the WeakSet is the perfect use case for us.

Most developers will not find a need for WeakSets or WeakMaps. These will likely be utilized by library authors. However, the conventions mentioned previously may come up in some cases so it is nice to know the reason for these items and why they are there. If we do not have a reason to use something, then we should most likely not use it, this is definitely the case with these two items since they have really specific use cases and one of the major use cases for WeakMaps is being delivered to us in the ECMAScript standard (private variables).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.228.95