defunct

bounding brokenness

Lightweight JavaScript dictionaries with arbitrary keys

Update (2014-04-07): Consider using JavaScript Map objects instead.

A well-known fact is that you can use any1 JavaScript object as a dictionary (map) from strings to objects.

> var dict = {};
> dict.key1 = "foo";
> dict["key2"] = "bar";

This works well as long as the keys are constants, or in general under your control. However, things don’t work as well when you start allowing arbitrary, user-controlled keys. Say you use the tags defined in Thunderbird as keys. What happens if a user defines a tag named “watch”?

> var dict = {};
> ("watch" in dict)
true

Uh oh.

You might suspect that hasOwnProperty fixes this, and it does, but

  1. it’s clunky
  2. it doesn’t fix other magical keywords like __proto__ (though I’d like to know what sort of user has a tag named __proto__ :) )

Even worse, since __proto__ is both non-enumerable and non-configurable, it’ll be lost in a for..in loop.

There are other issues too: subtle changes in semantics because of a key named toString, for instance. What if a programmer has a debug statement printing out a dictionary somewhere (not that it’s generally too useful)?

Clearly, plain objects don’t cut it for arbitrary keys, so we in Thunderbird needed something a little more sophisticated. We still wanted to retain most of the speed of direct property access, though. I spent a weekend investigating what we could do, and what was finally agreed upon was a lightweight wrapper around objects.

The API (get/set/has/del) is inspired by, but somewhat different from, Python’s dict API. Of course, now that we’re using a wrapper anyway, we can add convenience stuff to make using it more enticing:

  • separate iterators over keys, values and key-value pairs
  • a working toString function!
  • a count property

We’ve landed this in comm-central and it’s available to any Thunderbird, SeaMonkey or extension developers who want to use it – simply use Components.utils.import("resource:///modules/dictUtils.js"); to import it into your scope. For examples, see the tests.

Here’s the GitHub repository where I experimented, including an aborted attempt to use ECMAScript 5 proxies.

Update (2011-03-23): The module’s in mozilla-central now! To use it, add the line

Components.utils.import("resource://gre/modules/Dict.jsm");

to your scope.


1 You should only be using plain objects as dictionaries, though. In particular, you shouldn’t be using JavaScript’s Array objects as dictionaries.