问题描述
JavaScript没有内置的通用映射类型(有时称为关联数组或字典),允许通过任意键访问任意值。JavaScript的基本数据结构是对象,这是一种特殊类型的映射,它只接受字符串作为键,并具有特殊的语义,如原型继承、getter和setter以及一些进一步的巫术。
当使用对象作为映射时,你必须记住键将通过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类中。
我的地图实现
下面的实现将额外地将键-值对存储在双链表中,以便对键和值进行快速迭代。要提供自己的哈希函数,可以在创建实例后重写实例的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建议重写toString()方法,大概是用我们的哈希函数。这是不可行的,因为它不适用于基本值(为基本值更改toString()是一个非常糟糕的主意)。如果希望toString()为任意对象返回有意义的值,则必须修改Object。原型,有些人(不包括我自己)认为这是禁止的。
我的地图实现的当前版本以及其他JavaScript好东西可以从这里获得。