15

Based on my understanding of the docs (here and here) one would need a reference to the memory address for it to work:

const foo = {};
const map = new Map();
map.set(foo,'123');  // Can only be done if memory address of `foo` is known. Any other shimming would require stringification of foo

This is because JavaScript object {} keys can only be strings (at least in ES5).

Yet I see Map shim being available : https://github.com/zloirock/core-js#map. I tried reading the source but its too neatly abstracted (internally uses strong collection which then imports 10 more files)

Question

Answer any of the following please

  • Is there a simple trick to it and can it truly even be done (without stringification)?
  • Perhaps it mutates foo to store some string on it and then uses that as the key?
  • Something else and maybe I am reading the docs wrong?
basarat
  • 261,912
  • 58
  • 460
  • 511
  • 4
    I'm pretty sure that the shims simply use `===` to compare object identity, and therefor have many O(N) operations instead of the O(1) or O(log(N)) operations that the native implementations could have. – Jeremy Mar 09 '16 at 23:19
  • ES2015 `Map` cannot be shimmed correctly, I believe the closest implentation would be to keep a record of the keys, some sort of iteration, and a regular comparison against objects, as Jeremy mentions as well. – adeneo Mar 09 '16 at 23:24
  • [Here's](https://cloud.github.com/downloads/eriwen/es6-map-shim/es6-map-shim-0.2.js) a readable polyfill btw – adeneo Mar 09 '16 at 23:25
  • All answers so far show "good" polyfill in terms of basic functionality mechanism, but - as per spec "Map object must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements in the collection." condition that can't be met by those solutions. – ArtoAle Mar 09 '16 at 23:43
  • 1
    @ArtoAle The mutate and add a `_id` on the key to use for lookups solution in the accepted answer does that. Nevertheless my suspicions are correct and there is no *zero impact* implementation for ES5. Thanks for all your time – basarat Mar 10 '16 at 00:19
  • 1
    @basarat totally agree - you can improve performances by modifying the key object - even though you might be unable to do so e.g. if the `key` has gone through `Object.freeze` – ArtoAle Mar 10 '16 at 04:06
  • @adeneo Your readable shim appears to be kaput (not surprising six years later, but not less true, I'm afraid). – ruffin Jun 17 '22 at 21:09

3 Answers3

11

There are two ways that come to mind. First, obviously, you can have an array of keys, and search it linearly:

Map1 = {
    keys: [],
    values: [],
};

Map1.set = function(key, val) {
    var k = this.keys.indexOf(key);
    if(k < 0)
        this.keys[k = this.keys.length] = key;
    this.values[k] = val;
};

Map1.get = function(key) {
    return this.values[this.keys.indexOf(key)];
};


foo = {};
bar = {};

Map1.set(foo, 'xxx');
Map1.set(bar, 'yyy');

document.write(Map1.get(foo) + Map1.get(bar) + "<br>")

The second option is to add a special "key" marker to an object which is used as a key:

Map2 = {
    uid: 0,
    values: {}
};

Map2.set = function(key, val) {
    key = typeof key === 'object'
        ? (key.__uid = key.__uid || ++this.uid)
        : String(key);
    this.values[key] = val;
};

Map2.get = function(key) {
    key = typeof key === 'object'
        ? key.__uid
        : String(key);
    return this.values[key];
};


foo = {};
bar = {};

Map2.set(foo, 'xxx');
Map2.set(bar, 'yyy');

document.write(Map2.get(foo) + Map2.get(bar) + "<br>")

Unlike the 1st option, the second one is O(1). It can be done more accurately by making uid non-writable/enumerable. Also, each Map should have its own "uid" name (this can be easily set up in the Map constructor).

georg
  • 211,518
  • 52
  • 313
  • 390
8

The trick is to store in an array and perform the lookup in O(n) time by iterating and using strict comparison—instead of using a true hash function which would be O(1) lookup. For example consider this:

var myObj = {};

var someArray = [{}, {}, myObj, {}];

console.log(someArray.indexOf(myObj)); // returns 2

Here is my implementation from another answer: Javascript HashTable use Object key

function Map() {
    var keys = [], values = [];

    return {
        put: function (key, value) {
            var index = keys.indexOf(key);
            if(index == -1) {
                keys.push(key);
                values.push(value);
            }
            else {
                values[index] = value;
            }
        },
        get: function (key) {
            return values[keys.indexOf(key)];
        }
    };
}
Community
  • 1
  • 1
Peter
  • 3,998
  • 24
  • 33
3

Have a look at my polyfill here. I am not advertising my polyfill, rather all I am saying is that it is the simplest and most straightforward I have yet to find, and thus it is the most suitable for learning and educational analysis. Basically, how it works is it uses a lookup table for the keys and a corresponding value table as visualized below.

var k = {}, j = [], m = document, z = NaN;
var m = new Map([
    [k, "foobar"], [j, -0xf], [m, true], [z, function(){}]
]);




Index      Key                 Value
##### ################    ################
0.    k ({})              "foobar"
1.    j ([])              -15
2.    m (Document)        true
3.    z (NaN)             function(){}

Internally, each item is stored at a different index, or at least that is the way I like to do it. This is also similar to the way the browser implements it internally. Unfortunately, I have seen some other polyfills that attempt to instead store the key on the object itself, and mess with all the internal methods to hide it, resulting in the entire webpage running 10000% slower and the maps being so slow that it takes nearly a full millisecond just to set and get new properties. Plus, I cannot fathom how many countless hours they waisted just trying to monkey-patch all the internal methods such as hasOwnProperty.

As for how and why my polyfill works, javascript objects are stored at a different place in memory. That is why [] !== [] and indexOf on an array of javascript objects works properly. It is because they are not the same array.

Jack G
  • 4,553
  • 2
  • 41
  • 50
  • Nice, tight `Map` shim so far, and much easier than bringing in all of `core-js` if you need a `Map` shim specifically. Now let's wait for the edge conditions to show up. `;^D` – ruffin Jun 17 '22 at 21:07
  • @ruffin Glad to hear you are not falling for core-js. It's ridiculously bloated and not just for the sake of pedantic standards compliance but it seems like the maintainer has a single-minded obsession with a "bigger = better" philosophy (maybe due to personal insecurities and feelings of inadequacy?) and treats larger file sizes as a goal to strive towards. For example, there's literally a polyfill for some obscure parseInt bug that doesn't affect any real JavaScript code in early versions of MS Edge that cannot be in use any longer (as Microsoft forces updates without asking). – Jack G Nov 06 '22 at 18:47