JavaScript中是否有某种哈希码函数?


150

基本上,我试图创建一个具有唯一对象的对象,即一个集合。我有一个绝妙的主意,就是仅将JavaScript对象与属性名称的对象一起使用。如,

set[obj] = true;

在一定程度上这是可行的。它适用于字符串和数字,但是对于其他对象,它们似乎都“哈希”为相同的值并访问相同的属性。有什么方法可以为对象生成唯一的哈希值?字符串和数字是如何做到的,我可以覆盖相同的行为吗?


32
对象全部“哈希”为相同值的原因是因为您尚未覆盖其toString方法。由于键必须是字符串,因此将自动调用toString方法以获得有效的键,因此所有对象都将转换为相同的默认字符串:“ [object Object]”。
2012年

4
JSON.stringify(obj)obj.toSource()可能会根据问题和目标平台为您服务。
AnnanFay 2013年

4
@Annan JSON.stringify(obj)实际上只是将(整个)对象转换为字符串。因此,您基本上只是将对象复制到自身上。这是没有意义的,浪费空间而不是最优的。
Metalstorm

1
@Metalstorm True,这就是为什么它取决于您的问题所在。当我通过Google找到这个问题时,我的最终解决方案是在对象上调用toSource()。另一种方法就是在源上使用常规哈希。
AnnanFay 2014年

@Annan,toSource请不要在Chrome中使用btw
Pacerier

Answers:


35

JavaScript对象只能将字符串用作键(其他任何东西都将转换为字符串)。

或者,您可以维护一个数组,该数组为有问题的对象建立索引,并将其索引字符串用作对该对象的引用。像这样:

var ObjectReference = [];
ObjectReference.push(obj);

set['ObjectReference.' + ObjectReference.indexOf(obj)] = true;

显然,这有点冗长,但是您可以编写一些方法来处理它并获取并设置所有有害的内容。

编辑:

您的猜测是事实-这是JavaScript中定义的行为-特别是发生了toString转换,这意味着您可以在将用作属性名称的对象上定义自己的toString函数。-奥利耶

这带来了另一个有趣的观点;您可以在要哈希的对象上定义toString方法,该方法可以构成它们的哈希标识符。


另一种选择是给每个对象一个随机值,因为它是哈希值-可能是一个随机数+总滴答声-然后具有一组函数来从数组中添加/删除对象。
Sugendran

4
如果您两次添加相同的对象,则将失败。它会认为这是不同的。
Daniel X Moore

“如果两次添加相同的对象,将失败。它将认为它是不同的。” 好点子。一种解决方案是将Array归类为ObjectReference,将重复的检查挂钩到push()中。我现在没有时间编辑此解决方案,但希望以后能记得。
眼睑失明

8
我喜欢此解决方案,因为它在对象中不需要任何其他属性。但是,如果您尝试拥有一个干净的垃圾收集器,它将变得成问题。在您的方法中,尽管其他引用已被删除,但是它将保存对象。这可能会导致更大的应用程序出现问题。
约翰尼,

35
如果每次引用对象时都需要对数组进行线性扫描,那么散列对象的意义何在?
Bordaigorl 2014年

57

如果您想要像JavaScript中的Java一样的hashCode()函数,那就是您的:

String.prototype.hashCode = function(){
    var hash = 0;
    for (var i = 0; i < this.length; i++) {
        var character = this.charCodeAt(i);
        hash = ((hash<<5)-hash)+character;
        hash = hash & hash; // Convert to 32bit integer
    }
    return hash;
}

这就是Java(按位运算符)的实现方式。

请注意,hashCode可以为正也可以为负,这很正常,请参阅HashCode给出负值。因此,您可以考虑Math.abs()与此功能一起使用。


5
这会创建-hash,而不是完美
qodeninja 2012年

2
@KimKha char是JS中的保留字,可能会引起一些问题。使用其他名称会更好。
szeryf

16
@qodeninja说谁?这是我第一次听到这样的说法。您可以链接到某些来源吗?哈希通常使用固定大小的整数算术和位运算来计算,因此可以预期得到正数或负数的结果。
szeryf

