Photo by Ehimetalor Akhere Unuabona on Unsplash
Collections in JS
Understand collections from JS land?
Without any further ado, let's get straight to it :)
What are the collections?
Collections are the type of data structure that follows Iteration protocols
The iterable protocol allows JavaScript objects to define or customize their iteration behavior, such as what values are looped over in a for...of
construct. Some built-in types are built-in iterables with a default iteration behavior, such as Array
or Map
, while other types (such as Object
) are not.
So whenever we loop through an element using the for-of loop, behind the scenes, the for-loop works like this:
const str = "Hello World";
const iterator = str[Symbol.iterator](); // String iterator object
let stringIterator = iterator.next(); // has a method next
/* Imperative way */
while (!stringIterator.done) {
const { value } = stringIterator;
console.log(value);
stringIterator = iterator.next();
}
/* Or declarative way */
const iterator = string[Symbol.iterator]();
for(let v of iterator) {
console.log(v);
}
Make object iterator
const obj = {
a: "a",
b: "b",
c: "c",
[Symbol.iterator]: function () {
const keys = Object.keys(this);
let i = 0;
return {
next: () => {
return i < keys.length
? {
done: false,
value: this[keys[i++]],
}
: {
done: true,
value: undefined,
};
},
};
},
};
for (let i of obj) {
console.log(i);
}
Array
Arrays are one of the most common data types. Using them, we can handle a collection of data. But just like everything in JS, an array is an object, and it's kind of a surprise because why is it an object? Well, the short answer is that being an object can have some great benefits, such as allowing us to access methods
Create an array
Using the built-in Array constructor
Using array literal
const ninjas = ["Kuma", "Hattori", "Yagyu"];
const samurai = new Array("Oda", "Tomoe");
Array bounds
Because JS is a dynamic language, when we define an array with a length of 3, later we assign it the value array[4]="ninja". The array will expand to accommodate the new situation
Array methods
Let's start our discussion with some basic methods of an array
push adds an item to the end of array
unshift adds an item to the beginning of an array
pop removes an item at the end of an array
shift removes an item at the beginning of an array
Remove item at arbitrary location
Delete item from an array, leaving a hole in it
Splice method
The splice method is the versitle method in JS; it can either delete or insert element at certain location in an array depending on parameters we pass in. Let's explore it
const ninjas = ["1", "2", "3", "4"];
const removedItems = ninjas.splice(1, 1);
// splice returns an array of removed item ["2"];
ninjas;
// ["1","3","4"]
//Replace
ninja.splice(1,2,"5","6","7");
ninja;
// ["1","5","6","7","4"];
Common operation on arrays
Iterating
Mapping
Testing
Finding
Aggegating
Iterating
const ninjas = ["Yagyu", "Kuma", "Hattori"];
for(let i = 0; i < ninjas.length; i++){
console.log(ninjas[i] !== null, ninjas[i]);
}
But this method has a lot of noisy details that developers has to pay attention to, so instead of doing that manually, JS offers us a built-in function that does the exact same thing
ninjas.forEach(ninja => { console.log(ninja !== null, ninja);
));
Mapping an array
const ninjas = [
{name: "Yagyu", weapon: "shuriken"},
{name: "Yoshi", weapon: "katana"},
{name: "Kuma", weapon: "wakizashi"}
];
const weapons = ninjas.map(ninja => ninja.weapon);
Testing an array
When working with collections of items , we'll often run into situations where we need know whether all or at least some of the array items satisfy certain conditions
const ninjas = [
{name: "Yagyu", weapon: "shuriken"},
{name: "Yoshi" },
{name: "Kuma", weapon: "wakizashi"}
];
const allNinjasAreNamed = ninjas.every(ninja => "name" in ninja);
const allNinjasAreArmed = ninjas.every(ninja => "weapon" in ninja);
const someNinjasAreArmed = ninjas.some(ninja => "weapon" in ninja);
Searching arrays
Find a single item
Another common methods that we often use is finding item in an array
const ninjas = [
{name: "Yagyu", weapon: "shuriken"},
{name: "Yoshi" },
{name: "Kuma", weapon: "wakizashi"}
];
const ninjaWithWakizashi = ninjas.find(ninja => {
return ninja.weapon === "wakizashi";
});
Find multiple items
const armedNinjas = ninjas.filter(ninja => "weapon" in ninja);
Find an index of item
const yoshiIndex = ninjas.findIndex(ninja => ninja === "Yoshi");
Sorting an array
One of the most common array operations is sorting—arranging items systematically in some order. Unfortunately, correctly implementing sorting algorithms isn’t the easiest of programming tasks. JS has provided us the built-in sort method
array.sort((a, b) => a – b);
JS engine that implemented the sorting algorithm. The only thing we have to provide is callback that informs the sorting algorithm about the relationship between two items
If a callback returns a value less than 0, then item a should come before item b.
If a callback returns a value equal to 0 , then items a and b are equal
If a callback returns a value greater than 0, then item a should come after item b.
const ninjas = [{name: "Yoshi"}, {name: "Yagyu"}, {name: "Kuma"}];
ninjas.sort(function(ninja1, ninja2){
if(ninja1.name < ninja2.name) { return -1; }
if(ninja1.name > ninja2.name) { return 1; }
return 0;
});
Aggregating
const numbers = [1, 2, 3, 4];
const sum = 0;
numbers.forEach(number => {
sum += number;
});
// Simplify the problem
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((aggregated, number)=> aggeregated+
number, 0);
Array-like objects (Custom Object Implementation)
Has indexed access to the elements and a non-negative length property to know the number of elements in it. These are the only similarities it has with an array.
Doesn't have any of the Array methods like push, pop, join, map, etc.
So it's not inherited these utilities methods from array interface
arr_like.forEach((el)=>console.log(el));
forEach is not a function
But why should we care?
JS is weird, and it indeed has this data structure in its implementation. We can encounter it with various different scenarios
document.getElementsByClassName
children
arguments properties
DOMTokenList
function checkArgs() {
console.log(arguments);
}
document.getElementsByTagName('li');
And we can't use array methods on this type of data structure. What a pity
How do I convert it to an array?
Using ES6 spread operator
Array.from
function checkArgs() {
// Using spread operator
[...arguments].forEach((elem) => {
console.log(elem);
});
}
const collection = Array.from(document.getElementsByTagName('li'))
Summary
Array-like objects are not arrays. We can have index access and length properties access but we can't use array methods
There're many ways to convert array-like-objects to arrays (spread operator, Array.from methods)
Array are mutable
Yes, you heard it right. Arrays are not immutable
function arraySum(arr) {
let sum = 0, num ;
while( (num=arr.pop() !== undefined) {
sum +=num;
}
return sum;
}
function arraySum(arr) {
// We can clone the copy of it to prevent mutate the original array
const copyingArray = [...arr];
....
}
Well this is good but not that explicit , so better yet to switch to TS
function arraySum(arr:readonly number[]) {
...
arr.pop(); // 'pop' does not exist on type 'readonly number[]'
}
You can read from its element, but you can't write to them
You can read its length, but you can't set it
You can't call pop or other methods that mutate the array
The index signature in array is string
JS is a famously quirky language and I agree with you
A JS object is a collection of key and value pairs. The keys are usually strings. Let's have a look at the example
x = {};
x[[1,2,3]] = 2;
// {'1,2,3':2} it converts an array of number to string by calling
// toString method
x = { 1:"1",2:"2",3:"3" } // number key will be converted to string
Object.keys(x);
["1","2","3"];
What about an array, then?
typeof [];
// object
x=[1,2,3];
x[0]
//1
//but also
x["0"]; // works too
Object.keys(x);
//["1",2","3"];
//So when you access index in array with number and you think it's number
//but in fact JS convert those number to string to access to the array
//Array is anboject so their keys are string not number
//In TS they attempts to bring some sanity to this by allowing only
//numeric to be key in array but oc in run-time TS will be gone away
//and you can use string to access the array index
interface Array<T> {
[n:number]:T;
}
Array for Algorithms
Maps
Properties inherited through prototypes
Don't use objects as maps because objects have inherited properties that we may not need.
The key is converted to string
const obj1 = {};
const obj2 = {};
const map = {[obj1]:"obj1",[obj2]:"obj2"};
//Convert object to string "[Object object]";
map[obj1];
'obj2'
map[obj2];
'obj2'
Creating our first map
const ninjaIslandMap = new Map();
const ninja1 = { name: "Yoshi"};
const ninja2 = { name: "Hattori"};
const ninja3 = { name: "Kuma"};
ninjaIslandMap.set(ninja1, { homeIsland :"Japan" } );
ninjaIslandMap.set(ninja2, { homeIsland :"VN" } );
ninjaIslandMap.get(ninja1).homeIsland; // Japan
ninjaIslandMap.get(ninja2).homeIsland; // VN
A map can store object as its key
Some of the properties that can be used with map
set
get
delete
has
clear
Iterating over maps
My favourite thing about maps is that we can easily iterate over them compared to objects, when we have to use some JS utilities to loop over them.
Loop over keys
Loop over values
Loop over key and value pairs
const directory = new Map();
directory.set("Yoshi", "+81 26 6462");
directory.set("Kuma", "+81 52 2378 6462");
directory.set("Hiro", "+81 76 277 46");
for(let item of directory){
item[0] , item[1]
}
for(let key of directory.keys()) {
key;
directory.get(key)
}
for(let value of directory.values()) {
value;
}
Compare object vs map
Sets
In the real world, we have to deal with collections of distinct items (meaning each item can appear once), and we have not yet had this data structure in JS before ES6. Luckily, with the new ES6, we have this cool collection that we can utilize
const set = new Set(["Kuma", "Hattori", "Yagyu", "Hattori"]);
Set has a very similar API to map
has
add
delete
clear
Union of sets
A union of two sets, A and B for example , creates a new set that contains all elements from both A and B
const ninjas = ["Kuma", "Hattori", "Yagyu"];
const samurai = ["Hattori", "Oda", "Tomoe"];
const warriors = new Set([...ninjas, ...samurai]);
assert(warriors.size === 5, "There are 5 warriors in total");
Intersection of sets
The intersection of two sets, A and B , creates a set that contains element of A that are also in B
const ninjas = new Set(["Kuma", "Hattori", "Yagyu"]);
const samurai = new Set(["Hattori", "Oda", "Tomoe"]);
const ninjaSamurais = new Set(
[...ninjas].filter(ninja => samurai.has(ninja))
);
Difference of sets
The difference of two sets, A and B, contains all elements that are in set A but are not in set B
const ninjas = new Set(["Kuma", "Hattori", "Yagyu"]);
const samurai = new Set(["Hattori", "Oda", "Tomoe"]);
const pureNinJas = new Set(
[...ninjas].filter((ninja=>!sumurai.has(ninja)));
Perform a lookup with Set
Summary
A collection in JS is any object that follows iterator protocol
Set and Map data structures give us more power, from easy to iterate to performance-benchmark
So all of the time , I would say prefer using map over object and set over array