【JavaScript】你都如何得到一个ID?
在前端开发中,我们总会有时需要一个唯一的id。不知道你都是怎么做的呢?
时间戳
在前端获取当前的时间戳,一般使用Date.now()就足够了。
但是同步场景中的连续调用,会返回同一个时间,而在许多场景下我们经常会选择时间戳做唯一性标识。
vite
在 vite 的源码中有一个方法来做这件事。就是简单的+1
let lastDateNow = 0
/**
* Similar to `Date.now()`, but strictly monotonically increasing.
*
* This function will never return the same value.
* Thus, the value may differ from the actual time.
*
* related: https://github.com/vitejs/vite/issues/19804
*/
export function monotonicDateNow(): number {
const now = Date.now()
if (now > lastDateNow) {
lastDateNow = now
return lastDateNow
}
lastDateNow++
return lastDateNow
}
yeast
在yeast中,同样使用了时间戳,但是通过再增加一个序号,来严格确保唯一。
- 额外的,
yeast通过base64字符表映射0-9的数字,极大缩短了标识符的长度。
'use strict';
var alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'.split('')
, length = 64
, map = {}
, seed = 0
, i = 0
, prev;
/**
* Return a string representing the specified number.
*
* @param {Number} num The number to convert.
* @returns {String} The string representation of the number.
* @api public
*/
function encode(num) {
var encoded = '';
do {
encoded = alphabet[num % length] + encoded;
num = Math.floor(num / length);
} while (num > 0);
return encoded;
}
/**
* Return the integer value specified by the given string.
*
* @param {String} str The string to convert.
* @returns {Number} The integer value represented by the string.
* @api public
*/
function decode(str) {
var decoded = 0;
for (i = 0; i < str.length; i++) {
decoded = decoded * length + map[str.charAt(i)];
}
return decoded;
}
/**
* Yeast: A tiny growing id generator.
*
* @returns {String} A unique id.
* @api public
*/
function yeast() {
var now = encode(+new Date());
if (now !== prev) return seed = 0, prev = now;
return now +'.'+ encode(seed++);
}
//
// Map each character to its index.
//
for (; i < length; i++) map[alphabet[i]] = i;
//
// Expose the `yeast`, `encode` and `decode` functions.
//
yeast.encode = encode;
yeast.decode = decode;
module.exports = yeast;
随机数
上述获取的数字都是强时间相关性的,可以唯一,但并不是真正的随机。
36进制
一个不太讲究的(但被滥用的)简洁方案是依赖Math.random()和toString(36)将随机数转成由0-9,a-z共36个字符组成的字符串。
function id() {
Math.random().toString(36).slice(2); // '4y352sxop'
}
nanoid
nanoid提供了更加易于理解的一个方法,同样用了base64字符。
/* @ts-self-types="./index.d.ts" */
let urlAlphabet =
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'
export let nanoid = (size = 21) => {
let id = ''
let i = size | 0
while (i--) {
id += urlAlphabet[(Math.random() * 64) | 0]
}
return id
}
crypto.getRandomValues
但是Math.random可以被polyfill修改,被认为是不安全的。所以nanoid改为利用window.crypto.getRandomValues()来获取随机数。
crypto.getRandomValues(new Uint8Array(21))就是生成一个长度为21的uint8数组,每个元素都是0-255的随机数。通过取uint8的后6位,拿到一个0-63的数字,再从字符表中拿到字符。
/* @ts-self-types="./index.d.ts" */
import { urlAlphabet as scopedUrlAlphabet } from './url-alphabet/index.js'
export let nanoid = (size = 21) => {
let id = ''
let bytes = crypto.getRandomValues(new Uint8Array((size |= 0)))
while (size--) {
// 63 is 00111111.
// This mask is used to get 6 bits from random byte.
// bytes[size] & 63 gives a number from 0 to 63.
// scopedUrlAlphabet[bytes[size] & 63] gives a symbol from alphabet.
id += scopedUrlAlphabet[bytes[size] & 63]
}
return id
}
上难度
你敢说确定理解上面的代码了是吧?👆🏻
除了使用给定的64个字符外,nanoid还提供了一个使用自定义字符集的方法
export let random = bytes => crypto.getRandomValues(new Uint8Array(bytes))
export let customRandom = (alphabet, defaultSize, getRandom) => {
let mask = (2 << Math.log2(alphabet.length - 1)) - 1
let step = -~((1.6 * mask * defaultSize) / alphabet.length)
return (size = defaultSize) => {
let id = ''
while (true) {
let bytes = getRandom(step)
let j = step | 0
while (j--) {
id += alphabet[bytes[j] & mask] || ''
if (id.length >= size) return id
}
}
}
}
export let customAlphabet = (alphabet, size = 21) =>
customRandom(alphabet, size | 0, random)
眼花缭乱啊,先看调用方法
customAlphabet('abcdefg', 21);
// 第一个是字符集
// 第二个是预备生成多长的字符串(也就是随机选择多少次)
再看封装的random(),就是生个n个字节。
整个customRandom的主逻辑还是一个while循环。但其中有些位操作需要解释下
let mask = (2 << Math.log2(alphabet.length - 1)) - 1
Math.log2(alphabet.length - 1)计算字母表长度减1后以2为底的对数2 << Math.log2(...)将2左移相应位数,相当于找到大于等于字母表长度的最小2的幂次- 最后减
1得到掩码,这个掩码用于从随机字节中提取足够的位数来覆盖整个字母表
例如,如果字母表长度为62: Math.log2(61) ≈ 5.932 << 5.93 ≈ 2 << 5 = 64mask = 64 - 1 = 63
let step = -~((1.6 * mask * defaultSize) / alphabet.length)
- 这个公式计算每次需要生成的随机字节数量
1.6 * mask * defaultSize是一个经验公式,用于确保有足够的随机数据- 除以
alphabet.length进行调整 -~是一个技巧性的向上取整操作(等同于Math.ceil())- 这确保生成足够的随机字节来创建指定长度的
ID
let j = step | 0
虽然 -~ 操作会产生一个整数,但由于浮点数运算的特性,step 仍然可能是一个浮点数。为了确保j是一个整数(用于循环计数),使用 step | 0 来强制转换为整数。
这是比 Math.floor() 或 parseInt() 更高效的整数转换方法。
while (true) {
let bytes = getRandom(step)
}
这里外层循环是为了得到足够的bytes,在第一版中字符集和长度是已知的。现在字符集的长度不一定是8,16,32,64,128等,有可能落在某个区间,比如26个字符的话就不足32,我们预测的mask=(2 << Math.log2(26 - 1)) - 1 = 31 ,这超出了26的范围,所以有可能访问到alphabet[27~31]出现空字符。这种时候就需要重新生成一个。 所以就有了step = -~((1.6 * mask * defaultSize) / alphabet.length)这个奇怪的公式,而不是直接用size。
更多推荐


所有评论(0)