¿Cómo creo un GUID/UUID?
¿Cómo creo GUID (identificadores únicos a nivel mundial) en JavaScript? El GUID/UUID debe tener al menos 32 caracteres y debe permanecer en el rango ASCII para evitar problemas al pasarlos.
No estoy seguro de qué rutinas están disponibles en todos los navegadores, qué tan "aleatorio" y sembrado es el generador de números aleatorios incorporado, etc.
[Editado el 5 de marzo de 2023 para reflejar las mejores prácticas más recientes para producir UUID compatibles con RFC4122]
crypto.randomUUID()
ahora es estándar en todos los navegadores modernos y tiempos de ejecución JS. Sin embargo, debido a que las nuevas API del navegador están restringidas a contextos seguros , este método solo está disponible para páginas servidas localmente ( localhost
o 127.0.0.1
) o a través de HTTPS.
Para los lectores interesados en otras versiones de UUID, generando UUID en plataformas heredadas o en contextos no seguros, existe el uuid
módulo . Está bien probado y respaldado.
A falta de lo anterior, existe este método (basado en la respuesta original a esta pregunta):
function uuidv4() {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
console.log(uuidv4());
Nota: Se desaconseja encarecidamente el uso de cualquier generador de UUID que dependa de élMath.random()
(incluidos los fragmentos que aparecen en versiones anteriores de esta respuesta) por las razones que se explican mejor aquí . TL;DR: las soluciones basadas en Math.random()
no ofrecen buenas garantías de unicidad.
Los UUID (Universally Unique IDentifier), también conocidos como GUID (Globally Unique IDentifier), según RFC 4122 , son identificadores diseñados para proporcionar ciertas garantías de unicidad.
Si bien es posible implementar UUID compatibles con RFC en unas pocas líneas de código JavaScript (por ejemplo, consulte la respuesta de @broofa a continuación), existen varios errores comunes:
- Formato de identificación no válido (los UUID deben tener el formato "
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
", donde x es uno de [0-9, af] M es uno de [1-5] y N es [8, 9, a o b] - Uso de una fuente de aleatoriedad de baja calidad (como
Math.random
)
Por lo tanto, se anima a los desarrolladores que escriben código para entornos de producción a utilizar una implementación rigurosa y bien mantenida, como el módulo uuid .
Realmente me gusta lo limpia que es la respuesta de Broofa , pero es lamentable que las implementaciones deficientesMath.random
dejen la posibilidad de colisión.
Aquí hay una solución similar compatible con RFC4122 versión 4 que resuelve ese problema compensando los primeros 13 números hexadecimales por una porción hexadecimal de la marca de tiempo, y una vez agotados, los compensa por una porción hexadecimal de los microsegundos desde la carga de la página. De esa manera, incluso si Math.random
está en la misma semilla, ambos clientes tendrían que generar el UUID exactamente la misma cantidad de microsegundos desde que se cargó la página (si se admite el tiempo de alto rendimiento) Y exactamente en el mismo milisegundo (o más de 10,000 años después) para obtener el mismo 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>
Aquí tienes un violín para probar.
Fragmento modernizado para ES6
Mostrar fragmento de código
La respuesta de broofa es bastante hábil, de hecho: impresionantemente inteligente, realmente... compatible con RFC4122, algo legible y compacta. ¡Impresionante!
Pero si observa esa expresión regular, esas muchas replace()
devoluciones de llamada toString()
y Math.random()
llamadas a funciones (donde solo usa cuatro bits del resultado y desperdicia el resto), puede comenzar a preguntarse sobre el rendimiento. De hecho, joelpt incluso decidió descartar un RFC para la velocidad del GUID genérico con generateQuickGUID
.
Pero, ¿podemos conseguir velocidad y cumplimiento de RFC? ¡Yo digo si! ¿Podemos mantener la legibilidad? Bueno... En realidad no, pero es fácil si lo sigues.
Pero primero, mis resultados, en comparación con broofa guid
(la respuesta aceptada) y el que no cumple con 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.
Entonces, en mi sexta iteración de optimizaciones, superé la respuesta más popular por más de 12 veces , la respuesta aceptada por más de 9 veces y la respuesta rápida que no cumple por 2 o 3 veces . Y sigo cumpliendo con RFC 4122.
¿Interesado en cómo? Puse la fuente completa en http://jsfiddle.net/jcward/7hyaC/3/ y en https://jsben.ch/xczxS
Para una explicación, comencemos con el código de 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())
Por lo tanto, lo reemplaza x
con cualquier dígito hexadecimal aleatorio, y
con datos aleatorios (excepto forzar que los dos bits superiores cumplan con 10
la especificación RFC), y la expresión regular no coincide con los caracteres -
o 4
, por lo que no tiene que lidiar con ellos. Muy, muy hábil.
Lo primero que debe saber es que las llamadas a funciones son costosas, al igual que las expresiones regulares (aunque solo usa 1, tiene 32 devoluciones de llamada, una para cada coincidencia, y en cada una de las 32 devoluciones de llamada llama a Math.random() y v. a Cadena (16)).
El primer paso hacia el rendimiento es eliminar RegEx y sus funciones de devolución de llamada y utilizar un bucle simple en su lugar. Esto significa que tenemos que lidiar con los personajes -
y 4
mientras que broofa no lo hizo. Además, tenga en cuenta que podemos usar la indexación de String Array para mantener su elegante arquitectura de plantilla de String:
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())
Básicamente, la misma lógica interna, excepto que verificamos -
o 4
y usamos un bucle while (en lugar de replace()
devoluciones de llamada) ¡nos brinda una mejora de casi 3 veces!
El siguiente paso es pequeño en el escritorio, pero marca una diferencia decente en el móvil. Hagamos menos llamadas a Math.random() y utilicemos todos esos bits aleatorios en lugar de desechar el 87% de ellos con un búfer aleatorio que se desplaza en cada iteración. También saquemos la definición de plantilla del ciclo, por si acaso ayuda:
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())
Esto nos ahorra entre un 10 y un 30% según la plataforma. Nada mal. Pero el siguiente gran paso elimina por completo las llamadas a la función toString con un clásico de optimización: la tabla de búsqueda. Una tabla de búsqueda simple de 16 elementos realizará el trabajo de toString(16) en mucho menos tiempo:
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())
La siguiente optimización es otro clásico. Dado que solo manejamos cuatro bits de salida en cada iteración del bucle, reduzcamos el número de bucles a la mitad y procesemos ocho bits en cada iteración. Esto es complicado ya que todavía tenemos que manejar las posiciones de bits que cumplen con RFC, pero no es demasiado difícil. Luego tenemos que crear una tabla de búsqueda más grande (16x16 o 256) para almacenar 0x00 - 0xFF, y la construimos solo una vez, fuera de la función 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())
Probé un e6() que procesa 16 bits a la vez, todavía usando la LUT de 256 elementos , y mostró los rendimientos decrecientes de la optimización. Aunque tenía menos iteraciones, la lógica interna se complicaba por el mayor procesamiento y funcionaba igual en el escritorio, y solo ~10% más rápido en el móvil.
La última técnica de optimización a aplicar es desenrollar el bucle. Dado que estamos repitiendo un número fijo de veces, técnicamente podemos escribir todo esto a mano. Intenté esto una vez con una única variable aleatoria, r
que seguí reasignando, y el rendimiento se desplomó. Pero con cuatro variables asignadas datos aleatorios desde el principio, luego usando la tabla de búsqueda y aplicando los bits RFC adecuados, esta versión los elimina a todos:
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())
Modualizado: http://jcward.com/UUID.js -UUID.generate()
Lo curioso es que generar 16 bytes de datos aleatorios es la parte fácil. Todo el truco consiste en expresarlo en formato de cadena que cumpla con RFC, y se logra mejor con 16 bytes de datos aleatorios, un bucle desenrollado y una tabla de búsqueda.
Espero que mi lógica sea correcta: es muy fácil cometer un error en este tipo de trabajo tedioso. Pero los resultados me parecen buenos. ¡Espero que hayas disfrutado de este loco viaje a través de la optimización del código!
Tenga en cuenta: mi objetivo principal era mostrar y enseñar posibles estrategias de optimización. Otras respuestas cubren temas importantes como colisiones y números verdaderamente aleatorios, que son importantes para generar buenos UUID.
Usar:
let uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2);
Mostrar fragmento de código
Si los ID se generan con una diferencia de más de 1 milisegundo, son 100 % únicos.
Si se generan dos ID en intervalos más cortos, y suponiendo que el método aleatorio es verdaderamente aleatorio, esto generaría ID que tienen un 99,99999999999999% de probabilidad de ser globalmente únicos (colisión en 1 de 10^15).
Puede aumentar este número agregando más dígitos, pero para generar identificaciones 100% únicas necesitará usar un contador global.
Si necesita compatibilidad RFC, este formato se considerará un GUID de versión 4 válido:
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('-');
Mostrar fragmento de código
El código anterior sigue la intención, pero no la letra del RFC. Entre otras discrepancias, le faltan unos pocos dígitos aleatorios. (Agregue más dígitos aleatorios si lo necesita) La ventaja es que esto es realmente rápido :) Puede probar la validez de su GUID aquí