一道面试题的思考
今天在群里讨论,突然想起之前的一道面试题,特地拿出来讲解一下。同时进行记录与分享,如有不对,敬请指正。((Math.random() * (2 ** 31 - 1)) << 0).toString(36);
请谈谈你对上述代码的理解,这段代码主要用来生成一个随机字符串。涉及到的知识点很多。
Math.random()
会生成一个 [0-1) 区间的随机数(包含0);**
是 V8 在 ES7 中新加入的幂运算符,功能上相当于Math.pow
。但相比 Math.pow,**
运算符的运算效率更高(某些情况下,两者的运算结果可能并不相等);Number.prototype.toString
可以把一个 Number 类型的数字按照特定的“进制”进行编码;toString(36)
表示将当前的数字以 BASE36 (相当于36进制)的方式进行格式化。BASE36 的格式化方法同 BASE64,只不过基准元字符为 “[0-9a-z]” 共36个字符。- “<<” 左移运算符只支持 32 位数字的左移运算,在这里用来去除小数位,只保留整数位;
- 因此这里使用
2 ** 31 - 1
为 << 左移运算符的最大安全数字; - 上述的生成随机字符串代码可以用:
((Math.random() * (2 ** 53 - 1))).toString(36).split(".")[0];
来代替; - 扩展解析:*
2 ** 53 - 1
这里的 “2 ** 53 - 1” 其实是 JS 中的最大安全整数,即 Number.MAX_SAFE_INTEGER
。JS 中对浮点数的规范是基于 IEEE754 规范制定的。
首先需要知道的是,在 JS 中所有的 Number 数字类型在内存中都是以 64 位双精度浮点类型无差别对待的。因此某种程度上,Typed Array(Int8Array、Int16Array 等)这种数据类型可以降低我们对内存的使用率。在 IEEE754 协议下,64 位的双精度浮点被分为“1位符号位”,“11位指数位”和“52位的小数位”。
比如小数 0.1 对应的二进制格式的小数位为:“1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001…(无限循环)”,由于内存中 IEEE754 协议规定最多只预留52位小数位,因此这里的无限循环会在内存中进行“零舍一入”,变成“1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010”。同理其他小数也会有类似的问题,因此常见的 0.1 + 0.2 不等于 0.3 的问题便是由此诞生的。
Number.MAX_VALUE & Number.MAX_SAFE_INTEGER?
由于指数位只有11位,因此能够取得的最大值为 (2048 - 1) = 2047,由于指数有正有负,为了使正指数与负指数平衡,这里 IEEE754 选择一个偏移量 bias,其值为1023。因此将可选的指数范围转换为-1023到1024。
当指数位的值位2047时,即指数为 (2047 - bias) = 1024 时,此时代表的值为正负 Inifinity。当指数位为0时,即指数为 (0 - bias) = -1023 时,此时代表的值为正负 0。因此这两个极端值均无法代表最大或最小的可见值。
因此当可见的最大正整值是为当符号位为0,指数值为1023,小数位全为1时。即 (-1)**0 * 2**1023 * (Number.parseInt( "1".repeat(53) ,2) * 2**-52);
, 同理最小正整值为 (-1)**0 * 2**-1022 * (Number.parseInt("0".repeat(52) + "1", 2) * 2**-52);
由上述推断可以得出,所谓最大安全整数即 Number.MAX_SAFE_INTEGER
就是在小于该范围的所有整数在内存中都可以进行准确的存储(不会超过最大的小数位)。因此只有当小数位长度小于等于52时,才能保证该整数的“安全性”,因此改变指数位的值为 52,即每个小数位都表示浮点数的整数部分。所得的最大安全整数为 (-1)**0 * 2**52 * (Number.parseInt("1".repeat(53),2) * 2**-52);
, 即 2 ** 53 - 1
。
评论 | Comments