简体   繁体   中英

Javascript HashTable use Object key

I want to create a hash table with Object keys that are not converted into String.

Some thing like this:

var object1 = new Object();
var object2 = new Object();

var myHash = new HashTable();

myHash.put(object1, "value1");
myHash.put(object2, "value2");

alert(myHash.get(object1), myHash.get(object2)); // I wish that it will print value1 value2

EDIT: See my answer for full solution

Here is a simple Map implementation that will work with any type of key, including object references, and it will not mutate the key in any way:

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)];
        }
    };
}

While this yields the same functionality as a hash table, it's not actually implemented using a hash function since it iterates over arrays and has a worst case performance of O(n). However, for the vast majority of sensible use cases this shouldn't be a problem at all. The indexOf function is implemented by the JavaScript engine and is highly optimized.

Here is a proposal:

function HashTable() {
    this.hashes = {};
}

HashTable.prototype = {
    constructor: HashTable,

    put: function( key, value ) {
        this.hashes[ JSON.stringify( key ) ] = value;
    },

    get: function( key ) {
        return this.hashes[ JSON.stringify( key ) ];
    }
};

The API is exactly as shown in your question.

You can't play with the reference in js however (so two empty objects will look like the same to the hashtable), because you have no way to get it. See this answer for more details: How to get javascript object references or reference count?

Jsfiddle demo: http://jsfiddle.net/HKz3e/

However, for the unique side of things, you could play with the original objects, like in this way:

function HashTable() {
    this.hashes = {},
    this.id = 0;
}

HashTable.prototype = {
    constructor: HashTable,

    put: function( obj, value ) {
        obj.id = this.id;
        this.hashes[ this.id ] = value;
        this.id++;
    },

    get: function( obj ) {
        return this.hashes[ obj.id ];
    }
};

Jsfiddle demo: http://jsfiddle.net/HKz3e/2/

This means that your objects need to have a property named id that you won't use elsewhere. If you want to have this property as non-enumerable, I suggest you take a look at defineProperty (it's not cross-browser however, even with ES5-Shim, it doesn't work in IE7).

It also means you are limited on the number of items you can store in this hashtable. Limited to 2 53 , that is.

And now, the "it's not going to work anywhere" solution: use ES6 WeakMaps. They are done exactly for this purpose: having objects as keys. I suggest you read MDN for more information: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/WeakMap

It slightly differs from your API though (it's set and not put ):

var myMap = new WeakMap(),
    object1 = {},
    object2 = {};

myMap.set( object1, 'value1' );
myMap.set( object2, 'value2' );

console.log( myMap.get( object1 ) ); // "value1"
console.log( myMap.get( object2 ) ); // "value2"

Jsfiddle demo with a weakmap shim: http://jsfiddle.net/Ralt/HKz3e/9/

However, weakmaps are implemented in FF and Chrome ( only if you enable the "Experimental javascript features" flag in chrome however). There are shims available, like this one: https://gist.github.com/1269991 . Use at your own risk.

You can also use Maps , they may more suit your needs, since you also need to store primitive values (strings) as keys. Doc , Shim .

I took @Florian Margaine's suggestion to higher level and came up with this:

function HashTable(){
    var hash = new Object();
    this.put = function(key, value){
        if(typeof key === "string"){
            hash[key] = value;
        }
        else{
            if(key._hashtableUniqueId == undefined){
                key._hashtableUniqueId = UniqueId.prototype.generateId();
            }
            hash[key._hashtableUniqueId] = value;
        }

    };

    this.get = function(key){
        if(typeof key === "string"){
            return hash[key];
        }
        if(key._hashtableUniqueId == undefined){
            return undefined;
        }
        return hash[key._hashtableUniqueId];
    };
}

function UniqueId(){

}

UniqueId.prototype._id = 0;
UniqueId.prototype.generateId = function(){
    return (++UniqueId.prototype._id).toString();
};

Usage

var map = new HashTable();
var object1 = new Object();
map.put(object1, "Cocakola");
alert(map.get(object1)); // Cocakola

//Overriding
map.put(object1, "Cocakola 2");
alert(map.get(object1)); // Cocakola 2

// String key is used as String     
map.put("myKey", "MyValue");
alert(map.get("myKey")); // MyValue
alert(map.get("my".concat("Key"))); // MyValue

// Invalid keys 
alert(map.get("unknownKey")); // undefined
alert(map.get(new Object())); // undefined

Here is a proposal, combining @Florian's solution with @Laurent's.

function HashTable() {
    this.hashes = [];
}

HashTable.prototype = {
    constructor: HashTable,

    put: function( key, value ) {
        this.hashes.push({
            key: key,
            value: value
        });
    },

    get: function( key ) {
        for( var i = 0; i < this.hashes.length; i++ ){
            if(this.hashes[i].key == key){
                return this.hashes[i].value;
            }
        }
    }
};

It wont change your object in any way and it doesn't rely on JSON.stringify.

I know that I am a year late, but for all others who stumble upon this thread, I've written the ordered object stringify to JSON, that solves the above noted dilemma: http://stamat.wordpress.com/javascript-object-ordered-property-stringify/

Also I was playing with custom hash table implementations which is also related to the topic: http://stamat.wordpress.com/javascript-quickly-find-very-large-objects-in-a-large-array/

//SORT WITH STRINGIFICATION

var orderedStringify = function(o, fn) {
    var props = [];
    var res = '{';
    for(var i in o) {
        props.push(i);
    }
    props = props.sort(fn);

    for(var i = 0; i < props.length; i++) {
        var val = o[props[i]];
        var type = types[whatis(val)];
        if(type === 3) {
            val = orderedStringify(val, fn);
        } else if(type === 2) {
            val = arrayStringify(val, fn);
        } else if(type === 1) {
            val = '"'+val+'"';
        }

        if(type !== 4)
            res += '"'+props[i]+'":'+ val+',';
    }

    return res.substring(res, res.lastIndexOf(','))+'}';
};

//orderedStringify for array containing objects
var arrayStringify = function(a, fn) {
    var res = '[';
    for(var i = 0; i < a.length; i++) {
        var val = a[i];
        var type = types[whatis(val)];
        if(type === 3) {
            val = orderedStringify(val, fn);
        } else if(type === 2) {
            val = arrayStringify(val);
        } else if(type === 1) {
            val = '"'+val+'"';
        }

        if(type !== 4)
            res += ''+ val+',';
    }

    return res.substring(res, res.lastIndexOf(','))+']';
}

Based on Peters answer, but with proper class design (not abusing closures), so the values are debuggable. Renamed from Map to ObjectMap , because Map is a builtin function. Also added the exists method:

ObjectMap = function() {
    this.keys = [];
    this.values = [];
}

ObjectMap.prototype.set = function(key, value) {
    var index = this.keys.indexOf(key);
    if (index == -1) {
        this.keys.push(key);
        this.values.push(value);
    } else {
        this.values[index] = value;
    }
}

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

ObjectMap.prototype.exists = function(key) {
    return this.keys.indexOf(key) != -1;
}

/*
    TestObject = function() {}

    testA = new TestObject()
    testB = new TestObject()

    om = new ObjectMap()
    om.set(testA, true)
    om.get(testB)
    om.exists(testB)
    om.exists(testA)
    om.exists(testB)
*/

Using JSON.stringify() is completely awkward to me, and gives the client no real control over how their keys are uniquely identified. The objects that are used as keys should have a hashing function, but my guess is that in most cases overriding the toString() method, so that they will return unique strings, will work fine:

var myMap = {};

var myKey = { toString: function(){ return '12345' }};
var myValue = 6;

// same as myMap['12345']
myMap[myKey] = myValue;

Obviously, toString() should do something meaningful with the object's properties to create a unique string. If you want to enforce that your keys are valid, you can create a wrapper and in the get() and put() methods, add a check like:

if(!key.hasOwnProperty('toString')){
   throw(new Error('keys must override toString()'));
}

But if you are going to go thru that much work, you may as well use something other than toString() ; something that makes your intent more clear. So a very simple proposal would be:

function HashTable() {
    this.hashes = {};
}

HashTable.prototype = {
    constructor: HashTable,

    put: function( key, value ) {
        // check that the key is meaningful, 
        // also will cause an error if primitive type
        if( !key.hasOwnProperty( 'hashString' ) ){
           throw( new Error( 'keys must implement hashString()' ) );
        }
        // use .hashString() because it makes the intent of the code clear
        this.hashes[ key.hashString() ] = value;
    },

    get: function( key ) {
        // check that the key is meaningful, 
        // also will cause an error if primitive type
        if( !key.hasOwnProperty( 'hashString' ) ){
           throw( new Error( 'keys must implement hashString()' ) );
        }
        // use .hashString() because it make the intent of the code clear
        return this.hashes[ key.hashString()  ];
    }
};

Inspired by @florian, here's a way where the id doesn't need JSON.stringify :

'use strict';

module.exports = HashTable;

function HashTable () {
  this.index = [];
  this.table = [];
}

HashTable.prototype = {

  constructor: HashTable,

  set: function (id, key, value) {
    var index = this.index.indexOf(id);
    if (index === -1) {
      index = this.index.length;
      this.index.push(id);
      this.table[index] = {};
    }
    this.table[index][key] = value;
  },

  get: function (id, key) {
    var index = this.index.indexOf(id);
    if (index === -1) {
      return undefined;
    }
    return this.table[index][key];
  }

};

I took @Ilya_Gazman solution and improved it by setting '_hashtableUniqueId' as a not enumerable property (it won't appear in JSON requests neither will be listed in for loops). Also removed UniqueId object, since it is enough using only HastTable function closure. For usage details please see Ilya_Gazman post

function HashTable() {
   var hash = new Object();

   return {
       put: function (key, value) {
           if(!HashTable.uid){
               HashTable.uid = 0;
           }
           if (typeof key === "string") {
               hash[key] = value;
           } else {
               if (key._hashtableUniqueId === undefined) {
                   Object.defineProperty(key, '_hashtableUniqueId', {
                       enumerable: false,
                       value: HashTable.uid++
                   });
               }
               hash[key._hashtableUniqueId] = value;
           }
       },
       get: function (key) {
           if (typeof key === "string") {
               return hash[key];
           }
           if (key._hashtableUniqueId === undefined) {
               return undefined;
           }
           return hash[key._hashtableUniqueId];
       }
   };
}

The best solution is to use WeakMap when you can (ie when you target browsers supporting it)

Otherwise you can use the following workaround (Typescript written and collision safe):

// Run this in the beginning of your app (or put it into a file you just import)
(enableObjectID)();

const uniqueId: symbol = Symbol('The unique id of an object');

function enableObjectID(): void {
    if (typeof Object['id'] !== 'undefined') {
        return;
    }

    let id: number = 0;

    Object['id'] = (object: any) => {
        const hasUniqueId: boolean = !!object[uniqueId];
        if (!hasUniqueId) {
            object[uniqueId] = ++id;
        }

        return object[uniqueId];
    };
}

Then you can simply get a unique number for any object in your code (like if would have been for pointer address)

let objectA = {};
let objectB = {};
let dico = {};

dico[(<any>Object).id(objectA)] = "value1";

// or 

dico[Object['id'](objectA);] = "value1";

// If you are not using typescript you don't need the casting

dico[Object.id(objectA)] = "value1"

I know I'm late, but here's a simple HashMap implementation:

 Function.prototype.toJSON = Function.prototype.toString; //taken from https://stackoverflow.com/questions/1249531/how-to-get-a-javascript-objects-class function getNativeClass(obj) { if (typeof obj === "undefined") return "undefined"; if (obj === null) return "null"; return Object.prototype.toString.call(obj).match(/^\\[object\\s(.*)\\]$/)[1]; } function globals() { if (typeof global === "object") //node return global; return this; } function lookup(x) { return globals()[x]; } function getAnyClass(obj) { if (typeof obj === "undefined") return "undefined"; if (obj === null) return "null"; return obj.constructor.name; } //taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#examples var getCircularReplacer = () => { const seen = new WeakSet(); return (key, value) => { if (typeof value === "object" && value !== null) { if (seen.has(value)) { return "[Circular]"; } seen.add(value); } return value; }; }; function encode(x) { if (typeof x === "object" && x !== null) { var y = myClone(x); x = Object.getPrototypeOf(x); for (var i = 0; i < Object.getOwnPropertyNames(y).length; i++) { //Make enumerable x[Object.getOwnPropertyNames(y)[i]] = y[Object.getOwnPropertyNames(y)[i]]; } } return getAnyClass(x) + " " + JSON.stringify(x, getCircularReplacer()); } function decode(x) { var a = x.split(" ").slice(1).join(" "); //OBJECT if (typeof lookup(x.split(" ")[0])) { return new (lookup(x.split(" ")[0]))(JSON.parse(a)) } else { return JSON.parse(a); } } //taken from https://github.com/feross/fromentries/blob/master/index.js /*! fromentries. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */ function fromEntries(iterable) { return [...iterable].reduce((obj, [key, val]) => { obj[key] = val return obj }, {}) } var toEnumerable = (obj) => { return fromEntries( Object.getOwnPropertyNames(obj).map(prop => [prop, obj[prop]]) ); }; //taken from https://stackoverflow.com/questions/41474986/how-to-clone-a-javascript-es6-class-instance function myClone(instanceOfBlah) { if (typeof instanceOfBlah !== "object" || !instanceOfBlah) { return instanceOfBlah; } const clone = Object.assign({}, toEnumerable(instanceOfBlah)); const Blah = instanceOfBlah.constructor; Object.setPrototypeOf(clone, Blah.prototype); return clone; } function HashMap(a) { if (typeof a === "undefined") { a = []; } a = Array.from(a); a = a.map((e) => [encode(e[0]), e[1]]); this.a = a; } HashMap.from = function (a) { var temp = myClone(a); //convert to array a = []; for (var i = 0; i < Object.getOwnPropertyNames(temp).length; i++) { a.push([Object.getOwnPropertyNames(temp)[i], temp[Object.getOwnPropertyNames(temp)[i]]]); } return new HashMap(a); } HashMap.prototype.put = function (x, y) { this.a.push([encode(x), y]); } HashMap.prototype.get = function (x) { var t1 = this.a.map((e) => e[0]); return this.a[t1.indexOf(encode(x))][1]; } HashMap.prototype.length = function () { return this.a.length; } HashMap.prototype.toString = function () { var result = []; for (var i = 0; i < this.length(); i++) { result.push(JSON.stringify(decode(this.a[i][0]), getCircularReplacer()) + " => " + this.a[i][1]); } return "HashMap {" + result + "}"; } var foo = new HashMap(); foo.put("SQRT3", Math.sqrt(3)); foo.put({}, "bar"); console.log(foo.get({})); console.log(foo.toString());

Note that it is ordered. Methods:

  • put : Adds an item
  • get : Access an item
  • from (static): Convert from a JavaScript object
  • toString : Convert to string

Minified and without the test:

function getNativeClass(t){return void 0===t?"undefined":null===t?"null":Object.prototype.toString.call(t).match(/^\[object\s(.*)\]$/)[1]}function globals(){return"object"==typeof global?global:this}function lookup(t){return globals()[t]}function getAnyClass(t){return void 0===t?"undefined":null===t?"null":t.constructor.name}Function.prototype.toJSON=Function.prototype.toString;var getCircularReplacer=()=>{const t=new WeakSet;return(e,r)=>{if("object"==typeof r&&null!==r){if(t.has(r))return"[Circular]";t.add(r)}return r}};function encode(t){if("object"==typeof t&&null!==t){var e=myClone(t);t=Object.getPrototypeOf(t);for(var r=0;r<Object.getOwnPropertyNames(e).length;r++)t[Object.getOwnPropertyNames(e)[r]]=e[Object.getOwnPropertyNames(e)[r]]}return getAnyClass(t)+" "+JSON.stringify(t,getCircularReplacer())}function decode(t){var e=t.split(" ").slice(1).join(" ");return lookup(t.split(" ")[0]),new(lookup(t.split(" ")[0]))(JSON.parse(e))}function fromEntries(t){return[...t].reduce((t,[e,r])=>(t[e]=r,t),{})}var toEnumerable=t=>fromEntries(Object.getOwnPropertyNames(t).map(e=>[e,t[e]]));function myClone(t){if("object"!=typeof t||!t)return t;const e=Object.assign({},toEnumerable(t)),r=t.constructor;return Object.setPrototypeOf(e,r.prototype),e}function HashMap(t){void 0===t&&(t=[]),t=(t=Array.from(t)).map(t=>[encode(t[0]),t[1]]),this.a=t}HashMap.from=function(t){var e=myClone(t);t=[];for(var r=0;r<Object.getOwnPropertyNames(e).length;r++)t.push([Object.getOwnPropertyNames(e)[r],e[Object.getOwnPropertyNames(e)[r]]]);return new HashMap(t)},HashMap.prototype.put=function(t,e){this.a.push([encode(t),e])},HashMap.prototype.get=function(t){var e=this.a.map(t=>t[0]);return this.a[e.indexOf(encode(t))][1]},HashMap.prototype.length=function(){return this.a.length},HashMap.prototype.toString=function(){for(var t=[],e=0;e<this.length();e++)t.push(JSON.stringify(decode(this.a[e][0]),getCircularReplacer())+" => "+this.a[e][1]);return"HashMap {"+t+"}"};

Also, you can customize the encoder and decoder by changing encode and decode functions.

As in florian's answer, you can't play with the reference in js however (so two empty objects will look like the same to the hashtable).

class Dict{
    constructor(){
        this.keys = [];
        this.values = [];
        this.set = this.set.bind(this);
    }

    set(key, value){
        this.keys.push(key);
        this.values.push(value);
    }

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

    all(){
        return this.keys.map((kk, ii)=>[kk, this.values[ii]]);
    }
}

let d1 = new Dict();

let k1 = {1: 'a'};
d1.set(k1, 2);
console.log(d1.get(k1));  // 2
let k2 = {2: 'b'};
d1.set(k2, 3);


console.log(d1.all());
// [ [ { '1': 'a' }, 2 ], [ { '2': 'b' }, 3 ] ]

When you say you don't want your Object keys converted into Strings, I'm going to assume it's because you just don't want the entire code contents of your Objects being used as keys. This, of course, makes perfect sense.

While there is no "hash table" in Javascript per-se, you can accomplish what you're looking for by simply overriding your Object's prototype.toString and returning a valid key value that will be unique to each instance. One way to do this is with Symbol() :

function Obj () {
    this.symbol = Symbol() // Guaranteed to be unique to each instance
}

Obj.prototype.toString = function () {
    return this.symbol // Return the unique Symbol, instead of Obj's stringified code
}

let a = new Obj()
let b = new Obj()

let table = {}

table[a] = 'A'
table[b] = 'B'

console.log(table)      // {Symbol(): 'A', Symbol(): 'B'}
console.log(table[a])   // A
console.log(table[b])   // B

Just use the strict equality operator when looking up the object: ===

var objects = [];
objects.push(object1);
objects.push(object2);

objects[0] === object1; // true
objects[1] === object1; // false

The implementation will depend on how you store the objects in the HashTable class.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM