问题描述
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建议覆盖该方法,大概是用我们的哈希函数。这是不可行的,因为它不适用于基元值(更改基元是一个非常糟糕的主意)。如果我们想为任意对象返回有意义的值,我们将不得不修改 ,有些人(不包括我自己)认为这是verboten。toString()
toString()
toString()
Object.prototype
我的实现的当前版本以及其他JavaScript好东西都可以从这里获得。Map