如何生成类似于 “aX4j9Z” 的短 UID (在 JS 中)

113

为了我的 JavaScript 网站应用程序,我想生成短的 GUID(用于不同类型的对象 - 字符串和字符串数组)

我希望我的 UID(GUID)类似于 "aX4j9Z"。

所以这些 UID 应该足够轻量级,以便进行 Web 传输和 JS 字符串处理,并且对于结构不是非常庞大的情况下(不超过10k个元素),具有相当的唯一性。我的意思是,在生成 UID 后,我可以检查此 UID 是否已存在于结构中,并在需要时重新生成它。


2
什么是“uid”和“guid”?生成唯一值的最简单方法是从类似于“x”的字符串开始,然后附加由计数器生成的数字,这样您就可以得到“x0”,“x1”等。 “unique”在什么上下文中使用?元素ID和名称?某个对象的属性?还是其他? - RobG
1
只需放置一个有关此主题的GISTS搜索链接:https://gist.github.com/search?l=JavaScript&q=unique+id - vsync
13个回答

153

如果您没有特殊需求,请查看@Mohamed的答案,使用预打包的解决方案(shortid)比其他任何解决方案更好。


使用6个字符的字母数字序列已足够随机索引10k个集合(366=22亿,363=46656)。

function generateUID() {
    // I generate the UID from two parts here 
    // to ensure the random number provide enough bits.
    var firstPart = (Math.random() * 46656) | 0;
    var secondPart = (Math.random() * 46656) | 0;
    firstPart = ("000" + firstPart.toString(36)).slice(-3);
    secondPart = ("000" + secondPart.toString(36)).slice(-3);
    return firstPart + secondPart;
}

随机生成的UID在生成约 √N 个数字后将会发生碰撞(生日悖论),因此为了安全起见,需要生成6位数而无需进行检查(旧版本仅生成4位数,如果不进行检查,则在生成1300个ID后就会发生碰撞)。

如果进行碰撞检查,则数字位数可以减少3或4位,但请注意,当您生成越来越多的UID时,性能将线性降低。

var _generatedUIDs = {};
function generateUIDWithCollisionChecking() {
    while (true) {
        var uid = ("0000" + ((Math.random() * Math.pow(36, 4)) | 0).toString(36)).slice(-4);
        if (!_generatedUIDs.hasOwnProperty(uid)) {
            _generatedUIDs[uid] = true;
            return uid;
        }
    }
}
考虑使用顺序生成器(例如user134_item1user134_item2,…),如果您需要唯一性而不是不可预测性。您可以"哈希"顺序生成的字符串以恢复不可预测性。
使用Math.random生成的UID不安全(您也不应该信任客户端)。在关键任务中不要依赖于其唯一性或不可预测性。

4
如果生成的随机数为“0”、“0.000000000001”等,导致最终字符串变成“4z”,那么添加“0000”可以确保它至少有4个字符长度。 - user578895
1
@RobG: 因为楼主想要一个“短”UID。 - kennytm
1
@RobG:如果只需要在一个会话中为客户端保证唯一性,确保没有竞争条件,那么这个方法很有效。 - kennytm
@ChrisScott 是的,10000个ID中有2%的碰撞概率。生成7位数字可以将其降低到0.06%。但只要ID是随机生成且没有相关性,就始终存在小概率的碰撞可能性。 - kennytm
1
对于小块随机ID,这种方法效果++,简短而不需要下载外部库。目前用于为动态创建的HTML元素生成ID。 - Mark Carpenter Jr
显示剩余9条评论

77

更新于08/2020:

shortid已被nanoid取代,它更小更快:

  • 小巧。 108字节(经过最小化处理和gzip压缩)。无依赖项。Size Limit控制大小。
  • 快速。比UUID快40%。
  • 安全。它使用密码学强大的随机API。可在集群中使用。
  • 紧凑。它使用比UUID更大的字母表(A-Za-z0-9_-)。因此,ID大小从36减少到21个符号。
  • 可移植。Nano ID已移植到14种编程语言。
import { nanoid } from 'nanoid'

// 21 characters (default)
// ~149 billion years needed, in order to have a 1% probability of at least one collision.
console.log(nanoid()) //=> "V1StGXR8_Z5jdHi6B-myT"

// 11 characters
// ~139 years needed, in order to have a 1% probability of at least one collision.
console.log(nanoid(11)) //=> "bdkjNOkq9PO"

了解更多信息:https://zelark.github.io/nano-id-cc/


旧回答

还有一个很棒的npm包:shortid

惊人的短且不连续友好的URL唯一ID生成器。

ShortId创建惊人短且不连续的友好的URL唯一ID,非常适合用于URL缩短、MongoDB和Redis id以及用户可能会看到的任何其他id。

  • 默认情况下是7-14个友好的URL字符:A-Z、a-z、0-9、_-
  • 不连续,因此不可预测。
  • 支持集群(自动)、自定义种子、自定义字母表。
  • 可以生成任意数量的id而不重复,甚至每天数百万次。
  • 非常适合游戏,特别是如果您担心作弊,因此不希望使用易于猜测的ID。
  • 应用程序可以重新启动任意次数,而不会重复使用ID。
  • 流行的Mongo ID/Mongoose ID替代品。
  • 适用于Node、io.js和Web浏览器。
  • 包括Mocha测试。

用法

var shortid = require('shortid');
console.log(shortid.generate()); //PPBqWA9

在我看来,这应该是被接受的答案。另一个选项,远远不如这个好,当然是用空格替换连字符,replace(/[-]/g, ''),这样可以缩短到32个字符长度。 - What Would Be Cool
2
非常不同意“下载可能有害的软件包”应该是编程问题的答案。 - Fredrik Schön
6
非常强烈地不同意。开发人员来这里是为了获得代码答案和学习,而不是看到NPM软件包的下载链接,无论它们的完美程度如何。根据我的使用情况,我不能使用任何软件包,并且必须将解决方案集成到代码本身中。 - vsync
13
我不同意那些持不同意见的人!原帖想要UIDs,没有提到你额外的限制。对于大多数人来说,使用经过良好测试的库比重新发明轮子更好,是正确的解决方案。如果你有不同的问题,可以发布一个不同的问题或替代回答。 - charles-allen
@Mohamed Ramrami:nanoid() 的结果是唯一的吗? - Abolfazl Roshanzamir
显示剩余2条评论

36

这里有一个一行代码,但它只会提供小写字母和数字:

var uuid = Math.random().toString(36).slice(-6);

console.log(uuid);


4
可以输入 Date.now() 来获取具有一定含义的序列: Math.floor(Date.now() / 1000).toString(36); - Campbeln
1
@Campbeln 注意,这将导致很多重复。 - Oliver Dixon

14

获取一个从100000000开始的简单计数器,将该数字转换为36进制。

(100000000).toString(36);  //1njchs

(2100000000).toString(36); //yqaadc

就像 YouTube 一样,您可以轻松地拥有 20 亿个优雅独特的 ID。


2
我发现这很有帮助;不用下载整个包或添加一个函数来完成这个任务,这真是不错。我使用自纪元以来的毫秒数:(Math.round(Date.now())).toString(36) - dave
1
@dave 有一件需要考虑的事情是,如果您一次处理大量数据,您经常会生成相同的ID。我使用 for (let i = 0; i < 1000; i++) { console.log(Math.round(Date.now()).toString(36)); } 直接在Chrome浏览器的控制台中输入,每次批处理约有50个重复的ID。如果一次处理大量数据,这将不必要地限制您的数据处理能力。 - Steven
@dave,我不确定你为什么要用Math.round()包装Date.now(),因为Date.now()会返回一个整数。否则,当ID只需要偶尔使用一次时,自然的解决方案就是这样。 - Steven

5
考虑到 ES6 特性的实际能力,我们可以在 JavaScript 中生成哈希 ID,而不依赖于第三方库。
我使用 JavaScript 提供的内置函数实现了一个非常简单的生成器。此实现在下面的代码中使用了 Crypto.getRandomValues()Uint8Array()

const hashID = size => {
  const MASK = 0x3d
  const LETTERS = 'abcdefghijklmnopqrstuvwxyz'
  const NUMBERS = '1234567890'
  const charset = `${NUMBERS}${LETTERS}${LETTERS.toUpperCase()}`.split('')

  const bytes = new Uint8Array(size)
  crypto.getRandomValues(bytes)

  return bytes.reduce((acc, byte) => `${acc}${charset[byte & MASK]}`, '')
}

console.log({id: hashID(6)})

该实现使用以下字符:[A-Z], [a-z], [0-9],总共有62个字符,如果我们添加 _-,则可以完整达到64个字符,如下所示:

const hashID = size => {
  const MASK = 0x3d
  const LETTERS = 'abcdefghijklmnopqrstuvwxyz'
  const NUMBERS = '1234567890'
  const charset = `${NUMBERS}${LETTERS}${LETTERS.toUpperCase()}_-`.split('')

  const bytes = new Uint8Array(size)
  crypto.getRandomValues(bytes)

  return bytes.reduce((acc, byte) => `${acc}${charset[byte & MASK]}`, '')
}

console.log(`id: ${hashID(6)}`)

注意:

每小时生成1000个ID,每个ID长度为6个字符,需要大约2天的时间才能达到至少发生一次碰撞的1%概率。在将其应用于您的项目时,请牢记这一点。


此外,还可以查看广泛支持的 crypto.randomUUID 方法:https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID - Grafluxe
不,实际上用户询问如何使用JavaScript生成一个6个字符长的哈希字符串。randomUUID是一个生成包含36个字符长UUID的字符串的函数。其次,它需要使用HTTPS。因此,它太具体了,对于提出问题的用户可能没有用处。 - Teocci

5
下面的代码生成了238,328个由3个字符组成的唯一值,如果大小写敏感并且数字在所有位置上都允许出现。如果需要大小写不敏感,则从chars字符串中删除大写或小写字符,它将生成42,875个唯一值。
可以轻松地适应使第一个字符始终为字母或全部为字母的情况。
无疑可以进行优化,并且在达到限制时也可以拒绝返回id。
var nextId = (function() {
  var nextIndex = [0,0,0];
  var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
  var num = chars.length;

  return function() {
    var a = nextIndex[0];
    var b = nextIndex[1];
    var c = nextIndex[2];
    var id = chars[a] + chars[b] + chars[c];

    a = ++a % num;

    if (!a) {
      b = ++b % num; 

      if (!b) {
        c = ++c % num; 
      }
    }
    nextIndex = [a, b, c]; 
    return id;
  }
}());

4
var letters = 'abcdefghijklmnopqrstuvwxyz';
var numbers = '1234567890';
var charset = letters + letters.toUpperCase() + numbers;

function randomElement(array) {
    with (Math)
        return array[floor(random()*array.length)];
}

function randomString(length) {
    var R = '';
    for(var i=0; i<length; i++)
        R += randomElement(charset);
    return R;
}

1
请解释为什么会对一个正确而优雅的答案进行负评,而不对其他类似的答案进行负评,谢谢。 - ninjagecko
3
我不是那个给你点踩的人,但我几乎想再给你点一次踩,只因为你使用了 with(Math) 这种让人想吐的写法 :) - user578895
@cwolves - 我认为 with 关键字可以在非性能代码中使用而无需担心问题,且 "with is EVIL" 的说法很容易被夸大了。=)这里既不涉及性能问题(如果仅是性能问题,那就不要使用它),也不会创建或覆盖变量(因为不进行任何赋值操作),也不会与全局变量混淆。相较于重新定义整个 Math 模块的全局作用域,我更愿意承受一定的性能损失。 - ninjagecko
3
@cWolves - 其实算了,我刚意识到如果一个人使用 with (Math) 并且定义了一个变量 var max = ... 那么就会覆盖掉 Math.max......... 好的,不再使用 with 了。 - ninjagecko
1
我并不是真的关心它的性能,更多的是它的细微差别,以及你必须去查找之前的代码行才能确定floorrandom实际上是什么。 - user578895

2
这将生成一系列唯一的值。它在RobG的答案基础上进行了改进,当所有值都用尽时,它会增加字符串长度。
var IdGenerator = (function () {

    var defaultCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()_-+=[]{};:?/.>,<|".split("");

    var IdGenerator = function IdGenerator(charset) {
        this._charset = (typeof charset === "undefined") ? defaultCharset : charset;
        this.reset();
    };

    IdGenerator.prototype._str = function () {
        var str = "",
            perm = this._perm,
            chars = this._charset,
            len = perm.length,
            i;
        for (i = 0; i < len; i++) {
            str += chars[perm[i]];
        }
        return str;
    };

    IdGenerator.prototype._inc = function () {
        var perm = this._perm,
            max = this._charset.length - 1,
            i;
        for (i = 0; true; i++) {
            if (i > perm.length - 1) {
                perm.push(0);
                return;
            } else {
                perm[i]++;
                if (perm[i] > max) {
                    perm[i] = 0;
                } else {
                    return;
                }
            }
        }
    };

    IdGenerator.prototype.reset = function () {
        this._perm = [];
    };

    IdGenerator.prototype.current = function () {
        return this._str();
    };

    IdGenerator.prototype.next = function () {
        this._inc();
        return this._str();
    };

    return IdGenerator;

}).call(null);

使用方法:

var g = new IdGenerator(),
    i;

for (i = 0; i < 100; i++) {
   console.log(g.next());
}

这个代码片段包含上述实现和一个递归版本。


1
这个解决方案将Math.random()与计数器相结合。 Math.random()应该提供约53位的熵(与UUIDv4的128位相比),但当与计数器结合时,应该可以提供足够的唯一性用于临时 ID。

let _id_counter = 0
function id() {
  return '_' + (_id_counter++).toString(36) + '_' + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36)
}

console.log(Array.from({length: 100}).map(() => id()))

特点:

  • 简单实现
  • 输出大约13个字符
  • 不区分大小写
  • 可安全用作HTML的id和React的key
  • 不适合用于数据库存储

1

你可以将GUID缩短为20个可打印的ASCII字符,而不会丢失信息或GUID的唯一性。

Jeff Atwood多年前就在博客中谈到了这个问题:
Equipping our ASCII Armor


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