7
挑剔,但是...“如果(this.length == 0)返回哈希值;” 是多余的:),并且会亲自将“字符”更改为“代码”。
Metalstorm

10
@qodeninja和@szeryf:您只需要小心使用它。例如,我尝试对具有20个元素pickOne["helloo".hashCode() % 20]的数组pickOne进行处理。我得到的undefined原因是哈希码为负,所以这是一个示例,其中某人(我)隐式地假定为正哈希码。
Jim Pivarski

31

最简单的方法是为每个对象提供自己的独特toString方法:

(function() {
    var id = 0;

    /*global MyObject */
    MyObject = function() {
        this.objectId = '<#MyObject:' + (id++) + '>';
        this.toString= function() {
            return this.objectId;
        };
    };
})();

我遇到了同样的问题,这对我来说是最小的麻烦,完全可以解决,而且重新实现某些胖Java样式Hashtable并向对象类添加equals()和添加hashCode()要容易得多。只要确保您也不要将字符串'<#MyObject:12>粘贴到您的哈希中,否则它将擦除具有该ID的现有对象的条目。

现在我所有的哈希值都变得很冷。几天前,我也刚刚发布了一个博客文章,内容涉及这个确切的主题


28
但这没有抓住重点。Java具有equals()和,hashCode()因此两个等效的对象具有相同的哈希值。使用上述方法意味着的每个实例都MyObject将具有唯一的字符串,这意味着您必须保留对该对象的引用,才能从映射中检索正确的值。拥有键是没有意义的,因为它与对象的唯一性无关。对于toString()用作键的特定对象类型,将需要实现有用的功能。
sethro 2014年

@sethro您可以toString为这些对象实现,以便它直接映射到一个等价关系,以便两个对象在被视为“相等”时创建相同的字符串。
Daniel X Moore

3
没错,那就是只有用正确的方法toString(),让你使用一个Object作为Set。我想我误解了您的答案,试图提供一种通用的解决方案,以避免编写toString()等效的案例equals()hashCode()个案案例。
sethro 2014年

3
陶醉。这不是哈希码,请参阅我的答案:stackoverflow.com/a/14953738/524126哈希码的真正实现:stackoverflow.com/a/15868654/524126
Metalstorm

5
@Metalstorm的问题不是询问“真实的”哈希码,而是询问如何在JavaScript中成功地将对象用作集合。
Daniel X Moore

20

您描述的内容已包含在ECMAScript 6规范(JavaScript的下一个版本)的一部分Harmony WeakMaps中。也就是说:一组键,键可以是任何值(包括未定义),并且是不可枚举的。

这意味着除非直接引用链接到该键的键(任何对象!),否则就不可能引用该值。对于许多与效率和垃圾收集有关的引擎实现原因而言,这一点很重要,但它也非常酷,因为它允许新的语义(例如可撤销的访问权限和传递数据)而不会暴露数据发送者。

MDN

var wm1 = new WeakMap(),
    wm2 = new WeakMap();
var o1 = {},
    o2 = function(){},
    o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // A value can be anything, including an object or a function.
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // Keys and values can be any objects. Even WeakMaps!

wm1.get(o2); // "azerty"
wm2.get(o2); // Undefined, because there is no value for o2 on wm2.
wm2.get(o3); // Undefined, because that is the set value.

wm1.has(o2); // True
wm2.has(o2); // False
wm2.has(o3); // True (even if the value itself is 'undefined').

wm1.has(o1);   // True
wm1.delete(o1);
wm1.has(o1);   // False

WeakMaps在当前的Firefox,Chrome和Edge中可用。Node v7和带有该--harmony-weak-maps标志的v6也支持它们。


1
和之间有什么区别Map
smac89'7

@ smac89 WeakMap有局限性:1)仅将对象作为键2)没有大小属性​​3)没有迭代器或forEach方法4)没有清除方法。关键是对象-因此当从内存中删除对象时-与该对象连接的WeakMap中的数据也将被删除。当我们要保留信息(仅当对象存在时才应存在)时,这非常有用。因此,WeakMap仅具有以下方法:设置,删除以进行写入和获取,具有用于读取
Ekaterina Tokareva

