本文深入探讨JavaScript中Hash Map的实现方式。首先指出直接使用Object存储键值对会因原型链继承导致潜在冲突,如toString属性干扰。接着介绍通过Object.create(null)创建“裸对象”来切断原型链,从而避免属性名冲突和类型转换错误,实现真正的哈希映射结构。
JavaScript Hash Map 的实现背景与问题
在JavaScript中,开发者通常使用简单的对象来存储键值对。然而,普通的Object并非真正的哈希映射,若使用不当可能引发潜在问题。JavaScript标准库中并未提供跨浏览器兼容的原生哈希映射结构,因此需要寻找更安全的对象属性声明方式。
Hash Map 基础实现与使用
以下是Hash Map的一种基础实现代码。
var hashMap = {
Set : function(key,value){this[key] = value},
Get : function(key){return this[key]},
Contains : function(key){return this.Get(key) == null?false:true},
Remove : function(key){delete this[key]}
}
以下是该Hash Map的具体使用示例。
hashMap.Set("name","John Smith");
hashMap.Set("age",24);
hashMap.Get("name");//John Smith
hashMap.Contains("title");//false
hashMap.Contains("name");//true
hashMap.Remove("age");
Object 原型链带来的属性冲突
利用普通Object声明成员时存在隐患。
这一问题通常源于对象原型链的继承机制。以toString方法为例,若使用in操作符检测对象是否包含某属性,可能会得到意外结果:
var map = {};
'toString' in map; // true
因为in操作符会遍历整个原型链以查找属性是否存在。解决此问题的方法是使用hasOwnProperty方法直接检测对象自身属性:
var map = {};
map.hasOwnProperty('toString'); // false
该方法在常规情况下运行正常,但如果用户自定义了名为hasOwnProperty的属性,则会引发麻烦:
var map = {};
map.hasOwnProperty = 'foo';
map.hasOwnProperty('hasOwnproperty'); // TypeError
快速修复此问题的方法是调用原生对象的方法。
var map = {};
map.hasOwnProperty = 'foo';
{}.hasOwnProperty.call(map, 'hasOwnproperty'); // true
这种方式不会引发任何问题,但在每次判断对象属性是否存在时,都需要过滤掉原型链中的方法:
var map = {};
var has = {}.hasOwnProperty;
for(var key in map){
if(has.call(map, key)){
// do something
}
}
利用 Object.create 构建裸对象
裸对象的概念。
创建真正Hash Map的关键在于解除所有原型对象的关联。我们可以利用Object.create来实现这一效果:
var obj = {};
// is equivalent to:
var obj = Object.create(Object.prototype);
此外,这种方法允许完全放弃原型链,直接使用null作为继承对象。
var map = Object.create(null);
map instanceof Object; // false
Object.prototype.isPrototypeOf(map); // false
Object.getPrototypeOf(map); // null
这些裸对象(或称为字典)是Hash Map的理想选择。由于不存在原型链冲突,它们能抵抗任何类型转换,例如尝试进行字符串拼接时会抛出错误。
var map = Object.create(null);
map + ""; // TypeError: Cannot convert object to primitive value
裸对象中没有任何保留字,它是专为Hash Map设计的,例如:
var map = Object.create(null);
'toString' in map; // false
进一步地,for...in循环的使用变得更加简洁,我们只需按以下方式编写循环代码。
var map = Object.create(null);
for(var key in map){
// do something
}
除了上述区别外,裸对象的使用方式与普通Object键值存储无异。该对象支持序列化,可以声明原型并被继承,上下文变量的使用方式也保持一致。
var map = Object.create(null);
Object.defineProperties(map, {
'foo': {
value: 1,
enumerable: true
},
'bar': {
value: 2,
enumerable: false
}
});
map.foo; // 1
map['bar']; // 2
JSON.stringify(map); // {"foo":1}
{}.hasOwnProperty.call(map, 'foo'); // true
{}.propertyIsEnumerable.call(map, 'bar'); // false
前文提到的属性检测方法同样适用于裸对象。
var map = Object.create(null);
typeof map; // object
{}.toString.call(map); // [object Object]
{}.valueOf.call(map); // Object {}