Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <h1>Problem description</h1> <p>JavaScript has no built-in general <em>map</em> type (sometimes called <em>associative array</em> or <em>dictionary</em>) which allows to access arbitrary values by arbitrary keys. JavaScript's fundamental data structure is the <em>object</em>, a special type of map which only accepts strings as keys and has special semantics like prototypical inheritance, getters and setters and some further voodoo.</p> <p>When usings objects as maps, you have to remember that the key will be converted to a string value via <code>toString()</code>, which results in mapping <code>5</code> and <code>'5'</code> to the same value and all objects which don't overwrite the <code>toString()</code> method to the value indexed by <code>'[object Object]'</code>. You might also involuntarily access its inherited properties if you don't check <code>hasOwnProperty()</code>.</p> <p>JavaScript's built-in <em>array</em> type does not help one bit: JavaScript arrays are not associative arrays, but just objects with a few more special properties. If you want to know why they can't be used as maps, <a href="http://andrewdupont.net/2006/05/18/javascript-associative-arrays-considered-harmful/" rel="noreferrer">look here</a>.</p> <h1>Eugene's Solution</h1> <p>Eugene Lazutkin already described the basic idea of using a custom hash function to generate unique strings which can be used to look up the associated values as properties of a dictionary object. This will most likely be the fastest solution, because objects are internally implemented as <em>hash tables</em>.</p> <ul> <li><strong>Note:</strong> Hash tables (sometimes called <em>hash maps</em>) are a particular implementation of the map concept using a backing array and lookup via numeric hash values. The runtime environment might use other structures (such as <em>search trees</em> or <em>skip lists</em>) to implement JavaScript objects, but as objects are the fundamental data structure, they should be sufficiently optimised.</li> </ul> <p>In order to get a unique hash value for arbitrary objects, one possibility is to use a global counter and cache the hash value in the object itself (eg in a property named <code>__hash</code>).</p> <p>A hash function which does this is and works for both primitive values and objects is:</p> <pre><code>function hash(value) { return (typeof value) + ' ' + (value instanceof Object ? (value.__hash || (value.__hash = ++arguments.callee.current)) : value.toString()); } hash.current = 0; </code></pre> <p>This function can be used as described by Eugene. For convenience, we will further wrap it in a <code>Map</code> class.</p> <h1>My <code>Map</code> implementation</h1> <p>The following implementation will additionally store the key-value-pairs in a doubly linked list in order to allow fast iteration over both keys and values. To supply your own hash function, you can overwrite the instance's <code>hash()</code> method after creation.</p> <pre><code>// linking the key-value-pairs is optional // if no argument is provided, linkItems === undefined, i.e. !== false // --&gt; linking will be enabled function Map(linkItems) { this.current = undefined; this.size = 0; if(linkItems === false) this.disableLinking(); } Map.noop = function() { return this; }; Map.illegal = function() { throw new Error("illegal operation for maps without linking"); }; // map initialisation from existing object // doesn't add inherited properties if not explicitly instructed to: // omitting foreignKeys means foreignKeys === undefined, i.e. == false // --&gt; inherited properties won't be added Map.from = function(obj, foreignKeys) { var map = new Map; for(var prop in obj) { if(foreignKeys || obj.hasOwnProperty(prop)) map.put(prop, obj[prop]); } return map; }; Map.prototype.disableLinking = function() { this.link = Map.noop; this.unlink = Map.noop; this.disableLinking = Map.noop; this.next = Map.illegal; this.key = Map.illegal; this.value = Map.illegal; this.removeAll = Map.illegal; return this; }; // overwrite in Map instance if necessary Map.prototype.hash = function(value) { return (typeof value) + ' ' + (value instanceof Object ? (value.__hash || (value.__hash = ++arguments.callee.current)) : value.toString()); }; Map.prototype.hash.current = 0; // --- mapping functions Map.prototype.get = function(key) { var item = this[this.hash(key)]; return item === undefined ? undefined : item.value; }; Map.prototype.put = function(key, value) { var hash = this.hash(key); if(this[hash] === undefined) { var item = { key : key, value : value }; this[hash] = item; this.link(item); ++this.size; } else this[hash].value = value; return this; }; Map.prototype.remove = function(key) { var hash = this.hash(key); var item = this[hash]; if(item !== undefined) { --this.size; this.unlink(item); delete this[hash]; } return this; }; // only works if linked Map.prototype.removeAll = function() { while(this.size) this.remove(this.key()); return this; }; // --- linked list helper functions Map.prototype.link = function(item) { if(this.size == 0) { item.prev = item; item.next = item; this.current = item; } else { item.prev = this.current.prev; item.prev.next = item; item.next = this.current; this.current.prev = item; } }; Map.prototype.unlink = function(item) { if(this.size == 0) this.current = undefined; else { item.prev.next = item.next; item.next.prev = item.prev; if(item === this.current) this.current = item.next; } }; // --- iterator functions - only work if map is linked Map.prototype.next = function() { this.current = this.current.next; }; Map.prototype.key = function() { return this.current.key; }; Map.prototype.value = function() { return this.current.value; }; </code></pre> <h1>Example</h1> <p>The following script</p> <pre><code>var map = new Map; map.put('spam', 'eggs'). put('foo', 'bar'). put('foo', 'baz'). put({}, 'an object'). put({}, 'another object'). put(5, 'five'). put(5, 'five again'). put('5', 'another five'); for(var i = 0; i++ &lt; map.size; map.next()) document.writeln(map.hash(map.key()) + ' : ' + map.value()); </code></pre> <p>generates this output:</p> <pre><code>string spam : eggs string foo : baz object 1 : an object object 2 : another object number 5 : five again string 5 : another five </code></pre> <h1>Further considerations</h1> <p>PEZ suggested to overwrite the <code>toString()</code> method, presumably with our hash function. This is not feasible because it doesn't work for primitive values (changing <code>toString()</code> for primitives is a <em>very</em> bad idea). If we want <code>toString()</code> to return meaningful values for arbitrary objects, we would have to modify <code>Object.prototype</code>, which some people (myself not included) consider <em>verboten</em>.</p> <hr> <p><strong>Edit:</strong> The current version of my <code>Map</code> implementation as well as other JavaScript goodies <a href="http://mercurial.intuxication.org/hg/js-hacks/raw-file/tip/map.js" rel="noreferrer">can be obtained from here</a>.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload