JavaScript 哈希表使用对象键

47

我想要创建一个哈希表,其中的Object键不会被转换为字符串。

就像这样:

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

编辑:请查看我的答案获取完整解决方案


7
在ES6中,你可以使用WeakMap来实现这个目的。 - Anna B
2
WeakMap - neaumusic
以上两个链接都已失效,最新的链接为:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap。 - Koray Tugay
14个回答

24

这里是一个简单的Map实现,可以使用任何类型的键,包括对象引用,并且它不会以任何方式改变键:

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)];
        }
    };
}
虽然这与哈希表具有相同的功能,但它实际上不是使用哈希函数实现的,因为它迭代数组并具有最坏情况下O(n)的性能。 但是,在绝大多数合理的用例中,这根本不应该成为问题。JavaScript引擎通过高度优化来实现indexOf函数。

3
给未来的读者:请注意,Map 不再是一个好的命名,因为它与 JavaScript 内置的 Map 类型 冲突。 - Sasha Chedygov

20

这里是一个提议:

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

这个API与您的问题中所示完全相同。

然而,在 JavaScript 中,您无法使用引用进行操作(因此两个空对象将在散列表中看起来相同),因为您无法获取它。请参见此答案以了解更多细节:如何获取 JavaScript 对象引用或引用计数?

Jsfiddle演示:http://jsfiddle.net/HKz3e/

不过,对于唯一性方面,您可以操作原始对象,像这样:

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演示:http://jsfiddle.net/HKz3e/2/

这意味着您的对象需要有一个名为id的属性,您不会在其他地方使用它。如果您想将此属性设置为不可枚举,请查看defineProperty(但它不跨浏览器兼容,即使使用ES5-Shim,在IE7中也无法正常工作)。

这也意味着您在此哈希表中存储的项目数量受到限制。受限于2 53个。

现在,这是“无处可用”的解决方案:使用ES6 WeakMaps。它们正是出于这个目的而设计的:将对象作为键。建议您阅读MDN以获取更多信息:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap

但与您的API略有不同(它是set而不是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"

这是一个包含 WeakMap polyfill 的 JsFiddle 演示:http://jsfiddle.net/Ralt/HKz3e/9/

但是,WeakMap 已在 Firefox 和 Chrome 中实现(仅当您在 Chrome 中启用“实验性 JavaScript 特性”标志时才可用)。有可用的 shims,例如此处提供的: https://gist.github.com/1269991。请自行决定是否使用。

您也可以使用 Maps,它们可能更适合您的需求,因为您需要将原始值(字符串)作为键存储。 有关文档和 shim,参见:DocShim


1
这确实是一个解决方案,我只是想知道JSON.stringify有多重。 - Ilya Gazman
27
这是错误的。你不能依赖于JSON.stringify()中键的顺序。即使它们“大多数情况下”会返回相同的字符串,具有相同键和值的不同对象未必会返回相同的字符串。请参阅javascript - Is there a deterministic equivalent of JSON.stringify?以获取更好的(但不是简单的JSON.stringify())对象字符串。 - Peter V. Mørch
@Peter 实际上,正如你所链接的答案所说,JSON.stringify 在相同的实现上是确定性的。如果你真的想要在不同的实现中实现确定性顺序(这在那里并不需要),那么这很容易实现。 - Florian Margaine
3
我在那个问题中没有看到明确说明它应该在相同的实现下是确定性的。尽管如此,MDN的stringify页面说“非数组对象的属性不能保证按特定顺序串行化。不要依赖于同一对象内属性的顺序。"即使今天它能正常工作,也不能保证明天会一直这样。 - Peter V. Mørch
1
现在是2018年,WeakMaps的支持更好了。 - rebelzach
显示剩余12条评论

11

我采纳了 @Florian Margaine 的建议并将其提升到更高的水平,最终得出了这个:

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

用法

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

很高兴你解决了这个问题 :) - Florian Margaine
2
这需要在调用"put()"时始终保留对用作键的对象的引用。如果您要这样做,为什么不直接保留对值的引用,而要首先映射它们呢?您考虑过如果有两个等效但不是同一对象(!==),会发生什么吗?它们应该哈希到相同的值,但是"get()"方法将对使用"put()"中的确切对象之外的任何有效键失败,因为只向其中一个添加了"._hashtableUniqueId"属性。 - sethro
很棒的解决方案!我通过使用Object.defineProperty将_hashtableUniqueId添加到键中并将其配置为不可枚举来改进了此解决方案,因此它不会出现在例如json请求中。此外,UniqueID对象也是不必要的。您可以查看我的答案以获取详细信息。 - edrian
不,@ShawnMoore,这没问题。你会得到“101”。_hashtableUniqueId等于某个随机数。 - Ilya Gazman

