如何在JavaScript中使用关联数组/哈希表

608

我需要使用JavaScript存储一些类似于C#中的统计数据:

Dictionary<string, int> statistics;

statistics["Foo"] = 10;
statistics["Goo"] = statistics["Goo"] + 1;
statistics.Add("Zoo", 1);

在JavaScript中是否有类似HashtableDictionary<TKey, TValue>的东西?
我该如何以这种方式存储值?


2
JavaScript是一种弱类型语言,因此无法仅声明字符串或整数,只能声明一个变量并将其赋值为字符串或整数。 :D - Gordon Gustafson
你可能想要查看 xDict。http://jsfiddle.net/very/MuVwd/ 它是一个用Javascript编写的字符串=>任何内容的字典。 - Robert
这篇文章非常好地解释了Javascript中关联数组在底层是如何实现的。https://www.jayconrod.com/posts/52/a-tour-of-v8-object-representation - Shuklaswag
被接受的答案是在2009年编写的 - 它只支持字符串键。对于非字符串键,请使用Map或WeakMap,就像Vitalii的答案中所述 - ToolmakerSteve
11个回答

592

使用JavaScript对象作为关联数组

关联数组:简单来说,关联数组使用字符串而不是整数作为索引。

创建一个对象:

var dictionary = {};

JavaScript允许使用以下语法向对象添加属性:
Object.yourProperty = value;

同样的语法的另一种形式是:
Object["yourProperty"] = value;

如果可以的话,还要按照以下语法创建键值对对象映射:
var point = { x:3, y:2 };

point["x"] // returns 3
point.y // returns 2

您可以使用以下的for..in循环结构迭代遍历关联数组:
for(var key in Object.keys(dict)){
  var value = dict[key];
  /* use key/value for intended purpose */
}

40
请注意,作者使用 new Array() 初始化“关联数组”的方法已经不被推荐。文章最终提到了它的缺点,并建议使用 new Object(){} 作为首选替代方案,但这几乎是在文章末尾,我担心大多数读者无法看到这里。 - Daniel Lubarov
27
失败了。JavaScript不支持使用对象引用作为键,而Flash/AS3字典等其他语言可以。在JavaScript中,var obj1 = {}; var obj2 = {}; var table= {}; table[obj1] = "A"; table[obj2] = "B"; alert(table[obj1]); //显示 B,因为它无法区分键obj1和obj2;它们都被转换为字符串并变成类似于“Object”的内容。这是一个完全的失败,在JavaScript中让类型安全的序列化保持引用和循环引用不变是困难或者性能不佳的。而在Flash/AS3中很容易实现。 - Triynko
如果您需要按对象而不是按字符串进行查找,则此方法无法正常工作。现实生活中的用例:在同一街道地址上绘制多个地图上的事物。(我的特定情况:在相同的GPS坐标下绘制多个坑洞。) - Mike Warren
1
@Leo console.log({A:'B',C:'D'}[foo]) 应该输出 A B。 - ychaouche
4
例子看起来有误。对于字典,for...in 循环会遍历它的键,因此 Object.keys 在那里似乎放错了位置。Object.keys 返回字典的键数组,而对于数组,for...in 循环遍历的是 它的 “键”,对于数组来说是它的索引,而不是它的值。 - JHH
显示剩余3条评论

440
var associativeArray = {};
associativeArray["one"] = "First";
associativeArray["two"] = "Second";
associativeArray["three"] = "Third";

如果你来自一门面向对象的编程语言,你应该查看这篇文章


44
你也可以用更少的代码实现这个:var associativeArray = {"one" : "First", "two" : "second", "three" : "Third"}; 这样,associativeArray["one"] 将返回 "First",associativeArray["four"] 则返回 null。 - Tony Wickham

184

所有现代浏览器都支持JavaScript的Map对象。以下是使用Map比Object更好的几个原因:

  • 对象有原型,所以映射中有默认键。
  • 对象的键是字符串,而在Map中可以是任何值。
  • 可以轻松获取Map的大小,而必须跟踪对象的大小。

示例:

var myMap = new Map();

var keyObj = {},
    keyFunc = function () {},
    keyString = "a string";

myMap.set(keyString, "value associated with 'a string'");
myMap.set(keyObj, "value associated with keyObj");
myMap.set(keyFunc, "value associated with keyFunc");

myMap.size; // 3

myMap.get(keyString);    // "value associated with 'a string'"
myMap.get(keyObj);       // "value associated with keyObj"
myMap.get(keyFunc);      // "value associated with keyFunc"

如果您希望未被其他对象引用的键可以被垃圾回收,请考虑使用WeakMap而不是Map。


9
希望几年后这将成为被投票最多的答案。 - Cameron Lee
2
当您的键是一个对象但应该按值而不是引用进行比较时,此Map几乎没有用处。 - Siyuan Ren
9
在此回答发布一年多后,"所有现代浏览器都支持Map"仍然不正确。只有在桌面上,您可以指望至少基本的Map支持,而移动设备则没有。例如,Android浏览器根本不支持Map。即使在桌面上,某些实现也是不完整的。例如,IE11仍不支持通过"for...of..."枚举,因此如果要兼容IE,则必须使用恶心的.forEach替代方法。此外,我尝试过的任何浏览器中,JSON.stringify()都不适用于Map。另外,在IE或Safari中不起作用的是初始值设定项。 - Dave Burton
1
这很安全,现在适用于桌面|基本支持| Chrome: v38; Edge: v12; Firefox: v13; IE: v11; Opera: v25; Safari: v7.1; 但仍不适用于移动设备|基本支持| Chrome: v38; Android: 无; Firefox: v13; IE Mobile: 无; Opera Mobile: 无; Safari Mobile: v8; - kayleeFrye_onDeck
4
浏览器的支持非常好。请再次检查。无论如何,这很容易进行填充,因此本地浏览器支持不是问题。 - Brad
显示剩余3条评论

134

除非你有特殊原因不使用,否则请使用普通对象。在 JavaScript 中,可以使用哈希表风格的语法引用对象属性:

Unless you have a specific reason not to, just use a normal object. Object properties in JavaScript can be referenced using hashtable-style syntax:
var hashtable = {};
hashtable.foo = "bar";
hashtable['bar'] = "foo";

现在可以将foobar元素都称为:

hashtable['foo'];
hashtable['bar'];

// Or
hashtable.foo;
hashtable.bar;

当然,这意味着你的键必须是字符串。如果它们不是字符串,则会在内部转换为字符串,因此可能仍然可用。但结果可能有所不同。


10
请注意,当设置属性时,您的整数会转换为字符串:var hash = {}; hash[1] = "foo"; alert(hash["1"]); 弹出窗口显示 "foo"。 - Tim Down
1
在上面的例子中,hashtable['foo'],我如何删除 'foo' 的键值组合。也就是说,类似于 Python 的 dict.pop(keyname)。 - Nanda Kishore
18
如果你的其中一个键是 "proto" 或 "parent",会发生什么?(说明:该问题涉及JavaScript编程语言中对象原型和继承属性的命名约定) - PleaseStand
7
注意,在JavaScript中,对象不能用作键。虽然可以使用对象作为键,但它们会被转换为它们的字符串表示形式,因此任何对象最终都会成为完全相同的键。请参见@TimDown在下面提出的jshashtable建议。 - ericsoco
21
这个例子很令人困惑,因为在两个实例中你都使用了foo和bar作为键和值。更清晰的做法是展示var dict = {}; dict.key1 = "val1"; dict["key2"] = "val2";,dict的key1元素可以通过dict["key1"]dict.key1等价地引用。 - Jim
显示剩余5条评论

50

由于JavaScript中的每个对象都像哈希表一样运作,并且通常是以哈希表实现的,因此我就沿用了这种方式...

var hashSweetHashTable = {};

