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 itssize
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 toWeakSet
(not primitives). - An object exists in the set while it is reachable from somewhere else.
- Like
Set
, it supportsadd
,has
anddelete
, but notsize
,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!