这不能完全正确地工作... var m = new Map();m.set({},"abc"); console.log(m.get({}) //=>undefined仅当您具有与set命令中最初引用的变量相同的变量时,它才有效。EGvar m = new Map();a={};m.set(a,"abc"); console.log(m.get(a) //=>undefined
Sancarn '18

1
@Sancarn不必是相同的变量,但是它们必须指向相同的对象。在第一个示例中,您有两个不同的对象,它们看起来相同,但是它们具有不同的地址。
Svish

1
@Svish好地方!虽然我现在知道了,但那时我可能还没有做过:)
Sancarn

19

我选择的解决方案与Daniel的解决方案相似,但是当第一次通过getHashCode函数请求哈希时,我将哈希显式添加到了该对象,而不是使用对象工厂并覆盖toString。有点混乱,但更适合我的需求:)

Function.prototype.getHashCode = (function(id) {
    return function() {
        if (!this.hashCode) {
            this.hashCode = '<hash|#' + (id++) + '>';
        }
        return this.hashCode;
    }
}(0));

7
如果要这样,最好将hashCode via Object.definePropertywith enumerable设置为set false,这样就不会导致任何for .. in循环崩溃。
塞巴斯蒂安·诺瓦克

14

对于我的特定情况,我只关心键和基元值的对象相等性。对我有用的解决方案是将对象转换为其JSON表示形式,并将其用作哈希。存在局限性,例如键定义的顺序可能不一致;但是就像我说的那样对我有用,因为这些对象都是在一处生成的。

var hashtable = {};

var myObject = {a:0,b:1,c:2};

var hash = JSON.stringify(myObject);
// '{"a":0,"b":1,"c":2}'

hashtable[hash] = myObject;
// {
//   '{"a":0,"b":1,"c":2}': myObject
// }

10

不久前,我组合了一个小的JavaScript模块,以生成字符串,对象,数组等的哈希码。(我刚刚将其提交给GitHub :))

用法:

Hashcode.value("stackoverflow")
// -2559914341
Hashcode.value({ 'site' : "stackoverflow" })
// -3579752159

javascript的GC也不会阻塞循环引用吗?
克莱顿·拉本达

@Ryan Long我可能会说,如果您有循环引用,则需要重构代码;)
Metalstorm 2014年

11
@Metalstorm“那么您需要重构代码”您在开玩笑吗?每个DOM元素的父对和子对构成一个循环引用。
克里斯·米德尔顿

8
它对具有数值属性的对象进行散列处理的效果很差,在许多情况下返回相同的值,var hash1 = Hashcode.value({ a: 1, b: 2 }); var hash2 = Hashcode.value({ a: 2, b: 1 }); console.log(hash1, hash2);即将记录2867874173 2867874173
JulienBérubé15年

9

JavaScript规范将索引属性访问定义为对索引名称执行toString转换。例如,

myObject[myProperty] = ...;

是相同的

myObject[myProperty.toString()] = ...;

就像在JavaScript中一样

myObject["someProperty"]

是相同的

myObject.someProperty