30
因为没有展示如何实际访问“哈希表”中的值,所以被踩了。 - IQAndreas
我晚了9年(当时我对编程一无所知,更不用说这个网站了),但是...如果您正在尝试在地图上存储点,并且需要查看某些东西是否已经在地图上的某个点上,那么您最好使用HashTable来进行此操作,通过坐标(一个对象,而不是字符串)进行查找。 - Mike Warren
@MikeWarren 如果 hashSweetHashTable.foo 被设置,那么应该进入 if 代码块。 - Koray Tugay

22

在C#中,代码看起来像这样:

Dictionary<string,int> dictionary = new Dictionary<string,int>();
dictionary.add("sample1", 1);
dictionary.add("sample2", 2);
或者
var dictionary = new Dictionary<string, int> {
    {"sample1", 1},
    {"sample2", 2}
};

在JavaScript中:

var dictionary = {
    "sample1": 1,
    "sample2": 2
}

C#字典对象包含有用的方法,比如dictionary.ContainsKey()

在JavaScript中,我们可以使用hasOwnProperty,例如:

if (dictionary.hasOwnProperty("sample1"))
    console.log("sample1 key found and its value is"+ dictionary["sample1"]);

1
点赞,因为我不必写关于hasOwnProperty的答案。 - brichins

18
如果你需要的密钥不仅仅是字符串,而是任意对象,那么你可以使用我的jshashtable

3
在我找到这个之前,我在摸索JS风格的对象作为关联数组时,花了多少时间在对象实际上不能用作键上?谢谢你,Tim。 - ericsoco
1
Flash/AS3字典,以及大多数其他语言,都支持将对象引用作为键。JavaScript仍然没有实现它,但我认为它将作为某种Map类在未来的规范中出现。与此同时,使用polyfills;标准就是这样。哦,等等...终于在2015年,Map似乎已经到来了:https://dev59.com/T3M_5IYBdhLWcg3w1G6N#30088129,并且被“现代”浏览器支持,哈哈:http://kangax.github.io/compat-table/es6/#Map(但并不被广泛支持)。只比AS3慢了十年。 - Triynko
1
Tim,也许你应该更新jshashtable,以便在可用时使用Map()。 - Dave Burton
1
@DaveBurton:好计划。我一有时间就会这样做。 - Tim Down

10

提示:

几年前,我实现了以下哈希表,它有一些 Map 类缺失的功能。然而,现在这些特性已经不再缺失了——现在可以对一个 Map 进行条目迭代,获取其键、值或二者的数组(这些操作会复制到新分配的数组中,这是一种浪费内存的做法,其时间复杂度总是慢于 O(n)),删除指定键的特定项,并清空整个映射。
因此,我的哈希表实现仅用于兼容性目的,在这种情况下,基于此编写适当的 polyfill 将是一种更明智的方法。


function Hashtable() {

    this._map = new Map();
    this._indexes = new Map();
    this._keys = [];
    this._values = [];

    this.put = function(key, value) {
        var newKey = !this.containsKey(key);
        this._map.set(key, value);
        if (newKey) {
            this._indexes.set(key, this.length);
            this._keys.push(key);
            this._values.push(value);
        }
    };

    this.remove = function(key) {
        if (!this.containsKey(key))
            return;
        this._map.delete(key);
        var index = this._indexes.get(key);
        this._indexes.delete(key);
        this._keys.splice(index, 1);
        this._values.splice(index, 1);
    };

    this.indexOfKey = function(key) {
        return this._indexes.get(key);
    };

    this.indexOfValue = function(value) {
        return this._values.indexOf(value) != -1;
    };

    this.get = function(key) {
        return this._map.get(key);
    };

    this.entryAt = function(index) {

        var item = {};

        Object.defineProperty(item, "key", {
            value: this.keys[index],
            writable: false
        });

        Object.defineProperty(item, "value", {
            value: this.values[index],
            writable: false
        });

        return item;
    };

    this.clear = function() {

        var length = this.length;

        for (var i = 0; i < length; i++) {
            var key = this.keys[i];
            this._map.delete(key);
            this._indexes.delete(key);
        }

        this._keys.splice(0, length);
    };

    this.containsKey = function(key) {
        return this._map.has(key);
    };

    this.containsValue = function(value) {
        return this._values.indexOf(value) != -1;
    };

    this.forEach = function(iterator) {
        for (var i = 0; i < this.length; i++)
            iterator(this.keys[i], this.values[i], i);
    };

    Object.defineProperty(this, "length", {
        get: function() {
            return this._keys.length;
        }
    });

    Object.defineProperty(this, "keys", {
        get: function() {
            return this._keys;
        }
    });

    Object.defineProperty(this, "values", {
        get: function() {
            return this._values;
        }
    });

    Object.defineProperty(this, "entries", {
        get: function() {
            var entries = new Array(this.length);
            for (var i = 0; i < entries.length; i++)
                entries[i] = this.entryAt(i);
            return entries;
        }
    });
}

Hashtable 类的文档

方法:

  • get(key)

    返回指定键所关联的值。

    参数:
    key:要检索值的键。


  • put(key, value)

    将指定的值关联到指定的键上。

    参数:
    key:要关联值的键。
    value:要关联到键上的值。


  • remove(key)

    移除指定的键及其关联的值。

    参数:
    key:要移除的键。


  • clear()

    清空整个哈希表,即移除所有条目。


  • indexOfKey(key)

    按条目添加的顺序返回指定键的索引。

    参数:
    key:要获取索引的键。


  • indexOfValue(value)

    按条目添加的顺序返回指定值的索引。

    参数:
    value:要获取索引的值。

    备注:
    此处使用的是“引用相等(identity)”而非“值相等(equality)”进行比较。


  • entryAt(index)

    返回一个对象,该对象具有“key”和“value”属性,表示指定索引处的条目。

    参数:
    index:要获取条目的索引。


  • containsKey(key)

    返回哈希表是否包含指定的键。

    参数: key:要查找的键。


  • containsValue(value)

    返回散列表是否包含指定值。

    参数:
    value:要查找的值。


  • forEach(iterator)

    遍历散列表中的所有条目,调用指定的iterator方法。

    参数:
    iterator:一个具有三个参数的方法,keyvalueindex,其中index表示按添加顺序排列的条目的索引。

属性:

  • length(只读)

    获取散列表中条目的数量。

  • keys(只读)

    获取散列表中所有键的数组。

  • values(只读)

    获取散列表中所有值的数组。

  • entries(只读)

    获取散列表中所有条目的数组。它们与方法entryAt()表示的方式相同。


6
function HashTable() {
    this.length = 0;
    this.items = new Array();
    for (var i = 0; i < arguments.length; i += 2) {
        if (typeof (arguments[i + 1]) != 'undefined') {
            this.items[arguments[i]] = arguments[i + 1];
            this.length++;
        }
    }

    this.removeItem = function (in_key) {
        var tmp_previous;
        if (typeof (this.items[in_key]) != 'undefined') {
            this.length--;
            var tmp_previous = this.items[in_key];
            delete this.items[in_key];
        }

        return tmp_previous;
    }

    this.getItem = function (in_key) {
        return this.items[in_key];
    }

    this.setItem = function (in_key, in_value) {
        var tmp_previous;
        if (typeof (in_value) != 'undefined') {
            if (typeof (this.items[in_key]) == 'undefined') {
                this.length++;
            } else {
                tmp_previous = this.items[in_key];
            }

            this.items[in_key] = in_value;
        }

        return tmp_previous;
    }

    this.hasItem = function (in_key) {
        return typeof (this.items[in_key]) != 'undefined';
    }

    this.clear = function () {
        for (var i in this.items) {
            delete this.items[i];
        }

        this.length = 0;
    }
}

