True Hash Maps in JavaScript
Using an object literal as a simple means to storing key-value pairs is common place within JavaScript. However, an object literal is not a true hash map and therefore poses potential liabilities if used in the wrong manner. While JavaScript may not offer native hash maps (at least not cross-browser), there is a superior alternative to object literals to capture the desired functionality without the pitfalls.
The Problem with Object Literals
The problem resides in the prototype chain of object literals as the properties and methods inherited from the Object
prototype can clash with the mechanisms used to assert the existence of a key. Take for instance the toString
method, checking the existence of a key with the same name using the in
operator will result in a false positive:
var map = {}; 'toString' in map; // true
This happens because the in
operator looks up the object’s prototype chain to find inherited properties, in this case the toString
method. To resolve this, the hasOwnProperty
method was conceived which only looks for the existence of a specified key as a direct property of an object:
var map = {}; map.hasOwnProperty('toString'); // false
This works fine until you encounter a key named “hasOwnProperty”. Overwriting this method would cause further attempts to invoke the hasOwnProperty
method to result in unexpected behavior, most likely an error depending on the new value:
var map = {}; map.hasOwnProperty = 'foo'; map.hasOwnProperty('hasOwnproperty'); // TypeError
A quick fix for this is leveraging a generic object literal, one that hasn’t been tampered with, and executing it’s hasOwnProperty
method in the context of your hash map:
var map = {}; map.hasOwnProperty = 'foo'; {}.hasOwnProperty.call(map, 'hasOwnproperty'); // true
This works without any issues, but nevertheless, the object still imposes restrictions on how it can be utilized. For instance, every time you wanted to enumerate the properties of the object within a for ... in
loop, you would need to filter out the properties of the prototype chain:
var map = {}; var has = {}.hasOwnProperty; for (var key in map) { if (has.call(map, key)) { // do something } }
This can become a bit tedious after awhile. Thankfully, there is a better way.
Bare Objects
The secret to creating a true hash map is loosing the prototype all together and the baggage that comes with it. To do this we can take advantage of the Object.create
method introduced in ES5. What’s unique about this method is that you can explicitly define the prototype for a new object. For example, defining a plain object literal with a little more verbosity:
var obj = {}; // is equivalent to: var obj = Object.create(Object.prototype);
In addition to defining a prototype of your choosing, you can also forgo a prototype completely by passing a null
value in place of a prototype object:
var map = Object.create(null); map instanceof Object; // false Object.prototype.isPrototypeOf(map); // false Object.getPrototypeOf(map); // null
These bare (aka dictionary) objects are ideal for hash maps because the absence of a [[Prototype]]
removes the risk of name conflicts. Since the object is completely void of any methods or properties, it will resist any type of coercion, attempting to do so would result in an error:
var map = Object.create(null); map + ""; // TypeError: Cannot convert object to primitive value
There is no primitive value or string representation because bare objects are not intended to be used for anything other than a key-value store, plain and simple. This serves to rationalize its distinct purpose. Keep in mind that the hasOwnProperty
method is also lost to the bare object, which is okay because the in
operator now works without any exceptions:
var map = Object.create(null); 'toString' in map; // false
Better yet, those tedious for ... in
loops become much simpler. We can finally write a loop the way it was meant to be:
var map = Object.create(null); for (var key in map) { // do something }
Despite the differences, for all intents and purposes, it still behaves much like an object literal. Properties can be accessed using dot or bracket notation, the object can be stringified, and the object can still be employed as the context of a method from the Object
prototype:
var map = Object.create(null); Object.defineProperties(map, { 'foo': { value: 1, enumerable: true }, 'bar': { value: 2, enumerable: false } }); map.foo; // 1 map['bar']; // 2 JSON.stringify(map); // {"foo":1} {}.hasOwnProperty.call(map, 'foo'); // true {}.propertyIsEnumerable.call(map, 'bar'); // false
Even different methods of type checking will indicate what you would expect from an object literal:
var map = Object.create(null); typeof map; // object {}.toString.call(map); // [object Object] {}.valueOf.call(map); // Object {}
All this makes it simple to replace object literals with bare objects and have them integrate nicely into a pre-existing application without forcing wide spread changes.
Conclusion
In the context of simple key-value stores, a bare object is the clear successor to object literals, helping to alleviate the quirks with a clearly defined purpose. For more fully-featured data structures, ES6 will introduce native hash maps in the form of the Map and Set objects, among others. Until then, or even after, you should look to bare objects for all your basic hash map needs.