是的,这也让我难过:-(



5

参考:https : //developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol

您可以使用Es6符号来创建唯一密钥和访问对象。从Symbol()返回的每个符号值都是唯一的。符号值可以用作对象属性的标识符。这是数据类型的唯一目的。

var obj = {};

obj[Symbol('a')] = 'a';
obj[Symbol.for('b')] = 'b';
obj['c'] = 'c';
obj.d = 'd';

2
除了没有真正的方法可以重新生成符号外,让x = Symbol('a'); 让y = Symbol('a'); console.log(x === y); //返回false,因此Symbol不能像哈希一样工作。
理查德·科莱特

3

这是我返回唯一整数的简单解决方案。

function hashcode(obj) {
    var hc = 0;
    var chars = JSON.stringify(obj).replace(/\{|\"|\}|\:|,/g, '');
    var len = chars.length;
    for (var i = 0; i < len; i++) {
        // Bump 7 to larger prime number to increase uniqueness
        hc += (chars.charCodeAt(i) * 7);
    }
    return hc;
}

2
这种复杂性破坏了hashCode()背后的整个思想
tuxSlayer

我认为这并不必要地复杂。不过,我很好奇:为什么要进行替换阶段?否则,这些排除将在charCodeAt上返回正常,不是吗?
格雷格·佩蒂特

由于hashcode({a:1, b:2}) === hashcode({a:2, b:1})和许多其他冲突而致。
maaartinus

3

基于标题,我们可以使用js生成强哈希,它可以用于根据对象,参数数组,字符串或其他任何东西生成唯一的哈希。

以后进行索引时,这可以避免任何可能的匹配错误,同时允许从参数中检索索引(避免搜索/循环对象等):

async function H(m) {
  const msgUint8 = new TextEncoder().encode(m)                       
  const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)          
  const hashArray = Array.from(new Uint8Array(hashBuffer))                    
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
  console.log(hashHex)
}

/* Examples ----------------------- */
H("An obscure ....")
H(JSON.stringify( {"hello" : "world"} ))
H(JSON.stringify( [54,51,54,47] ))

在我的浏览器中,上面的输出对您来说也应该是相同的(是吗?):

bf1cf3fe6975fe382ab392ec1dd42009380614be03d489f23601c11413cfca2b
93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588
d2f209e194045604a3b15bdfd7502898a0e848e4603c5a818bd01da69c00ad19

https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto/digest#Converting_a_digest_to_a_hex_string


1

我的解决方案为全局Object对象引入了静态函数。

(function() {
    var lastStorageId = 0;

    this.Object.hash = function(object) {
        var hash = object.__id;

        if (!hash)
             hash = object.__id = lastStorageId++;

        return '#' + hash;
    };
}());

我认为这与JavaScript中的其他对象操纵函数比较方便。


1
具有相同内部值的对象将散列到不同的散列,这不是散列(代码)所做的。
Metalstorm

在JavaScript中(而且我也认为几乎所有其他语言也是如此),使用相同内部值创建的两个对象仍然是不同的对象,因为底层数据类型每个都由一个新的对象实例表示。jsfiddle.net/h4G9f
Johnny

4
是的,但这不是哈希码的用途,哈希码用于检查对象状态的相等性。就像哈希一样,相同的输入(可变值)也出现相同的哈希。您正在寻找的是一个UUID(这是您的函数提供的)。
Metalstorm

1
你是对的。我误解了这个问题。确实很糟糕,接受的答案也不能提供一个好的解决方案。
约翰尼

兼顾您的功能,我会更倾向于这样:jsfiddle.net/xVSsd同样的结果,更短(LoC +字符),可能稍微快一点:)
Metalstorm

1

我将尝试比其他答案更深入。

即使JS具有更好的哈希支持,它也无法完美地对所有内容进行神奇的哈希处理,在许多情况下,您将必须定义自己的哈希函数。例如,Java具有良好的哈希支持,但是您仍然必须考虑并做一些工作。

一个问题是术语“哈希/哈希码”……存在加密哈希和非加密哈希。另一个问题是,您必须了解哈希为何有用以及它如何工作。

大多数时候,当我们谈论JavaScript或Java中的哈希时,我们谈论的是非加密哈希,通常是关于hashmap / hashtable的哈希(除非我们正在处理身份验证或密码,您可以使用NodeJS在服务器端进行此操作)。 ..)。

这取决于您拥有什么数据以及想要获得什么。

您的数据具有一些自然的“简单”唯一性:

  • 整数的哈希值是……整数,因为它是唯一的,幸运的是您!
  • 字符串的哈希值...取决于字符串,如果字符串表示唯一标识符,则可以将其视为哈希值(因此不需要哈希值)。
  • 间接地是唯一整数的任何事物都是最简单的情况
  • 这将遵守:如果对象相等,则哈希码相等

