在Javascript中从字符串生成哈希

940

我需要将字符串转换为某种哈希形式。在JavaScript中是否有这种可能性?

由于我没有使用服务器端语言,所以无法通过那种方式进行操作。


9
MD5不再安全,因此不要寻找该算法。 - henrikstroem
276
取决于你正在哈希什么;对于非安全目的,使用md5生成哈希值是没有问题的。 - Brad Koch
14
根据你要做的事情而定;在安全方面使用md5没有问题。当然,有更好的密码哈希方法,但是在签署URL等任务中使用md5就足够了。 - Paul Ferrett
127
我觉得有趣的是,尽管在这里的评论中MD5受到了批评,但几乎所有答案都推荐了更糟糕的哈希算法,并获得了很多赞。 - domen
93
使用MD5验证下载文件的完整性,并不会神奇地将你的密码发送给所有同事。 - James M. Lay
显示剩余14条评论
28个回答

1045

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

const str = 'revenue'
console.log(str, str.hashCode())

来源


41
这与Java中使用的相同。hash << 5 - hash 相当于 hash * 31 + char,但速度要快得多。这很好,因为它非常快,而且31是一个小质数。双赢! - corsiKa
18
@PeterAronZentai 为什么它是“无法使用”的?基于数字的代码 (hash * 31) + char 生成的输出与基于位移的代码 ((hash<<5)-hash)+char 生成的输出完全相同,即使对于非常长的字符串(我已经测试过包含一百万个字符的字符串),因此从准确性上来说它并不是“无法使用的”。无论是数字版本还是位移版本,复杂度都是O(n),因此从复杂度上来说它也不是“无法使用的”。 - TachyonVortex
26
有人能否评论输出的唯一性(或缺乏唯一性)?具体而言,如果我仅对长度小于'n'的字符串使用此哈希函数,那么最大的'n'是多少,以使得我不可能发生哈希碰撞? - Don McCurdy
62
这个需要放在字符串原型上吗?如果只是像这样使用“var hashCode = function hashCode (str) {etc...}”,然后使用“hashCode("mystring")”,会不会减少效率或降低效能? - rattray
11
我在想,删除这行代码 if (this.length == 0) return hash; 是否会对性能产生重大影响。就代码的整洁程度而言,在我看来,它只是噪音。 - user40171
显示剩余42条评论

392

这里的许多答案都是来自Java的String.hashCode哈希函数。它最初来自于Gosling Emacs 1981年的版本,非常薄弱,在现代JavaScript中在性能上毫无意义。实际上,通过使用ES6 Math.imul,实现可以显著地更快,但没有人注意到。我们可以做得比这好得多,性能也基本相同。

我写了一个——cyrb53,它是一个简单但高质量的53位哈希函数。它非常快速,提供非常好*的哈希分布,并且因为它输出53位,与任何32位哈希相比,具有显着较低的冲突率。此外,您可以忽略SA的CC许可证,因为它在我的GitHub上是公共领域

const cyrb53 = (str, seed = 0) => {
    let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
    for(let i = 0, ch; i < str.length; i++) {
        ch = str.charCodeAt(i);
        h1 = Math.imul(h1 ^ ch, 2654435761);
        h2 = Math.imul(h2 ^ ch, 1597334677);
    }
    h1  = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
    h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
    h2  = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
    h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
  
    return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};

console.log(`cyrb53('a') -> ${cyrb53('a')}`)
console.log(`cyrb53('b') -> ${cyrb53('b')}`)
console.log(`cyrb53('revenge') -> ${cyrb53('revenge')}`)
console.log(`cyrb53('revenue') -> ${cyrb53('revenue')}`)
console.log(`cyrb53('revenue', 1) -> ${cyrb53('revenue', 1)}`)
console.log(`cyrb53('revenue', 2) -> ${cyrb53('revenue', 2)}`)
console.log(`cyrb53('revenue', 3) -> ${cyrb53('revenue', 3)}`)

*这个算法与著名的MurmurHash/xxHash算法大致相似。它使用乘法和Xorshift的组合来生成哈希值,但不像那么彻底。因此,它的实现要简单得多,但可能无法通过SMHasher中的所有测试。这不是加密哈希函数,因此不要将其用于安全目的。