1
对于那些给这个回答点踩的人,能否请您评论一下原因?这个回答是在2011年发布的,而不是现在。 - Birey
2
我没有点踩,但是...你不应该把数组当作对象使用。我不确定这是否是你的意图。对于数组,请使用slice而不是delete进行重新索引;delete也可以,但会将其设置为undefined——最好明确指定;对于对象,也要使用= undefined,因为它更快(但占用更多内存)。简而言之:如果您打算使用字符串键,则始终使用对象:{}而不是数组:[]new Array(),否则js引擎会出现问题——它将看到1个变量的2种类型,这意味着无法优化,或者它将运行数组并意识到必须更改为对象(可能需要重新分配)。 - Graeme Wicksted
3
就像Alex Hawkins的回答一样,请提供一些解释,为什么这个看起来相当复杂的代码实际上比其他较短的答案更有用。 - Thomas Tempelmann

2

https://gist.github.com/alexhawkins/f6329420f40e5cafa0a4

var HashTable = function() {
  this._storage = [];
  this._count = 0;
  this._limit = 8;
}


HashTable.prototype.insert = function(key, value) {

  // Create an index for our storage location by passing
  // it through our hashing function
  var index = this.hashFunc(key, this._limit);

  // Retrieve the bucket at this particular index in
  // our storage, if one exists
  //[[ [k,v], [k,v], [k,v] ] , [ [k,v], [k,v] ]  [ [k,v] ] ]
  var bucket = this._storage[index]

  // Does a bucket exist or do we get undefined
  // when trying to retrieve said index?
  if (!bucket) {
    // Create the bucket
    var bucket = [];
    // Insert the bucket into our hashTable
    this._storage[index] = bucket;
  }

  var override = false;

  // Now iterate through our bucket to see if there are any conflicting
  // key value pairs within our bucket. If there are any, override them.
  for (var i = 0; i < bucket.length; i++) {
    var tuple = bucket[i];
    if (tuple[0] === key) {

      // Override value stored at this key
      tuple[1] = value;
      override = true;
    }
  }

  if (!override) {
    // Create a new tuple in our bucket.
    // Note that this could either be the new empty bucket we created above
    // or a bucket with other tupules with keys that are different than
    // the key of the tuple we are inserting. These tupules are in the same
    // bucket because their keys all equate to the same numeric index when
    // passing through our hash function.
    bucket.push([key, value]);
    this._count++

    // Now that we've added our new key/val pair to our storage
    // let's check to see if we need to resize our storage
    if (this._count > this._limit * 0.75) {
      this.resize(this._limit * 2);
    }
  }
  return this;
};


HashTable.prototype.remove = function(key) {
  var index = this.hashFunc(key, this._limit);
  var bucket = this._storage[index];
  if (!bucket) {
    return null;
  }

  // Iterate over the bucket
  for (var i = 0; i < bucket.length; i++) {
    var tuple = bucket[i];

    // Check to see if key is inside bucket
    if (tuple[0] === key) {

      // If it is, get rid of this tuple
      bucket.splice(i, 1);
      this._count--;
      if (this._count < this._limit * 0.25) {
        this._resize(this._limit / 2);
      }
      return tuple[1];
    }
  }
};


HashTable.prototype.retrieve = function(key) {
  var index = this.hashFunc(key, this._limit);
  var bucket = this._storage[index];

  if (!bucket) {
    return null;
  }

  for (var i = 0; i < bucket.length; i++) {
    var tuple = bucket[i];
    if (tuple[0] === key) {
      return tuple[1];
    }
  }

  return null;
};


HashTable.prototype.hashFunc = function(str, max) {
  var hash = 0;
  for (var i = 0; i < str.length; i++) {
    var letter = str[i];
    hash = (hash << 5) + letter.charCodeAt(0);
    hash = (hash & hash) % max;
  }
  return hash;
};


HashTable.prototype.resize = function(newLimit) {
  var oldStorage = this._storage;

  this._limit = newLimit;
  this._count = 0;
  this._storage = [];

  oldStorage.forEach(function(bucket) {
    if (!bucket) {
      return;
    }
    for (var i = 0; i < bucket.length; i++) {
      var tuple = bucket[i];
      this.insert(tuple[0], tuple[1]);
    }
  }.bind(this));
};


