Beyond the Basics: Object vs Map/WeakMap
Tags: javascript, webdev, programming, learning
Published: 2019-09-21 20:59:55

__TLDR__
* Map can have any value as it's keys, objects can only have strings or symbols
* Maps are ordered based on insertion
* Maps are easier to clear out than objects
* Destructuring works differently with Maps vs Objects
* WeakMap provides garbage collection benefits over regular Maps

ES6, also known as ECMAScript 2015, introduced many new features. In this article, I'll be discussing a new data structure that was introduced in ES6 known as a Map and the benefits/drawbacks of using a Map over a regular javascript object.


* We're on ES10 now, should I still care about ES6?
* Can't I just keep using regular objects?

The answer to both questions above is yes. Although you can still use a plain object, knowing when and how to use a Map provides many benefits that can make your code much cleaner.


## What is a Map?

A Map is structured very similar to an object, in that it holds key/value pairs. The distinction is how and where it stores those values. 

If you want to initialize the Map with data, you can pass an array containing or an iterable object with key/value pairs as an argument of the constructor.

```javascript
const map = new Map(); // Create a empty Map

// Map initialized containing [ 'one', 1 ], [ 'two', 2 ]
const anotherMap = new Map([ [ 'one', 1 ], [ 'two', 2 ] ]);
```

The two biggest benefits regarding how the data is stored in a Map:
* An object's keys are set as either a String or a Symbol, but a Map can have _anything_ set as the key...including functions, objects and primitives.
* Maps are ordered based on order of insertion

### Keys in Maps
Lets dig a little deeper into the differences between keys in objects and a Map:

Since keys get stored as strings, non-strings will get coerced. What this means is that basically "1" and 1 are the same when they get set as keys of an object.

```javascript
const obj = {};

// Integers will be casted to a string
obj[1] = 'one';
obj['1'] // one
obj[1]   // one

// Keys will not be casted when setting keys for a Map
const map = new Map();

map.set(1, 'one');           // a numeric key
map.set('1', 'another one'); // a string key
// map will contain two items: 1, 'one' and '1', 'another one'
```

When I said anything can be set as a key...I mean _anything_:
```javascript
const person = {
  name: 'John'
}

const map = new Map();

// Set an object as a key
map.set(person, 30);
map.get(person); // 30

// You can even use a map as a key for a map!
const anotherMap = new Map();

anotherMap.set(map, true);
anotherMap.get(map); // true
```
Maps use the `sameValueZero` algorithm when comparing keys. This is pretty similar to strict equality `===` but also considers `NaN === NaN`.

In objects you will need to set key/values one at a time, but since `Map.set()` returns the map you can chain calls:
```javascript
const map = new Map();

map.set(1, 'one')
  .set(2, 'two')
  .set(3, 'three')
  .entries();
// 1 => "one", 2 => "two", 3 => "three"
```

#### Deleting properties

Deleting properties from objects and Maps are pretty similar, but Maps provide a few extra benefits.

When deleting a property from an object, it will always return true unless the property is a non-configurable property.

When deleting a property from a Map, it will return true if the property existed and has been removed, otherwise it will return false if it doesn't exist.

```javascript
// deleting properties from objects
const obj = {
  one: 'one'
}

delete obj.one // true
delete obj.two // also true

// deleting properties from Maps
const map = new Map()

map.set('one', 'one')
map.delete('one') // true
map.delete('two') // false
```
But what if you want to delete all properties that belong to that object?
You could do:
```javascript
const obj = {
  one: 'one'
}

obj = {}
```
In this implementation you're not really removing properties, you're just setting obj to a new empty object and relying on the garbage collector to clean up the old object. The issue is that if the object is being referenced elsewhere, it will still exist. A better implementation would be:
```javascript
for (let key in obj){
    if (obj.hasOwnProperty(key)){
        delete obj[key];
    }
}
```
This is better but still doesn't handle keys that are Symbols. 
Maps make it very easy to clear all it's elements regardless what the key is:
```javascript
const values = [['1', 'one'], [true, '5'], [Symbol('test'), 10], [function() {}, 3]]
const map = new Map(values)

map.clear() // completely empties out the map
```

According to MDN docs regarding performance:
> A Map may perform better in scenarios involving frequent addition and removal of key pairs.

### Iterating Maps

As I mentioned in the beginning of this article, unlike objects, Maps are ordered based on insertion which makes iterating more predictable.
```javascript
const obj = {};

obj[5] = 'five';
obj[4] = 'four';
Object.entries(obj); // [ ['4', 'four'], ['5', "five"] ]

const map = new Map();

map.set(5, 'five')
  .set(4, 'four')
  .entries(); // [ 5 => "five", 4 => "four" ]

```

Similar to objects, there are three methods you can use for looping over Maps:
* `map.keys()` returns an iterable containing the keys
* `map.values()` returns an iterable containing the values
* `map.entries()` returns an iterable containing the `[key, value]` pairs

