5 JS Data Types You Must Know | SrishCodes

5 JS Data Types You Must Know | SrishCodes

Hi everyone, welcome back to SrishCodes where I teach you interesting tech concepts that you should know about as a current or aspiring software developer. Today I will be sharing with you about 5 JavaScript Data Types that you must know about. Let's get started!

Symbol

The data type symbol is a primitive data type. Every symbol value returned from Symbol() is unique.

let sym = new Symbol()  // TypeError

let sym1 = Symbol()

The Symbol() function:

  • Returns a value of type symbol
  • Has static properties that expose several members of built-in objects
  • Has static methods that expose the global symbol registry
  • Resembles a built-in object class
  • Is incomplete as a constructor because it does not support the syntax new Symbol()
let sym2 = Symbol('foo')
let sym3 = Symbol('foo')

console.log(sym2 == sym3); // false
console.log(Symbol('foo') === Symbol('foo')); // false
console.log(sym2.toString()); // Symbol(foo)
console.log(sym2.description); // foo

More static properties and methods, as well as instance properties and methods can be found here.

Global Symbol Registry

It is a registry (think: dictionary) for symbols that you can access via a string key. It spans all realms of your engine.

let globalSymbol = Symbol.for("global");
let localSymbol = Symbol("local");

console.log(Symbol.keyFor(globalSymbol)); // global
console.log(Symbol.keyFor(localSymbol)); // undefined

In a browser, the web page, an iframe, and web worker would all have their own realm with own global objects, but they could share symbols via this global registry.

// read from the global registry
// if the symbol did not exist, it is created
let id = Symbol.for("id");

// read it again (maybe from another part of the code)
let idAgain = Symbol.for("id");

// the same symbol
console.log( id === idAgain ); // true

You can also access symbols from their variable name.

let nameSym = Symbol.for("name");
let ageSym = Symbol.for("age");

// get name by symbol
alert( Symbol.keyFor(nameSym) ); // name
alert( Symbol.keyFor(ageSym) ); // age

Symbols in Objects

If we want to use a symbol in an object literal {...}, we need square brackets around it.

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123
};

console.log(Object.keys(user)); // ['name']
console.log(user[id]); // 123

That’s because we need the value from the variable id as the key, not the string “id”.

Symbols are skipped in for...in loops.

for (let key in user) {
    console.log(key); // name
}

They are, however, not skipped in Object.assign.

// symbols in Object.assign
let clone = Object.assign({}, user);

console.log(clone[id]); // 123

Map

Map is a collection of keyed data items, just like an Object.

let myMap = new Map();
let keyString = 'a string';

// setting the values
myMap.set(keyString, "value associated with 'a string'");

// getting the values
myMap.get(keyString);    // "value associated with 'a string'"
myMap.get('a string');    // "value associated with 'a string'"
myMap.get({});            // undefined

Below are also examples of how maps can work with arrays:

let kvArray = [['key1', 'value1'], ['key2', 'value2']];
let myMap = new Map(kvArray);

myMap.get('key1'); // returns "value1"

console.log(Array.from(myMap)); // [['key1', 'value1'], ['key2', 'value2']]
console.log([...myMap]); // [['key1', 'value1'], ['key2', 'value2']]
console.log(Array.from(myMap.keys())); // ["key1", "key2"]

However there are a few differences:

  • Map does not contain any keys by default. It only contains what is explicitly put into it.
  • Map keys can be of any type, including functions, objects or any primitive.
  • A for...of loop returns an array of [key, value] for each iteration. (More on this later)
  • The number of items in a Map is easily retrieved from its size property.
  • Performs better in scenarios involving frequent additions and removals of key-value pairs.

We cannot clone maps as shown below.

let original = new Map([[1, 'one']]);
let clone = new Map(original);

console.log(clone.get(1));       // one
console.log(original === clone); // false

We can, however, merge two maps into a single map.

let first = new Map([[1, 'one'], [2, 'two'], [3, 'three']]);
let second = new Map([[1, 'uno'], [2, 'dos']]);

// Merge two maps. The last repeated key wins.
// Spread operator essentially converts a Map to an Array
let merged = new Map([...first, ...second]);

console.log(merged.get(1)); // uno
console.log(merged.get(2)); // dos
console.log(merged.get(3)); // three

More static properties as well as instance properties and methods can be found here.

Iteration over Maps

We can use several different ways to iterate over maps. Let's first define a simple map that we want to work with.

let myMap = new Map();
myMap.set(0, 'zero');
myMap.set(1, 'one');

Now, what if we want both the keys and the values from the map? There are three methods to do so as shown below:

for (let [key, value] of myMap) {
  console.log(key + ' = ' + value);
}
// 0 = zero
// 1 = one

for (let [key, value] of myMap.entries()) {
  console.log(key + ' = ' + value);
}
// 0 = zero
// 1 = one

myMap.forEach(function(value, key) {
    console.log(key + ' = ' + value);
})
// 0 = zero
// 1 = one

Or very simply, we can just access the keys or only the values from a map:

