La opción más sencilla, disponible en contextos seguros (HTTPS o localhost), es crypto.randomUUID():
const crearUUID = () => crypto.randomUUID();
console.log(crearUUID()); // "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Si se necesita compatibilidad con navegadores que no exponen randomUUID, se puede recurrir directamente a crypto.getRandomValues y ensamblar manualmente los 16 bytes, respetando la versión 4 y la variante definida por el RFC 4122:
function uuidDesdeBytes() {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
return [
hex.substring(0, 8),
hex.substring(8, 12),
hex.substring(12, 16),
hex.substring(16, 20),
hex.substring(20, 32)
].join('-');
}
El byte en la posición 6 se fija con el nibble alto igual a 4 para indicar la versión, mientras que el byte en la posición 8 adopta la variante 10 en sus dos bits más significativos. El resto de bytes se obtienen de forma aleatoria criptográfica. En producción se recomienda preferir siempre crypto.getRandomValues o crypto.randomUUID en lugar de Math.random, ya que este último no garanitza aleatoriedad criptográfica.
Para escenarios que requieren manipular, comparar o formatear GUIDs con distintas representaciones (N, D, B, P), resulta útil contar con una pequeña utilidda orientada a objetos:
class IdentificadorUnico {
constructor(valor = null) {
this._valido = true;
this._caracteres = [];
if (typeof valor === 'string') {
this._parsearCadena(valor);
} else {
this._inicializarNulo();
}
}
_parsearCadena(texto) {
const limpio = texto.toLowerCase().replace(/[{}()\-\s]/g, '');
if (limpio.length !== 32 || /[^0-9a-f]/.test(limpio)) {
this._inicializarNulo();
} else {
this._caracteres = Array.from(limpio);
}
}
_inicializarNulo() {
this._caracteres = Array(32).fill('0');
}
static crear() {
const datos = new Uint8Array(16);
crypto.getRandomValues(datos);
datos[6] = (datos[6] & 0x0f) | 0x40;
datos[8] = (datos[8] & 0x3f) | 0x80;
const hexadecimal = Array.from(datos, b => b.toString(16).padStart(2, '0')).join('');
return new IdentificadorUnico(hexadecimal);
}
static nulo() {
return new IdentificadorUnico();
}
esIgual(otro) {
if (!otro || !(otro instanceof IdentificadorUnico) || !otro._valido) {
return false;
}
return this.aTexto() === otro.aTexto();
}
aTexto(formato = 'D') {
const formatosPermitidos = new Set(['N', 'D', 'B', 'P']);
const tipo = String(formato).toUpperCase();
const seleccion = formatosPermitidos.has(tipo) ? tipo : 'D';
const base = this._caracteres.join('');
if (seleccion === 'N') return base;
const conGuiones = [
base.substring(0, 8),
base.substring(8, 12),
base.substring(12, 16),
base.substring(16, 20),
base.substring(20, 32)
].join('-');
switch (seleccion) {
case 'B': return `{${conGuiones}}`;
case 'P': return `(${conGuiones})`;
default: return conGuiones;
}
}
}
La clase normaliza la entrada elliminando llaves, paréntesis, guiones y espacios; si la cadena no cumple el patrón hexadecimal de 32 caracteres, se genera un identificador nulo compuesto por ceros. Los formatos disponibles siguen la convención de .NET: N sin separadores, D con guiones, B entre llaves y P entre paréntesis.
const id = IdentificadorUnico.crear();
console.log(id.aTexto('D'));
console.log(id.aTexto('B'));