Objects use `Object.keys`, `Object.values`, and `Object.entries`. One main difference is that these return arrays whereas the map methods return iterables.

```javascript
const obj = {
  one: 1,
  two: 2,
};

for (let key of Object.keys(obj)) {
  console.log(key)
} // logs "one" then "two"

for (let value of Object.values(obj)) {
  console.log(value)
} // logs 1 then 2

for (let entry of Object.entries(obj)) {
  console.log(entry)
} // logs ["one", 1] then ["two", 2]
```

Maps work similarly:

```javascript
const map = new Map([["one", 1], ["two", 2]]);

for (let key of map.keys()) {
  console.log(key)
} // logs "one" then "two"

for (let value of map.values()) {
  console.log(value)
} // logs 1 then 2

for (let entry of map.entries()) {
  console.log(entry)
} // logs ["one", 1] then ["two", 2]

// One difference is that map.entries() is used by default in a for..of loop
for (let entry of map) {
  console.log(entry)
} // still logs ["one", 1] then ["two", 2]
```
Note: Since anything can be set as a key, maps will always iterate over all items. But for objects there are some properties that won't be iterated by default like Symbols.

### Converting between Maps and objects

Now that you know some of the differences, it might be helpful to know how to convert an object to a Map or vise versa to take advantages of the benefits of each data structure.

A Map requires an array or iterable, so we can use `Object.entries` to get the key/value pairs as an array and pass it into the constructor:

```javascript
const obj = {
  'one': 1,
  'two': 2,
}

const map = new Map(Object.entries(obj));

console.log(map.get('one')) // 1
```

Ok, that looks simple enough...but how the heck to we create an object from a Map? Luckily we have `Object.fromEntries ` which basically works the reverse way of `Object.entries`:

```javascript
const map = new Map();

map.set('one', 1);
map.set('two', 2);

const obj = Object.fromEntries(map.entries());
const obj = Object.fromEntries(map); // Or we can even omit the entries() since that's used by default

console.log(obj.one') // 1
```

### Destructuring

Since Maps are ordered similar to arrays, you lose the ability to destructure by keys like you can do with objects.

```javascript
const obj = {
  one: 1,
  two: 2,
  three: 3,
}

let { one, two } = obj;
console.log(one) // 1
```

Now lets try destructuring a Map:
```javascript
const map = new Map([ [ 'one', 1], ['two', 2] ]);

let { one, two } = map;
console.log(one) // undefined

// But you can destructure it similar to an array where you destructure by the order items were added into the map
let [ firstEntry, secondEntry ] = map;
console.log(firstEntry) // ["one", 1]
console.log(secondEntry) // ["two", 2]
```

## Map vs WeakMap

Now that you're a Map connoisseur, it will be beneficial to learn a little about WeakMap which was also introduced in ES6.

One main difference when using a WeakMap is that the keys have to be objects, not primitive values. Which means they will pass by reference.

So why use a WeakMap? The major advantage of using a WeakMap over a Map is memory benefits. 

Objects that are not-reachable get garbage collected, but if they exist in as a key in another reachable structure then they won't get garbage collected. Lets look at an example:

```javascript
let obj = { name: 'Matt' } // object can be accessed

let obj = null // overwrite the reference, the object above will be garbage collected and removed from memory
```
If the object is still reachable it will not be removed from memory:
```javascript
let obj = { name: 'Matt' } // object can be accessed

let map = new Map();
map.set(obj, true);

obj = null // overwrite the reference, but since it's still reachable through the map, the object will not be garbage collected
```

WeakSet does not prevent garbage-collection of it's key objects.
```javascript
let obj = { name: 'Matt' } // object can be accessed

let weakMap = new WeakMap();
weakMap.set(obj, true);

obj = null // overwrite the reference, the object was removed from memory
// weakMap is now empty
```

WeakMaps only have the following methods: `get`, `set`, `delete`, `has`.

Why just those? Because the Javascript engine handles the memory cleanup so it may choose to clean it up immediately or wait until more deletions happen. 

Therefore things like the current count of a WeakMap will never fully be accurate.

So when would you ever use a WeakMap?
When you want to use it for additional storage that only lasts until the reference is destroyed.

Lets say as an example you have users and you want to increase the count anytime they visit a page, but you no longer care once the user is gone.

```javascript
let userCountStorage = new WeakMap();

let user = { name: 'matt' };

incrementCount(user); // pretend this function adds the user to the userCountStorage or increments the count if they already exists

// increment every time they visit a page
incrementCount(user);
incrementCount(user);

// Now they're gone so we get rid of the reference
user = null // since the object is no longer reachable, the garbage collector will automatically also remove the item from our userCountStorage
```

In the above example if we used Map instead of WeakMap we would run into memory issues if we didnt manually remove the references from the storage once we destroyed the reference elsewhere.

##Summary

Using a Map or object is always situational but hopefully now you've learned some benefits and drawbacks for using each data structure.


More on Maps/Objects:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object