Categories
JavaScript Best Practices

Why Should We Stop Using Objects As Maps in JavaScript?

Spread the love

Before ES6, to make a dictionary or a map, we often used objects to store the keys and values. This has some problems that can be avoided with maps.

An object lets us map strings to values. However, with the pitfalls of JavaScript objects and the existence of the Map constructor, we can finally stop using objects as maps or dictionaries.

Inheritance and Reading Properties

Normally, objects in JavaScript inherit from the Object object if no prototype is explicitly set. This means that we have methods that are in the prototype of the object.

To check if the property is in the object or its prototype, we have to use the hasOwnProperty of the object. This is a pain and we can easily forget about this.

This means that we can accidentally get and set properties that aren’t actually in the object that we defined. For example, if we define an empty object:

let obj = {}

Then, when we write:

'toLocaleString' in obj;

We get the value of true returned. This is because the in operator designates properties in the object’s prototype as being part of the object, which we don’t really want for dictionaries or maps.

To create a pure object with no prototype, we have to write:

let obj = Object.create(null);

The create method takes a prototype object of the object it creates as an argument, so we’ll get an object that doesn’t inherit from any prototype. Built-in methods like toString or toLocaleString, they aren’t enumerable, so they won’t be included in the for...in loop.

However, if we create an object with enumerable properties as we do in the following code:

let obj = Object.create({
  a: 1
});

for (const prop in obj) {
  console.log(prop);
}

Then, we do get the a property logged in the for...in above, which loops through all the owned and inherited properties of an object.

To ignore the inherited properties, we can use the hasOwnProperty method of an object. For example, we can write:

let obj = Object.create({
  a: 1
});

for (const prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    console.log(prop);
  }
}

Then, we don’t get anything logged.

As we can see, accessing values with property keys can be tricky with regular JavaScript objects.

Overriding Values of Properties

With plain objects, we can easily override and delete existing properties. Any writable properties can have their value overridden or deleted.

We can assign values to any property that’s in an object. For example, we can write:

let obj = {};
obj.toString = null;

Then, when we run:

obj.toString();

We get the error Uncaught TypeError: obj.toString is not a function.

This is a big problem since we can easily change the value of any original or inherited property of an object. As we can see, we overwrote the built-in toString method with null with just one assignment operation.

This means that using objects as dictionaries or Maps is risky since we can easily do this accidentally. There’s no way to prevent this other than checking the values that may be names of built-in methods.

The Object’s Prototype

The prototype of an object is accessible by its __proto__ property. It’s a property that we can both get and set. For example, we can write:

let obj = Object.create({
  a: 1
});

obj.__proto__ = {
  b: 1
};

Then, our object’s prototype is { b: 1 }. This means that we changed the prototype of obj, which was { a: 1 } originally, to { b: 1 }, just by setting the __proto__ property of obj.

When we loop through the obj object with the for...in loop like the following code:

for (const prop in obj) {
  console.log(prop);
}

We get b logged.

This means that we have to avoid accessing the __proto__ property when we try to access our object that we use for a dictionary or map. What we have is another trap that might get us when using objects as maps or dictionaries.

Getting Own Enumerable Properties to Avoid Traps

To avoid traps of getting properties that are inherited from other objects, we can use the Object.keys to get the object’s own property names. It returns an array with the keys of the object that we defined and omits any inherited property names.

For example, if we have:

let obj = Object.create({
  a: 1
});
console.log(Object.keys(obj));

Then we get an empty array logged.

Similarly, Object.entries accepts an object as an argument and returns an array with arrays that have the key as the first element and the value of the key as the second element.

For example, if we write:

let obj = Object.create({
  a: 1
});
console.log(Object.entries(obj));

Then we also get an empty array logged.

ES6 Maps

Better yet, we should be using ES6 Map objects, which are an actual implementation of a map or dictionary.

Map objects have the set method that lets us add keys and values, which are the first and second arguments of what the method accepts respectively.

We can define Maps as we do in the following code:

let objMap = new Map();
objMap.set('foo', 'bar');
objMap.set('a', 1);
objMap.set('b', 2);

Instead of using the set method to add our keys and values, we can also pass a nested array where each entry of the array has the key as the first element and the value as the second element.

One good thing about Map objects is that we can use non-string keys. For example, we can write:

let objMap = new Map();
objMap.set(1, 'a');

We can also use nested arrays to define a Map. For example, instead of using the set method multiple times, we can write the following:

const arrayOfKeysAndValues = [
  ['foo', 'bar'],
  ['a', 1],
  ['b', 2]
]
let objMap = new Map(arrayOfKeysAndValues);
console.log(objMap)

There are also specialized methods to get entries by key, get all entries, loop through each entry, and remove entries. We can use the get method to get an entry by its key:

objMap.get('foo'); // 'bar'

We can also get a value from a non-string key, unlike objects. For instance, if we have:

let objMap = new Map();
objMap.set(true, 'a');

Then console.log(objMap.get(true)); will get us 'a'.

And we can clear all entries of the Map object with the clear method. For example, we can write:

objMap.clear();

We can get all entries with the objMap.entries method and we can use the for...of loop to loop through the items as well since it has an iterator.

Conclusion

We should stop using objects as dictionaries now. There are too many pitfalls since objects inherit from the Object object by default and other objects as we set them.

It also lets us override the value of methods like toString which isn’t a result we want most of the time.

To avoid these issues, we should use the Map object which was introduced in ES6. It has special methods to get and set entries and we can loop through them with the for...of loop or convert the object to an array.

Leave a Reply

Your email address will not be published. Required fields are marked *