JavaScript 哈希映射等效项问题描述尤金的解决方案我的实现Map例进一步的考虑

正如更新3中关于这个答案所明确指出的那样,这个符号:

var hash = {};
hash[X]

实际上并没有对对象进行哈希处理 ;它实际上只是转换为字符串(如果它是一个对象,或者各种基元类型的其他一些内置转换),然后在“”中查找该字符串,而不对其进行哈希处理。对象相等性也不被检查 - 如果两个不同的对象具有相同的字符串转换,它们将相互覆盖。XX.toString()hash

鉴于此 - 在JavaScript中是否有任何有效的哈希映射实现?

(例如,javascript hashmap的第二个Google结果会生成一个实现,对于任何操作都是O(n)。其他各种结果忽略了具有等效字符串表示形式的不同对象相互覆盖的事实。


答案 1

自己手动散列对象,并将生成的字符串用作常规 JavaScript 字典的键。毕竟,您处于最佳位置,可以知道是什么使您的对象独一无二。这就是我的工作。

例:

var key = function(obj){
  // Some unique object-dependent key
  return obj.totallyUniqueEmployeeIdKey; // Just an example
};

var dict = {};

dict[key(obj1)] = obj1;
dict[key(obj2)] = obj2;

通过这种方式,您可以控制 JavaScript 完成的索引,而无需繁重的内存分配和溢出处理。

当然,如果你真的想要“工业级解决方案”,你可以构建一个由关键功能参数化的类,并使用容器的所有必要API,但是......我们使用JavaScript,并试图简单和轻量级,所以这个功能解决方案简单快捷。

密钥功能可以像选择对象的正确属性(例如,密钥)或一组已经唯一的密钥一样简单,也可以是密钥组合(它们在一起是唯一的),或者像使用某些加密哈希(如 DojoX 编码DojoX UUID)一样复杂。虽然后一种解决方案可能会产生唯一的键,但就我个人而言,我尽量不惜一切代价避免使用它们,特别是如果我知道是什么让我的对象独一无二。

2014年更新:早在2008年回答了这个简单的解决方案仍然需要更多的解释。让我以问答的形式澄清这个想法。

您的解决方案没有真正的哈希值。它在哪里???

JavaScript是一种高级语言。其基本基元(对象)包括一个哈希表来保留属性。为了提高效率,此哈希表通常以低级语言编写。使用带有字符串键的简单对象,我们使用高效实现的哈希表,而无需我们付出任何努力。

你怎么知道他们使用哈希?

有三种主要方法可以使对象集合可通过键寻址:

  • 无序。在这种情况下,要按键检索对象,我们必须在找到它时检查所有键。平均而言,它需要n/ 2比较。
  • 命令。
    • 示例#1:一个排序数组 - 执行二进制搜索,我们将在平均~log2(n)比较后找到我们的键。好多了。
    • 示例 #2:一棵树。同样,它将是 ~log(n) 次尝试。
  • 哈希表。平均而言,它需要恒定的时间。比较:O(n) vs. O(log n) vs. O(1)。繁荣。

显然,JavaScript对象以某种形式使用哈希表来处理一般情况。

浏览器供应商真的使用哈希表吗???

真。

它们是否处理碰撞?

是的。见上文。如果您发现不相等的字符串发生冲突,请不要犹豫,向供应商提交错误。

那么你的想法是什么?

如果要对对象进行哈希处理,请找到使其唯一的原因并将其用作键。不要试图计算一个真正的哈希或模拟哈希表 - 它已经被底层的JavaScript对象有效地处理了。

将此密钥与JavaScript结合使用,以利用其内置的哈希表,同时避免与默认属性的可能冲突。Object

帮助您入门的示例:

  • 如果您的对象包含唯一的用户名 , 请将其用作键。
  • 如果它包含唯一的客户编号 , 请将其用作键。
    • 如果它包含唯一的政府颁发的号码(如美国 SSN)或护照号码,并且您的系统不允许重复,请将其用作密钥。
  • 如果字段组合是唯一的 , 请将其用作键。
    • 美国州缩写+驾驶执照号码是一把好钥匙。
    • 国家缩写+护照号码也是一个很好的钥匙。
  • 字段或整个对象上的某些函数可以返回唯一值 — 将其用作键。

我使用了您的建议,并使用用户名缓存了所有对象。但是一些聪明的家伙被命名为“toString”,这是一个内置的财产!我现在该怎么办?

显然,如果生成的密钥完全可能由拉丁字符组成,则应对此执行一些操作。例如,在开头或结尾添加您喜欢的任何非拉丁 Unicode 字符,以消除与默认属性“#toString”、“#MarySmith”的冲突。如果使用复合键,请使用某种非拉丁语分隔符分隔关键组件:“名称,城市,州”。

通常,这是我们必须发挥创造力并选择具有给定限制(唯一性,与默认属性的潜在冲突)的最简单键的地方。

注意:根据定义,唯一键不会发生冲突,而潜在的哈希冲突将由底层 .Object

您为什么不喜欢工业解决方案?

恕我直言,最好的代码根本没有代码:它没有错误,不需要维护,易于理解,并且可以立即执行。我看到的所有“JavaScript中的哈希表”都是>100行代码,涉及多个对象。将其与以下项进行比较: .dict[key] = value

另一点:是否有可能击败用低级语言编写的原始对象的性能,使用JavaScript和完全相同的原始对象来实现已经实现的内容?

我仍然想在没有任何键的情况下散列我的对象!

我们很幸运:ECMAScript 6(2015 年 6 月发布)定义了 mapset

从定义来看,他们可以使用对象的地址作为键,这使得对象立即变得明显,而无需人工键。OTOH,两个不同但相同的对象,将被映射为不同。

MDN的比较细分:

对象与 Maps 类似,因为两者都允许您将键设置为值、检索这些值、删除键以及检测某些内容是否存储在键上。因此(并且由于没有内置的替代方案),对象在历史上一直被用作地图;但是,有一些重要的区别使得在某些情况下使用Map更可取:

  • 对象的键是字符串和符号,而它们可以是 Map 的任何值,包括函数、对象和任何基元。
  • Map 中的键是有序的,而添加到对象的键不是。因此,在迭代它时,Map 对象将按插入顺序返回键。
  • 您可以使用 size 属性轻松获取 Map 的大小,而对象中的属性数必须手动确定。
  • Map 是可迭代的,因此可以直接迭代,而迭代对象需要以某种方式获取其键并迭代它们。
  • 对象有一个原型,因此地图中有一些默认键,如果您不小心,它们可能会与您的键发生冲突。从ES5开始,这可以通过使用map = Object.create(null)来绕过,但这很少这样做。
  • 在涉及频繁添加和删除密钥对的情况下,Map 的性能可能更好。

答案 2

问题描述

JavaScript没有内置的通用映射类型(有时称为关联数组字典),它允许通过任意键访问任意值。JavaScript的基本数据结构是对象,一种特殊类型的映射,它只接受字符串作为键,并具有特殊的语义,如原型继承,getters和setters以及一些进一步的巫毒教。

使用对象作为映射时,必须记住,键将通过 转换为字符串值,这将导致映射和相同的值,并且所有对象都不会将方法覆盖到 索引的值。如果不检查 ,您还可能不由自主地访问其继承的属性。toString()5'5'toString()'[object Object]'hasOwnProperty()

JavaScript的内置数组类型一点也无济于事:JavaScript数组不是关联数组,而只是具有更多特殊属性的对象。如果您想知道为什么它们不能用作地图,请查看此处

尤金的解决方案

Eugene Lazutkin已经描述了使用自定义哈希函数生成唯一字符串的基本思想,这些字符串可用于将关联值查找为字典对象的属性。这很可能是最快的解决方案,因为对象在内部实现为哈希表

  • 注意:哈希表(有时称为哈希映射)是使用支持数组和通过数字哈希值进行查找的映射概念的特定实现。运行时环境可能使用其他结构(如搜索树跳过列表)来实现 JavaScript 对象,但由于对象是基本数据结构,因此应对其进行充分优化。

为了获得任意对象的唯一哈希值,一种可能性是使用全局计数器并将哈希值缓存在对象本身中(例如,在名为 的属性中)。__hash

执行此操作的哈希函数适用于基元值和对象,如下所示:

function hash(value) {
    return (typeof value) + ' ' + (value instanceof Object ?
        (value.__hash || (value.__hash = ++arguments.callee.current)) :
        value.toString());
}

hash.current = 0;

这个函数可以按照 Eugene 的描述使用。为方便起见,我们将进一步将其包装在一个类中。Map

我的实现Map

以下实现还将键值对存储在双链表中,以便允许对键和值进行快速迭代。要提供您自己的哈希函数,您可以在创建后覆盖实例的方法。hash()

// Linking the key-value-pairs is optional.
// If no argument is provided, linkItems === undefined, i.e. !== false
// --> 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 an existing object
// doesn't add inherited properties if not explicitly instructed to:
// omitting foreignKeys means foreignKeys === undefined, i.e. == false
// --> 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;
};

以下脚本,

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++ < map.size; map.next())
    document.writeln(map.hash(map.key()) + ' : ' + map.value());

生成此输出:

string spam : eggs
string foo : baz
object 1 : an object
object 2 : another object
number 5 : five again
string 5 : another five

进一步的考虑

PEZ建议覆盖该方法,大概是用我们的哈希函数。这是不可行的,因为它不适用于基元值(更改基元是一个非常糟糕的主意)。如果我们想为任意对象返回有意义的值,我们将不得不修改 ,有些人(不包括我自己)认为这是verbotentoString()toString()toString()Object.prototype


我的实现的当前版本以及其他JavaScript好东西都可以从这里获得Map