您的数据具有一些自然的“复合”唯一性:

  • 例如,对于人员对象,您可以使用名字,姓氏,生日等来计算哈希值……请参见Java的工作原理:字符串的良好哈希函数,或使用其他一些ID信息,这些信息对于您的用例而言足够便宜且独特

您不知道您的数据是什么:

  • 祝您好运……您可以序列化为字符串并以Java样式对其进行哈希处理,但是如果字符串很大并且无法避免冲突(比如说整数(自身)的哈希),则可能会很昂贵。

对于未知数据,没有魔术般有效的哈希技术,在某些情况下,这很容易,在其他情况下,您可能需要三思。因此,即使JavaScript / ECMAScript添加了更多支持,也没有针对该问题的魔术语言解决方案。

实际上,您需要两件事:足够的独特性,足够的速度

除此之外,它还很棒:“如果对象相等,则哈希码相等”


0

如果您确实想要设置行为(我正在学习Java知识),那么将很难在JavaScript中找到解决方案。大多数开发人员会推荐一个唯一的键来表示每个对象,但这与set不同,因为您可以获得两个相同的对象,每个对象都有一个唯一的键。Java API通过比较哈希码值而非键来完成检查重复值的工作,并且由于JavaScript中没有对象的哈希码值表示形式,因此几乎不可能做到这一点。即使是Prototype JS库,它也指出了这一缺点:

“哈希可以看作是一个关联数组,将唯一键绑定到值(不一定是唯一的)……”

http://www.prototypejs.org/api/hash


0

除了解决失明问题外,以下函数还可以为任何对象返回可重现的唯一ID:

var uniqueIdList = [];
function getConstantUniqueIdFor(element) {
    // HACK, using a list results in O(n), but how do we hash e.g. a DOM node?
    if (uniqueIdList.indexOf(element) < 0) {
        uniqueIdList.push(element);
    }
    return uniqueIdList.indexOf(element);
}

如您所见,它使用查找列表非常低效,但这是我目前能找到的最好列表。


0

如果要将对象用作键,则需要覆盖其toString方法,如此处已提到的那样。使用的哈希函数都很好,但是它们仅适用于相同的对象,不适用于相等的对象。

我编写了一个小型库,该库从对象创建哈希,您可以轻松地将其用于此目的。对象甚至可以具有不同的顺序,散列将相同。在内部,您可以对哈希使用不同的类型(djb2,md5,sha1,sha256,sha512,tripmd160)。

这是文档中的一个小示例:

var hash = require('es-hash');

// Save data in an object with an object as a key
Object.prototype.toString = function () {
    return '[object Object #'+hash(this)+']';
}

var foo = {};

foo[{bar: 'foo'}] = 'foo';

/*
 * Output:
 *  foo
 *  undefined
 */
console.log(foo[{bar: 'foo'}]);
console.log(foo[{}]);

该软件包可以在浏览器和Node-J中使用。

仓库:https : //bitbucket.org/tehrengruber/es-js-hash


0

如果要在查找对象中具有唯一值,可以执行以下操作:

创建一个查找对象

var lookup = {};

设置哈希码功能

function getHashCode(obj) {
    var hashCode = '';
    if (typeof obj !== 'object')
        return hashCode + obj;
    for (var prop in obj) // No hasOwnProperty needed
        hashCode += prop + getHashCode(obj[prop]); // Add key + value to the result string
    return hashCode;
}

目的

var key = getHashCode({ 1: 3, 3: 7 });
// key = '1337'
lookup[key] = true;

数组

var key = getHashCode([1, 3, 3, 7]);
// key = '01132337'
lookup[key] = true;

其他种类

var key = getHashCode('StackOverflow');
// key = 'StackOverflow'
lookup[key] = true;

最后结果

{ 1337: true, 01132337: true, StackOverflow: true }

请注意,getHashCode如果对象或数组为空,则不会返回任何值

getHashCode([{},{},{}]);
// '012'
getHashCode([[],[],[]]);
// '012'

这类似于@ijmacd解决方案,只是getHashCode没有JSON依赖性。


您必须
对此

@tuxSlayer感谢您告诉我。您可以根据需要轻松扩展此代码,但我希望这个想法很清楚:)
A1rPun