for (let key of myMap.keys()) {
  console.log(key);
}
// 0
// 1


for (let value of myMap.values()) {
  console.log(value);
}
// zero
// one

Set

Set object lets you store unique values of any type, without keys, where each value may occur only once. Like Map, iteration is always in the insertion order (More on this later).

let set = new Set();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

// visits, some users come multiple times
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

console.log( set.size ); // 3

However for both Set and Map, we cannot reorder elements or directly get an element by its number.

Below are also examples of how sets can work with arrays:

let myArray = ['value1', 'value2', 'value3'];
let mySet = new Set(myArray);

mySet.has('value1');  // returns true
console.log([...mySet]); // ['value1', 'value2', 'value3']
console.log(Array.from(mySet)); // ['value1', 'value2', 'value3']

More static properties as well as instance properties and methods can be found here.

Iteration over Sets

There are a total of four different ways to iterate over sets as shown below:

// method 1
for (let item of mySet) {
    console.log(item);
}


// method 2
for (let item of mySet.keys()) {
    console.log(item);
}


// method 3
for (let item of mySet.values()) {
    console.log(item);
}


// method 4
for (let [key, value] of mySet.entries()) {
    console.log(key);
}

WeakMap

The first difference between Map and WeakMap is that WeakMap keys must be objects, not primitive values.

let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); // works fine
weakMap.set("test", "Whoops"); // Error

It does not prevent garbage collection in case there would be no other reference to the key object.

WeakMap also does not support iteration and methods keys(), values(), entries() - there is no way to get all keys or values from it. Supported methods include:

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

This is because the JS engine decides when to perform the memory cleanup. So, technically the current element count of a WeakMap is not known.

An example of the difference using Map and WeakMap is shown below.

// Map
let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // overwrite the reference

In this case, john is stored inside the Map and can be accessed using map.keys(). However, if we use a WeakMap, john is removed from memory and the map.

// WeakMap
let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // overwrite the reference

Use Case: Additional Data

For example, we have code that keeps a visit count for users. The information is stored in a map: a user object is the key and the visit count is the value. When a user leaves (its object gets garbage collected), we don’t want to store their visit count anymore.

let visitsCountMap = new WeakMap(); // weakmap: user => visits count

// increase the visits count
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

let john = { name: "John" };
countUser(john); // count his visits
john = null; // john leaves

Using Map, john object should be garbage collected but remains in memory as it is a key in visitsCountMap.

By using WeakMap, john object becomes unreachable by all means except as a key of WeakMap. The object then gets removed from memory, along with the information by that key from WeakMap.

Use Case: Caching

For multiple calls of process(obj) with the same object, it only calculates the result the first time and then just takes it from cache.

let cache = new WeakMap();
let obj = {/* some object */};

// calculate and remember the result
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculate the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

let result1 = process(obj);
let result2 = process(obj);

obj = null; // when obj is not needed

// Can't get cache.size, as it's a WeakMap,
// but it's 0 or soon be 0
// When obj gets garbage collected, cached data will be removed as well

Using Map, cache.size will be equal to 1 as the object is still cached. Hence we need to clean the cache when the object is not needed anymore.

If we replace Map with WeakMap as shown above, this problem disappears as the cached result will be removed from memory automatically after the object gets garbage collected.

Other Use Cases

Some use cases that would otherwise cause a memory leak and are enabled by WeakMaps include:

  • Keeping private data about a specific object and only giving access to it to people with a reference to the Map.
  • Keeping data about library objects without changing them or incurring overhead.
  • Keeping data about a small set of objects where many objects of the type exist to not incur problems with hidden classes JS engines use for objects of the same type.
  • Keeping data about host objects like DOM nodes in the browser.
  • Adding a capability to an object from the outside.

WeakSet

WeakSet behaves similarly:

  • It is analogous to Set, but we may only add objects to WeakSet (not primitives).
  • An object exists in the set while it is reachable from somewhere else.
  • Like Set, it supports add, has and delete, but not size, keys() and no iterations.

To see an example of this, let's first define our WeakSet.

let visitedSet = new WeakSet();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

visitedSet.add(john); // John visited us
visitedSet.add(pete); // Then Pete
visitedSet.add(john); // John again

visitedSet has two users now. Now how do we check if a user has visited?

// check if John visited?
alert(visitedSet.has(john)); // true

// check if Mary visited?
alert(visitedSet.has(mary)); // false

john = null;

// visitedSet will be cleaned automatically

TL:DR;

If you are confused, just remember this: WeakMap is a Map-like collection that allows only objects as keys. WeakSet is a Set-like collection that stores only objects.

Both of them do not support methods and properties that refer to all keys or their count. Only individual operations are allowed.

Both WeakMap and WeakSet are used as “secondary” data structures in addition to the “main” object storage. Once the object is removed from the main storage, if it is only found as the key of WeakMap or in a WeakSet, it will be removed from memory automatically.


And that was all for this now! If you are still reading, make sure to follow me for more as I have some exciting stuff coming up. Until next time!