我正在尝试在 JavaScript 中创建全局唯一标识符。我不确定所有浏览器上都有哪些例程,内置随机数生成器的“随机”和种子如何等等。
GUID / UUID 应至少为 32 个字符,并且应保持在 ASCII 范围内以避免在传递它们时出现问题。
我正在尝试在 JavaScript 中创建全局唯一标识符。我不确定所有浏览器上都有哪些例程,内置随机数生成器的“随机”和种子如何等等。
GUID / UUID 应至少为 32 个字符,并且应保持在 ASCII 范围内以避免在传递它们时出现问题。
[编辑 2021-10-16 以反映生成 RFC4122-complaint UUID 的最新最佳实践]
这里的大多数读者都希望使用该uuid
module。它经过充分测试和支持。
该crypto.randomUUID()
功能是在支持新兴的标准Node.js
和越来越多的浏览器。
如果这些都不适合你,有这个方法(基于这个问题的原始答案):
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
console.log(uuidv4());
注意:强烈建议不要使用依赖于 Math.random()的任何UUID 生成器(包括本答案以前版本中的片段),原因在此处 有最佳解释。TL;DR:基于 Math.random() 的解决方案不提供良好的唯一性保证。
UUID(通用唯一标识符),也称为 GUID(全局唯一标识符),根据RFC 4122,是旨在提供某些唯一性保证的标识符。
虽然可以在几行 JavaScript 代码中实现符合 RFC 的 UUID(例如,请参阅下面的@broofa 的回答),但有几个常见的陷阱:
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
”形式,其中 x 是 [0-9, af] 之一M是 [1-5] 之一,N是 [8, 9, a 或 b]Math.random
)因此,鼓励为生产环境编写代码的开发人员使用严格的、维护良好的实现,例如uuidmodule。
我真的很喜欢Broofa 的答案是多么干净,但不幸的是,糟糕的实现Math.random
留下了碰撞的机会。
这是一个类似的符合RFC4122版本 4 的解决方案,它通过将前 13 个十六进制数字偏移时间戳的十六进制部分来解决该问题,并且一旦从页面加载开始以微秒的十六进制部分耗尽偏移量。这样,即使Math.random
在同一个种子上,两个客户端也必须在页面加载后(如果支持高性能时间)和完全相同的毫秒(或 10,000 多年后)生成完全相同的微秒数以获得相同的 UUID:
function generateUUID() { // Public Domain/MIT
var d = new Date().getTime();//Timestamp
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16;//random number between 0 and 16
if(d > 0){//Use timestamp until depleted
r = (d + r)%16 | 0;
d = Math.floor(d/16);
} else {//Use microseconds since page-load if supported
r = (d2 + r)%16 | 0;
d2 = Math.floor(d2/16);
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
var onClick = function(){
document.getElementById('uuid').textContent = generateUUID();
}
onClick();
#uuid { font-family: monospace; font-size: 1.5em; }
<p id="uuid"></p>
<button id="generateUUID" onclick="onClick();">Generate UUID</button>
broofa 的答案非常巧妙,确实 - 令人印象深刻的聪明,真的......符合 RFC4122,有点可读且紧凑。惊人的!
但是,如果您正在查看该正则表达式、那么多replace()
回调、toString()
' 和Math.random()
函数调用(他只使用结果的四位并浪费了其余部分),您可能会开始怀疑性能。事实上,joelpt 甚至决定用generateQuickGUID
.
但是,我们能否获得速度和RFC 合规性?我说是!我们能保持可读性吗?嗯......不是真的,但如果你跟着做,这很容易。
但首先,与 broofa guid
(已接受的答案)和不符合 rfc 的结果相比,我的结果是generateQuickGuid
:
Desktop Android
broofa: 1617ms 12869ms
e1: 636ms 5778ms
e2: 606ms 4754ms
e3: 364ms 3003ms
e4: 329ms 2015ms
e5: 147ms 1156ms
e6: 146ms 1035ms
e7: 105ms 726ms
guid: 962ms 10762ms
generateQuickGuid: 292ms 2961ms
- Note: 500k iterations, results will vary by browser/CPU.
因此,通过我的第 6 次优化迭代,我击败了最受欢迎的答案12 倍以上,超过了9 倍的接受答案,以及2-3 倍的快速不合规答案。而且我仍然符合 RFC 4122。
有兴趣怎么办?我已经把完整的源代码放在http://jsfiddle.net/jcward/7hyaC/3/和http://jsperf.com/uuid-generator-opt/4
为了说明,让我们从broofa的代码开始:
function broofa() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
console.log(broofa())
所以它替换x
为任何随机的十六进制数字,y
随机数据(除了10
根据 RFC 规范强制前两位),并且正则表达式与-
或4
字符不匹配,因此他不必处理它们。非常非常光滑。
首先要知道的是,函数调用是昂贵的,正则表达式也是如此(尽管他只使用了 1 个,但它有 32 个回调,每个匹配一个,并且在 32 个回调中的每一个中它调用 Math.random() 和 v。 toString(16))。
提高性能的第一步是消除 RegEx 及其回调函数,并改用简单的循环。这意味着我们必须处理-
和4
字符,而 broofa 则没有。另外,请注意,我们可以使用字符串数组索引来保持他流畅的字符串模板架构:
function e1() {
var u='',i=0;
while(i++<36) {
var c='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'[i-1],r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);
u+=(c=='-'||c=='4')?c:v.toString(16)
}
return u;
}
console.log(e1())
基本上,相同的内部逻辑,除了我们检查-
or4
和使用 while 循环(而不是replace()
回调)使我们获得了近 3 倍的改进!
下一步是桌面上的一个小步骤,但在移动设备上有很大的不同。让我们减少 Math.random() 调用并利用所有这些随机位,而不是将其中的 87% 丢弃在每次迭代中移出的随机缓冲区中。让我们也将模板定义移出循环,以防万一它有帮助:
function e2() {
var u='',m='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx',i=0,rb=Math.random()*0xffffffff|0;
while(i++<36) {
var c=m[i-1],r=rb&0xf,v=c=='x'?r:(r&0x3|0x8);
u+=(c=='-'||c=='4')?c:v.toString(16);rb=i%8==0?Math.random()*0xffffffff|0:rb>>4
}
return u
}
console.log(e2())
根据平台的不同,这可以为我们节省 10-30%。不错。但是下一个重要步骤通过优化经典 - 查找表完全摆脱 toString 函数调用。一个简单的 16 元素查找表将在更短的时间内执行 toString(16) 的工作:
function e3() {
var h='0123456789abcdef';
var k='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
/* same as e4() below */
}
function e4() {
var h=['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'];
var k=['x','x','x','x','x','x','x','x','-','x','x','x','x','-','4','x','x','x','-','y','x','x','x','-','x','x','x','x','x','x','x','x','x','x','x','x'];
var u='',i=0,rb=Math.random()*0xffffffff|0;
while(i++<36) {
var c=k[i-1],r=rb&0xf,v=c=='x'?r:(r&0x3|0x8);
u+=(c=='-'||c=='4')?c:h[v];rb=i%8==0?Math.random()*0xffffffff|0:rb>>4
}
return u
}
console.log(e4())
接下来的优化是另一个经典。由于我们在每次循环迭代中只处理四位输出,让我们将循环次数减半并在每次迭代中处理八位。这很棘手,因为我们仍然必须处理符合 RFC 的位位置,但这并不难。然后我们必须制作一个更大的查找表(16x16 或 256)来存储 0x00 - 0xFF,并且我们只构建一次,在 e5() 函数之外。
var lut = []; for (var i=0; i<256; i++) { lut[i] = (i<16?'0':'')+(i).toString(16); }
function e5() {
var k=['x','x','x','x','-','x','x','-','4','x','-','y','x','-','x','x','x','x','x','x'];
var u='',i=0,rb=Math.random()*0xffffffff|0;
while(i++<20) {
var c=k[i-1],r=rb&0xff,v=c=='x'?r:(c=='y'?(r&0x3f|0x80):(r&0xf|0x40));
u+=(c=='-')?c:lut[v];rb=i%4==0?Math.random()*0xffffffff|0:rb>>8
}
return u
}
console.log(e5())
我尝试了一次处理 16 位的 e6(),仍然使用 256 个元素的LUT,它显示优化的收益递减。虽然它的迭代次数较少,但由于处理量的增加,内部逻辑变得复杂,它在桌面上的表现也是如此,而在移动设备上的速度只有约 10%。
要应用的最终优化技术 - 展开循环。因为我们循环了固定的次数,所以我们可以从技术上手工写出这一切。我用一个随机变量尝试了一次r
,我不断重新分配,性能下降。但是预先分配了四个变量的随机数据,然后使用查找表,并应用适当的 RFC 位,这个版本将它们全部吸光:
var lut = []; for (var i=0; i<256; i++) { lut[i] = (i<16?'0':'')+(i).toString(16); }
function e7()
{
var d0 = Math.random()*0xffffffff|0;
var d1 = Math.random()*0xffffffff|0;
var d2 = Math.random()*0xffffffff|0;
var d3 = Math.random()*0xffffffff|0;
return lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'-'+
lut[d1&0xff]+lut[d1>>8&0xff]+'-'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'-'+
lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'-'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+
lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff];
}
console.log(e7())
module化:http ://jcward.com/UUID.js -UUID.generate()
有趣的是,生成 16 字节的随机数据是容易的部分。整个技巧是用符合 RFC 的字符串格式表达它,并且它最紧密地使用 16 字节的随机数据、一个展开的循环和查找表来实现。
我希望我的逻辑是正确的——在这种单调乏味的工作中很容易出错。但输出对我来说看起来不错。我希望你喜欢这个疯狂的代码优化之旅!
请注意:我的主要目标是展示和教授潜在的优化策略。其他答案涵盖了重要的主题,例如碰撞和真正的随机数,这对于生成良好的 UUID 很重要。
用:
let uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2);
如果 ID 的生成间隔超过 1 毫秒,则它们是 100% 唯一的。
如果以较短的间隔生成两个 ID,并假设随机方法是真正随机的,则生成的 ID 有 99.99999999999999% 的可能性是全局唯一的(10^15 中的 1 个发生冲突)。
您可以通过添加更多数字来增加此数字,但要生成 100% 唯一 ID,您将需要使用全局计数器。
如果您需要 RFC 兼容性,此格式将作为有效的第 4 版 GUID 传递:
let u = Date.now().toString(16) + Math.random().toString(16) + '0'.repeat(16);
let guid = [u.substr(0,8), u.substr(8,4), '4000-8' + u.substr(13,3), u.substr(16,12)].join('-');
上面的代码遵循意图,但不是RFC的字母。在其他差异中,有几个随机数字短。(如果需要,添加更多随机数字)好处是这真的很快:) 你可以在这里测试你的 GUID 的有效性