这将为大型对象产生很长的键,从而可能会严重影响内存和性能
Gershom

0

我结合了双眼失明和KimKha的答案。

以下是angularjs服务,它支持数字,字符串和对象。

exports.Hash = () => {
  let hashFunc;
  function stringHash(string, noType) {
    let hashString = string;
    if (!noType) {
      hashString = `string${string}`;
    }
    var hash = 0;
    for (var i = 0; i < hashString.length; i++) {
        var character = hashString.charCodeAt(i);
        hash = ((hash<<5)-hash)+character;
        hash = hash & hash; // Convert to 32bit integer
    }
    return hash;
  }

  function objectHash(obj, exclude) {
    if (exclude.indexOf(obj) > -1) {
      return undefined;
    }
    let hash = '';
    const keys = Object.keys(obj).sort();
    for (let index = 0; index < keys.length; index += 1) {
      const key = keys[index];
      const keyHash = hashFunc(key);
      const attrHash = hashFunc(obj[key], exclude);
      exclude.push(obj[key]);
      hash += stringHash(`object${keyHash}${attrHash}`, true);
    }
    return stringHash(hash, true);
  }

  function Hash(unkType, exclude) {
    let ex = exclude;
    if (ex === undefined) {
      ex = [];
    }
    if (!isNaN(unkType) && typeof unkType !== 'string') {
      return unkType;
    }
    switch (typeof unkType) {
      case 'object':
        return objectHash(unkType, ex);
      default:
        return stringHash(String(unkType));
    }
  }

  hashFunc = Hash;

  return Hash;
};

用法示例:

Hash('hello world'), Hash('hello world') == Hash('hello world')
Hash({hello: 'hello world'}), Hash({hello: 'hello world'}) == Hash({hello: 'hello world'})
Hash({hello: 'hello world', goodbye: 'adios amigos'}), Hash({hello: 'hello world', goodbye: 'adios amigos'}) == Hash({goodbye: 'adios amigos', hello: 'hello world'})
Hash(['hello world']), Hash(['hello world']) == Hash(['hello world'])
Hash(1), Hash(1) == Hash(1)
Hash('1'), Hash('1') == Hash('1')

输出量

432700947 true
-411117486 true
1725787021 true
-1585332251 true
1 true
-1881759168 true

说明

如您所见,该服务的核心是KimKha创建的哈希函数。我在字符串中添加了类型,以便对象的构造也将影响最终的哈希值。对键进行哈希处理以防止对象冲突。

眼睑失明的对象比较用于通过自引用对象来防止无限递归。

用法

我创建了此服务,以便可以使用对象访问错误服务。这样一个服务可以向给定对象注册错误,而另一服务可以确定是否发现了任何错误。

JsonValidation.js

ErrorSvc({id: 1, json: '{attr: "not-valid"}'}, 'Invalid Json Syntax - key not double quoted');

UserOfData.js

ErrorSvc({id: 1, json: '{attr: "not-valid"}'});

这将返回:

['Invalid Json Syntax - key not double quoted']

ErrorSvc({id: 1, json: '{"attr": "not-valid"}'});

这将返回

[]

0

只需将隐藏的秘密属性与 defineProperty enumerable: false

它工作非常快

  • 首次读取的uniqueId1,257,500 ops / s
  • 所有其他:309,226,485 ops / s
var nextObjectId = 1
function getNextObjectId() {
    return nextObjectId++
}

var UNIQUE_ID_PROPERTY_NAME = '458d576952bc489ab45e98ac7f296fd9'
function getObjectUniqueId(object) {
    if (object == null) {
        return null
    }

    var id = object[UNIQUE_ID_PROPERTY_NAME]

    if (id != null) {
        return id
    }

    if (Object.isFrozen(object)) {
        return null
    }

    var uniqueId = getNextObjectId()
    Object.defineProperty(object, UNIQUE_ID_PROPERTY_NAME, {
        enumerable: false,
        configurable: false,
        writable: false,
        value: uniqueId,
    })

    return uniqueId
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.