像任何适当的哈希一样,它具有相当可接受的“雪崩”效应,这基本上意味着输入中的小变化会对输出产生很大的变化,使得生成的哈希看起来更“随机”:

"501c2ba782c97901" = cyrb53("a")
"459eda5bc254d2bf" = cyrb53("b")
"fbce64cc3b748385" = cyrb53("revenge")
"fb1d85148d13f93a" = cyrb53("revenue")

您可以选择提供一个种子(无符号整数,最大32位)用于相同输入的备用流:

"76fee5e6598ccd5c" = cyrb53("revenue", 1)
"1f672e2831253862" = cyrb53("revenue", 2)
"2b10de31708e6ab7" = cyrb53("revenue", 3)

从技术上讲,这是一个64位哈希,也就是说,两个不相关的32位哈希并行计算,但是JavaScript仅支持53位整数。如果方便的话,可以通过将return语句更改为十六进制字符串或数组来使用完整的64位输出。

return [h2>>>0, h1>>>0];
// or
return (h2>>>0).toString(16).padStart(8,0)+(h1>>>0).toString(16).padStart(8,0);
// or 
return 4294967296n * BigInt(h2) + BigInt(h1);

请注意,构建十六进制字符串会大大减慢批处理速度。数组更高效,但显然需要进行两个检查而不是一个。我还包括了BigInt,它应该比String稍微快一些,但仍比ArrayNumber慢得多。


仅为了好玩,这里是 TinySimpleHash,我能想到的最小的散列算法,但它仍然足够好。它是一个32位的散列算法,在只有89个字符的情况下,比FNVDJB2使用更好的随机性:

TSH=s=>{for(var i=0,h=9;i<s.length;)h=Math.imul(h^s.charCodeAt(i++),9**9);return h^h>>>9}