2
这里有一个提议,结合了@Florian的解决方案和@Laurent的方案。
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;
            }
        }
    }
};

它不会改变您的对象,也不依赖于JSON.stringify。

6
问题在于,它并不是一个“哈希”表;没有进行哈希。你只是为一个数组创建了一个误导性的封装。另外,由于使用了“==”,当使用混合类型作为键时,结果将是不可预测的。参考链接:http://www.informit.com/articles/article.aspx?p=1997934&seqNum=5。 - sethro

1
当你说你不想让你的对象键转换成字符串时,我会认为这是因为你不想让对象的整个代码内容被用作键。当然,这是完全有道理的。
虽然在 Javascript 中没有“哈希表”,但你可以通过简单地覆盖对象的原型 toString 并返回一个有效的键值来实现你想要的功能,这个键值将对每个实例都是唯一的。其中一种方法是使用 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

1

基于Peter的回答,但使用适当的类设计(不滥用闭包),以便值得到调试。将名称从Map重命名为ObjectMap,因为Map是一个内置函数。还添加了exists方法:

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)
*/

我使用闭包来强制封装 - 我们正在创建一个抽象。底层数据结构应仅可通过我们明确的公共接口进行可变操作。这减少了应用程序代码中出现错误/故障/误用的可能性。我们仍然可以在调试器中调试内部,或添加其他方便的方法,例如 hasKey()size() - Peter
@Peter,这只是通过F12/devtools不允许新开发人员真正理解内部结构而使理解变得模糊。你认为自己做了好事,但实际上它阻止了理解,因此导致了错误的产生。 - kungfooman
抽象化背后的整个思想是允许我们在更高的层次上思考我们试图解决的问题。Map 定义了一个契约并允许我们卸载一定量的心理负担。将其视为任何其他 JS 引擎内置功能。软件系统的全部内容都是关于在抽象之上构建抽象,直到我们可以用“我们想要什么”而不是“如何”来表达。这确实是防止大型项目(具有数百个贡献者)崩溃的唯一因素。 - Peter
抱歉,我不是有意针对个人。你说得对——像核心库、游戏引擎、内核、设备驱动程序等等都需要做出这种权衡——而且有很好的理由——以最大化性能。这需要一个高技能和纪律性强的团队。我所说的是在可能的情况下进行风险缓解。 - Peter
如果您在严格模式下定义变量时没有使用 var 关键字,则代码将无法正常工作。 - Nirvana
显示剩余2条评论

1
我知道我晚了一年,但对于所有其他遇到这个问题的人,我已经编写了有序对象的JSON字符串化,解决了上述问题:http://stamat.wordpress.com/javascript-object-ordered-property-stringify/ 此外,我正在尝试自定义哈希表实现,这也与该主题相关: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(','))+']';
}

0
受@florian的启发,这里有一种方法,其中id不需要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];
  }

};

0

最好的解决方案是在可能的情况下(即针对支持它的浏览器),使用WeakMap

否则,您可以使用以下解决方法(使用Typescript编写并且避免碰撞):

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

然后,您可以为代码中的任何对象获取唯一的数字(就像指针地址一样)

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"

0
我采用了@Ilya_Gazman的解决方案,并通过将“_hashtableUniqueId”设置为不可枚举属性来进行了改进(它不会出现在JSON请求中,也不会在for循环中列出)。同时删除了UniqueId对象,因为仅使用HastTable函数闭包就足够了。有关使用详细信息,请参阅Ilya_Gazman的帖子。
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];
       }
   };
}

如果我将一个项目映射到两个不同的哈希表中会怎样?您将获得重复的id...为了避免这种情况,我使用了UniqueId对象。Jsons也是不错的选择。 - Ilya Gazman
@Ilya_Gazman 感谢注意到这个问题!已经修复了上面的代码。在我的 AngularJS 实现中没有遇到这个问题,因为工厂方法中多了一个闭包。 - edrian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接