HashTable.prototype.retrieveAll = function() {
  console.log(this._storage);
  //console.log(this._limit);
};

/******************************TESTS*******************************/

var hashT = new HashTable();

hashT.insert('Alex Hawkins', '510-599-1930');
//hashT.retrieve();
//[ , , , [ [ 'Alex Hawkins', '510-599-1930' ] ] ]
hashT.insert('Boo Radley', '520-589-1970');
//hashT.retrieve();
//[ , [ [ 'Boo Radley', '520-589-1970' ] ], , [ [ 'Alex Hawkins', '510-599-1930' ] ] ]
hashT.insert('Vance Carter', '120-589-1970').insert('Rick Mires', '520-589-1970').insert('Tom Bradey', '520-589-1970').insert('Biff Tanin', '520-589-1970');
//hashT.retrieveAll();
/*
[ ,
  [ [ 'Boo Radley', '520-589-1970' ],
    [ 'Tom Bradey', '520-589-1970' ] ],
  ,
  [ [ 'Alex Hawkins', '510-599-1930' ],
    [ 'Rick Mires', '520-589-1970' ] ],
  ,
  ,
  [ [ 'Biff Tanin', '520-589-1970' ] ] ]
*/

// Override example (Phone Number Change)
//
hashT.insert('Rick Mires', '650-589-1970').insert('Tom Bradey', '818-589-1970').insert('Biff Tanin', '987-589-1970');
//hashT.retrieveAll();

/*
[ ,
  [ [ 'Boo Radley', '520-589-1970' ],
    [ 'Tom Bradey', '818-589-1970' ] ],
  ,
  [ [ 'Alex Hawkins', '510-599-1930' ],
    [ 'Rick Mires', '650-589-1970' ] ],
  ,
  ,
  [ [ 'Biff Tanin', '987-589-1970' ] ] ]

*/

hashT.remove('Rick Mires');
hashT.remove('Tom Bradey');
//hashT.retrieveAll();

/*
[ ,
  [ [ 'Boo Radley', '520-589-1970' ] ],
  ,
  [ [ 'Alex Hawkins', '510-599-1930' ] ],
  ,
  ,
  [ [ 'Biff Tanin', '987-589-1970' ] ] ]


*/

hashT.insert('Dick Mires', '650-589-1970').insert('Lam James', '818-589-1970').insert('Ricky Ticky Tavi', '987-589-1970');
hashT.retrieveAll();


/* NOTICE HOW THE HASH TABLE HAS NOW DOUBLED IN SIZE UPON REACHING 75% CAPACITY, i.e. 6/8. It is now size 16.
 [,
  ,
  [ [ 'Vance Carter', '120-589-1970' ] ],
  [ [ 'Alex Hawkins', '510-599-1930' ],
    [ 'Dick Mires', '650-589-1970' ],
    [ 'Lam James', '818-589-1970' ] ],
  ,
  ,
  ,
  ,
  ,
  [ [ 'Boo Radley', '520-589-1970' ],
    [ 'Ricky Ticky Tavi', '987-589-1970' ] ],
  ,
  ,
  ,
  ,
  [ [ 'Biff Tanin', '987-589-1970' ] ] ]

*/

console.log(hashT.retrieve('Lam James'));  // 818-589-1970
console.log(hashT.retrieve('Dick Mires')); // 650-589-1970
console.log(hashT.retrieve('Ricky Ticky Tavi')); //987-589-1970
console.log(hashT.retrieve('Alex Hawkins')); // 510-599-1930
console.log(hashT.retrieve('Lebron James')); // null

3
看起来不错。现在,请解释为什么这很有用,可能比其他回答更适合。 - Thomas Tempelmann
将数据存储在数组中不是违背哈希表的初衷吗? - Riza Khan

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