14
哇,这比通常用于短输入的*31要好得多。 :) - lapo
5
您可以使用 polyfill 或完整的 ES6 shim。但是IE11已经被冻结在2009年,没有更新了。 - bryc
4
你是如何选择那些并不一定是质数的大数字?它们对结果的效率和性能有什么影响? - Kossi D. T. S.
9
@KossiD.T.S. 我没有选择那些乘数;它们从别处借来的(L'Ecuyer,Knuth,MurmurHash)。通常这些数字是由聪明的人通过概率测试(例如模拟退火,遗传编程)寻找最佳统计结果而找到的,针对他们的使用情况进行调整。它们不会真正影响效率,只会影响哈希结果的质量。例如,使它们成为偶数通常会破坏一切,但数百万奇数组合可以给出不错的结果。我相信更好的常数可以被找到。我从未用SMHasher测试过这个。 - bryc
3
更新回答以澄清关于“种子”的问题。 它的目的是一个32位无符号整数。 因此,值可以是0到2 ^ 32-1,并且没有小数点。 所以没有浮点数; 在第一次XOR操作时,JS将仅删除小数部分。 “str”的长度没有限制。 另外,它可以很容易地与Array一起使用,而不是String,但是这个问题是关于字符串的。 - bryc
显示剩余36条评论

196

编辑

根据我的 jsperf 测试,被接受的答案实际上更快: http://jsperf.com/hashcodelordvlad

原始内容

如果有人感兴趣,这里有一个改进的(更快的)版本,但在缺乏 reduce 数组函数的旧浏览器上会失败。

hashCode = function(s) {
  return s.split("").reduce(function(a, b) {
    a = ((a << 5) - a) + b.charCodeAt(0);
    return a & a;
  }, 0);
}
 
 // testing
 console.log(hashCode("hello."));
 console.log(hashCode("this is a text."));
 console.log(hashCode("Despacito by Luis Fonsi"));

单行箭头函数版本:

hashCode = s => s.split('').reduce((a,b)=>{a=((a<<5)-a)+b.charCodeAt(0);return a&a},0)

 // testing
 console.log(hashCode("hello."));
 console.log(hashCode("this is a text."));
 console.log(hashCode("Despacito by Luis Fonsi"));


7
有没有办法获取仅为正数的哈希? - Prosto Trader
73
奇怪,我刚测试了一下,结果比被接受的答案慢得多。http://jsperf.com/hashcodelordvlad - lordvlad
196
不错的人 @lordvlad,实际上测试了自己的答案,并报告它运行较慢的情况。 - mikemaccana
14
我刚刚意识到接受的答案更快是有道理的,因为我的版本需要先将字符串转换为数组,分配新的内存并复制每个字符... - lordvlad
8
[].reduce.call(str, (p, c, i, a) => (p << 5) - p + a.charCodeAt(i), 0); - Dizzy
显示剩余18条评论

140

注意:即使使用最好的 32 位哈希,碰撞迟早会发生。
哈希碰撞概率可以计算为1 - e ^ (-k(k-1) / 2N, 可以近似表示为k^2 / 2N见此)。 这可能比直觉更高: 假设 32 位哈希和 k=10,000 个项,碰撞的概率为 1.2%。对于 77,163 个样本,概率变为 50%!(计算器)。 我建议在底部提供一个解决方案。
在回答这个问题哪种哈希算法最适合独特性和速度?时,Ian Boyd发表了一篇深入分析。简而言之(据我理解),他得出结论,MurmurHash是最好的选择,其次是FNV-1a
esmiralha提出的Java的String.hashCode()算法似乎是DJB2的变体。
  • FNV-1a比DJB2分布更均匀,但速度较慢
  • DJB2比FNV-1a更快,但容易产生碰撞
  • MurmurHash3比DJB2和FNV-1a更好且更快(但优化实现需要比FNV和DJB2更多的代码行)

以下是一些使用大输入字符串的基准测试: http://jsperf.com/32-bit-hash
当哈希短输入字符串时,相对于DJ2B和FNV-1a,murmur的性能会下降: http://jsperf.com/32-bit-hash/3

因此,一般我会推荐murmur3。
JavaScript实现请参见此处: https://github.com/garycourt/murmurhash-js

如果输入字符串很短且性能比分布质量更重要,则使用DJB2(由esmiralha的答案所建议)。

如果质量和小代码大小比速度更重要,我使用这个FNV-1a实现(基于this code)。

/**
 * Calculate a 32 bit FNV-1a hash
 * Found here: https://gist.github.com/vaiorabbit/5657561
 * Ref.: http://isthe.com/chongo/tech/comp/fnv/
 *
 * @param {string} str the input value
 * @param {boolean} [asString=false] set to true to return the hash value as 
 *     8-digit hex string instead of an integer
 * @param {integer} [seed] optionally pass the hash of the previous chunk
 * @returns {integer | string}
 */
function hashFnv32a(str, asString, seed) {
    /*jshint bitwise:false */
    var i, l,
        hval = (seed === undefined) ? 0x811c9dc5 : seed;

    for (i = 0, l = str.length; i < l; i++) {
        hval ^= str.charCodeAt(i);
        hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
    }
    if( asString ){
        // Convert to 8 digit hex string
        return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
    }
    return hval >>> 0;
}

提高碰撞概率

如此解释, 我们可以使用以下技巧扩展哈希位数:

function hash64(str) {
    var h1 = hash32(str);  // returns 32 bit (as 8 byte hex string)
    return h1 + hash32(h1 + str);  // 64 bit (as 16 byte hex string)
}

使用时请谨慎,不要期望过高。


4
这个功能会在哈希值前添加前导零,以便生成的哈希值始终为8个字符长度。这样在输出结果中更易于阅读和识别,但这是我的个人意见。 - mar10
太好了!我一直在苦苦寻找如何在JS中进行异或(^)运算,因为它返回负数。你用hval做的这件事情解决了我的问题。现在我有了一个算法,在C#和JS中返回相同的哈希值。我会在下面发布它们两个。谢谢! - djabraham
3
我很喜欢这个答案,因为它生成了一个更好的分布式哈希值:其他在此提出的函数将产生连续的哈希值。例如,hash("example1") - hash("example2") == 1,而这个函数则更加不可预测。 - GavinoGrifoni
建议在什么长度下使用 murmur3?我打算根据字符串长度决定要使用哪种哈希,以便于短字符串快速,长字符串安全,但是我不确定两种算法之间是否会有碰撞,这是一个好主意吗? - David
1
针对“FNV-1a比DJB2分布更好,但速度较慢”的回应 - 我认为应该说,如果使用ES6的Math.imul函数实现,FNV1a可以非常快。仅凭这一点就使它排名前列,并且从长远来看比DJB2更好的选择。 - bryc
显示剩余7条评论

100

基于ES6的已接受答案。更小、更易维护且适用于现代浏览器。

function hashCode(str) {
  return str.split('').reduce((prevHash, currVal) =>
    (((prevHash << 5) - prevHash) + currVal.charCodeAt(0))|0, 0);
}

// Test
console.log("hashCode(\"Hello!\"): ", hashCode('Hello!'));

编辑 (2019-11-04):

使用单行箭头函数版本:

const hashCode = s => s.split('').reduce((a,b) => (((a << 5) - a) + b.charCodeAt(0))|0, 0)

// test
console.log(hashCode('Hello!'))


1
谢谢分享。我在进行哈希之前添加了 str += "" 以避免传递非字符串参数时抛出 str.split is not a function 异常。 - BeetleJuice
8
但是它的速度要比这些网站慢得多,详见[https://jsperf.com/hashing-strings]。 - AndyO
4
有没有办法让这个产生的结果只是正面的,但仍然保持独特性? - Dids
2
@BeetleJuice 更恰当的问题是,如果您有一个设计为接收字符串的函数,那么为什么您的程序首先会发送一个非字符串呢?也许这个错误是调用者在做奇怪的事情的迹象。值得思考。 - Sukima
4
已接受的答案使用hash |= 0将其转换为32位整数。而这个实现没有这样做。这是一个错误吗? - Sukima
显示剩余9条评论

62

我有点惊讶,没有人谈论过新的SubtleCrypto API

要从字符串获取哈希值,您可以使用subtle.digest方法:

function getHash(str, algo = "SHA-256") {
  let strBuf = new TextEncoder().encode(str);
  return crypto.subtle.digest(algo, strBuf)
    .then(hash => {
      window.hash = hash;
      // here hash is an arrayBuffer, 
      // so we'll connvert it to its hex version
      let result = '';
      const view = new DataView(hash);
      for (let i = 0; i < hash.byteLength; i += 4) {
        result += ('00000000' + view.getUint32(i).toString(16)).slice(-8);
      }
      return result;
    });
}

getHash('hello world')
  .then(hash => {
    console.log(hash);
  });


5
我同意。将其转换为十六进制可以稍微有所不同... var promise = crypto.subtle.digest({name: "SHA-256"}, Uint8Array.from(data)); promise.then(function(result){ console.log(Array.prototype.map.call(new Uint8Array(result), x => x.toString(16).padStart(2, '0')).join('')); }); - Denis Giffeler
7
对于字符串的加密哈希函数来说,使用crypto有些过头了。crypto并不是特别高效的。 - bryc
1
可靠的质量随机数,无需依赖于人们运行测试,内置(无需自定义实现),可种子化,我只需要几百个数字来生成游戏地图,这似乎是完美的选择。但事实证明,没有任何同步方式可以实现它。每次调用种子随机引擎都必须提供一些异步回调函数,使代码变得超级难以阅读和荒谬。我不知道是谁想出了这个糟糕的crypto.subtle接口,所以最终我只能采用这个答案中的xmur3+sfc32:https://dev59.com/QHRB5IYBdhLWcg3wxZ1K#47593316 - Luc
我对这行代码 result += ('00000000' + view.getUint32(i).toString(16)).slice(-8); 感到困惑。为什么要添加 8 个零 00000000,然后在最后将它们切掉?.slice(-8) 是什么意思?当我删除这些部分时,在我的非常有限的测试中,我得到了完全相同的结果。我错过了什么吗? - I0_ol
1
@I0_ol 因为你总是得到一个高于 0xFF0000 的值。只需尝试使用 view = new DataView(new Uint32Array([0]).buffer)。你将得到没有填充的 "0" 和带有 "00000000" 填充的结果。 - Kaiido
显示剩余2条评论

44

这是更加精细和高效的变种,并且与Java标准object.hashCode()CharSequence方面有着相同的实现。

String.prototype.hashCode = function() {
    var hash = 0, i = 0, len = this.length;
    while ( i < len ) {
        hash  = ((hash << 5) - hash + this.charCodeAt(i++)) << 0;
    }
    return hash;
};

这里还有一个仅返回正数哈希码的函数:

String.prototype.hashcode = function() {
    return this.hashCode()+ 2147483647 + 1;
};

这里有一个与Java匹配的版本,仅返回正数哈希码:

public static long hashcode(Object obj) {
    return ((long) obj.hashCode()) + Integer.MAX_VALUE + 1l;
}

没有原型,适用于那些不想将其附加到String的人:

function hashCode(str) {
    var hash = 0, i = 0, len = str.length;
    while ( i < len ) {
        hash  = ((hash << 5) - hash + str.charCodeAt(i++)) << 0;
    }
    return hash;
}

function hashcode(str) {
    hashCode(str) + 2147483647 + 1;
}

尽情享受吧!


11
@koolaang 这是左移位运算符,具体信息请参考以下链接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Left_shift - mjs
57
@momomo 你是指左移操作(left shift)吗? - wdh
5
@momomo,我认为他是在问为什么要左移零位。 - jpfx1342
2
@jpfx1342 我后来意识到了这一点。没有建议的快速测试会产生不同的输出,所以是必需的。我相信它将括号中的内容转换为32位整数。32位整数转换是JS的一个怪癖。 - mjs
3
@Maykonn (2^32 - 1) 可以翻译为 @Maykonn(2的32次方减一)。 - Nijraj Gelani
显示剩余9条评论

30

如果有人需要的话,我将前两个答案结合起来,形成了一个适用于旧版浏览器的版本。如果reduce可用,则使用快速版本,并在不可用时退回到esmiralha的解决方案。

/**
 * @see https://dev59.com/XWsz5IYBdhLWcg3w2Lpu
 * @return {number}
 */
String.prototype.hashCode = function(){
    if (Array.prototype.reduce){
        return this.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);              
    } 
    var hash = 0;
    if (this.length === 0) return hash;
    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;
}

使用方法如下:

var hash = "some string to be hashed".hashCode();

如何优化此代码以在每个浏览器中运行更快。String.prototype.hashCode = function(){ var hash = 5381; if (this.length === 0) return hash; for (var i = 0; i < this.length; i++) { var character = this.charCodeAt(i); hash = ((hash<<5)+hash)^character; // 转换为32位整数 } return hash; } - Musakkhir Sayyed
这部分的目的是什么:return a & a?难道不应该只返回a吗? - James Stewart
我不确定你所说的"regardless"是什么意思,而且我也不是这方面的专家,但这是一个位运算符,可以进行一些数学运算。希望这能在某种程度上帮到你。 - Can Rau

22

UUID v3 和 UUID v5 实际上是针对给定输入字符串的哈希值。

  • UUID v3 基于 MD5,
  • UUID v5 基于 SHA-1。

因此,最明显的选择是选择 UUID v5。

幸运的是,有一个流行的 npm 包,其中包含所有 UUID 算法。

npm install uuid

要生成UUID v5,您需要一个唯一的命名空间。此命名空间类似于种子,并且应该是一个常量,以确保对于给定的输入,输出始终相同。具有讽刺意味的是,您应该生成一个UUID v4作为命名空间。而最简单的方法是使用一些在线工具

一旦您获得了命名空间,您就可以开始了。

import { v5 as uuidv5 } from 'uuid';

const MY_NAMESPACE = '1b671a64-40d5-491e-99b0-da01ff1f3341';
const hash = uuidv5('input', MY_NAMESPACE);

如果您的输入字符串始终是URL,那么有一些默认的命名空间可以使用。

const hashForURL = uuidv5('https://www.w3.org/', uuidv5.URL);

1
更喜欢这个答案,因为它调用了一个库。不知道有没有办法让生成的ID更短,比如10个字符?我查了一下NanoID,但似乎缺少接受额外参数的选项。 - Yan King Yin
2
@YanKingYin,它如此之长的部分原因是为了保证唯一性。这是这个答案的优点。;;; 然而,唯一性并不总是哈希码的要求。如果您不需要唯一性,则有很多其他算法可以给您更短的哈希值。 _(例如,索引算法有时使用不唯一的哈希值。然后查询可能会导致误报命中,这些命中将在严格比较后进行过滤。) - bvdb

14

虽然我来晚了,但你可以使用这个模块:crypto

const crypto = require('crypto');

const SALT = '$ome$alt';

function generateHash(pass) {
  return crypto.createHmac('sha256', SALT)
    .update(pass)
    .digest('hex');
}
此函数的结果始终是一个长度为64的字符串,类似于这样:"aa54e7563b1964037849528e7ba068eb7767b1fab74a8d80fe300828b996714a"

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