背景
由于在项目中使用到了 webSocket 的自定义二进制协议, 需要将二进制转为后端服务中定义的 Long 型而在 JavaScript 中的 Number 类型由于自身原因, 并不能完全表示 Long 型的数字, 因此需要我们通过其他的方式来对 Long 型值进行存储
目标
在 GitHub 中, 有一个实现了在 JavaScript 中存储 Long 型的对象, 具体代码可以戳此下面, 我们通过简单讲解一下这个库的具体实现来看看如何在 JavaScript 中实现一个 Long 型如果你了解了这个实现原理, 那么与之类似的, 在 JavaScript 中实现一个 Long Long 型或者其他类型的方法也是类似的
具体实现
其实, Long 的实现很简单, 我们现在只要回归到计算机的本质即可在计算机中, 其实存储的都是 01 字符串例如, Int 占 4 个字节 (我们以 32 位操作系统为例), 而 Long 则占 8 个字节
我们在存储中只需要将数据通过二进制进行存储, 然后在操作中对二进制进行操作即可
下面我们简单的来介绍一下 Long 的各个代表操作和思想
大致步骤
数据存储
在 Long 型对象中, 我们采用了高 32 位和低 32 位, 以及加上一个符号位判断的值, 用来进行数据的存储, 具体格式如下:
- function Long(low, high, unsigned) {
- this.low = low | 0;
- this.high = high | 0;
- this.unsigned = !!unsigned;
- }
通过对高低位的存储, 从而让两个 Number 来同时表示一个 Long 型的高位和低位, 从而满足了数值的长度要求
转换为 Long 型
我们目前只介绍一个通过字符串来讲数据从 String 型转换为 Long 型, 其他的转换例如从 Number 转换为 Long 型是类似的, 我们就不过多赘述了
先看实现函数:
- function fromString(str, unsigned, radix) {
- // 处理异常情况
- if (str.length === 0)
- throw Error('empty string');
- // 处理为 0 的情况
- if (str === "NaN" || str === "Infinity" || str === "+Infinity" || str === "-Infinity")
- return ZERO;
- // 处理只有两个参数的情况
- if (typeof unsigned === 'number') {
- // For goog.math.long compatibility
- radix = unsigned,
- unsigned = false;
- } else {
- unsigned = !! unsigned;
- }
- radix = radix || 10;
- if (radix < 2 || 36 < radix)
- throw RangeError('radix');
- var p;
- if ((p = str.indexOf('-')) > 0)
- throw Error('interior hyphen');
- else if (p === 0) {
- // 转为正值处理
- return fromString(str.substring(1), unsigned, radix).neg();
- }
- // 从最高位分 8 位处理一次, 如果长度超过 8 位, 则先处理高位, 然后将高位直接乘以进制的 8 次方, 再处理低后 8 位, 循环到最后 8 位为止
- var result = ZERO;
- for (var i = 0; i < str.length; i += 8) {
- var size = Math.min(8, str.length - i),
- value = parseInt(str.substring(i, i + size), radix);
- if (size < 8) {
- var power = fromNumber(pow_dbl(radix, size));
- result = result.mul(power).add(fromNumber(value));
- } else {
- result = result.mul(radixToPower);
- result = result.add(fromNumber(value));
- }
- }
- result.unsigned = unsigned;
- return result;
- }
下面我们简单的说下这个函数的实现:
对数据进行异常处理, 排除一些边界条件
如果字符串为一个带 "-" 号的值, 则转换为正值进行处理
如果字符串为一个常规的 Long 型值, 则先从最前面的 8 位开始处理, 将其通过指定的进制转换为 Long 型的值
处理接下来的 8 位, 并且将之前的结果乘以进制数的 8 次方, 即数字高地位的合并例如: 18 = 1 * 10^1 + 8
循环上面的操作, 直到剩余的字符串长度小于 8 为止, 即可结束, 得到转换之后的 Long 型
转换为字符串
Long 型转换为字符串的方式, 与字符串转换为 Long 型的步骤差不多, 差不多是一个相反的过程
- LongPrototype.toString = function toString(radix) {
- radix = radix || 10;
- if (radix < 2 || 36 < radix)
- throw RangeError('radix');
- if (this.isZero())
- return '0';
- // 如果是负值, Unsigned 型的 Long 值永远不会为负值
- if (this.isNegative()) {
- if (this.eq(MIN_VALUE)) {
- // We need to change the Long value before it can be negated, so we remove
- // the bottom-most digit in this base and then recurse to do the rest.
- var radixLong = fromNumber(radix),
- div = this.div(radixLong),
- rem1 = div.mul(radixLong).sub(this);
- return div.toString(radix) + rem1.toInt().toString(radix);
- } else
- return '-' + this.neg().toString(radix);
- }
- // 每次处理 6 位, 处理方式与字符串转换过来是类似的, 和数学中十进制转换为 N 进制方法相同相除法
- // Do several (6) digits each time through the loop, so as to
- // minimize the calls to the very expensive emulated div.
- var radixToPower = fromNumber(pow_dbl(radix, 6), this.unsigned),
- rem = this;
- var result = '';
- while (true) {
- var remDiv = rem.div(radixToPower),
- intval = rem.sub(remDiv.mul(radixToPower)).toInt() >>> 0,
- digits = intval.toString(radix);
- rem = remDiv;
- if (rem.isZero())
- return digits + result;
- else {
- while (digits.length < 6)
- digits = '0' + digits;
- result = '' + digits + result;
- }
- }
- };
上面这个函数的实现步骤正好相反:
处理各种边界条件
如果 Long 型为一个负值, 则转换为正值进行处理, 如果 Long 型为
0x80000000
时, 则对它进行了单独处理
在处理正值 Long 型为字符串时, 操作方法与我们数学中教的转换进制的相除法类似, 具体操作为: 先除以需要转换的进制数, 得到结果和余数, 将结果重新作为被除数相除直到被除数为 0, 再将余数拼接起来即可例如: 18(10 进制) 转换为 8 进制时, 操作是:
18 = 2 * 8 + 2; 2 = 0 * 8 + 2;
, 因此结果为 0x22 只是, 在此函数中, 一次相除的是进制数的 6 次方, 其余步骤是类似的
通过上面的操作得到字符串后返回即可
Long 型相加
在知道了 Long 型的存储本质是使用高低各 32 位以后, Long 型的运算其实就已经了解了我们只需要针对特定的操作进行相对应的二进制操作, 那么我们就能够得到相对应的结果, 下面的实例是 Long 型相加的操作, 我们简单了解下:
- LongPrototype.add = function add(addend) {
- if (!isLong(addend)) addend = fromValue(addend);
- // 将每个数字分成 4 个 16 比特的块, 然后将这些块加起来
- var a48 = this.high >>> 16;
- var a32 = this.high & 0xFFFF;
- var a16 = this.low >>> 16;
- var a00 = this.low & 0xFFFF;
- var b48 = addend.high >>> 16;
- var b32 = addend.high & 0xFFFF;
- var b16 = addend.low >>> 16;
- var b00 = addend.low & 0xFFFF;
- var c48 = 0,
- c32 = 0,
- c16 = 0,
- c00 = 0;
- c00 += a00 + b00;
- c16 += c00 >>> 16;
- c00 &= 0xFFFF;
- c16 += a16 + b16;
- c32 += c16 >>> 16;
- c16 &= 0xFFFF;
- c32 += a32 + b32;
- c48 += c32 >>> 16;
- c32 &= 0xFFFF;
- c48 += a48 + b48;
- c48 &= 0xFFFF;
- return fromBits((c16 << 16) | c00, (c48 << 16) | c32, this.unsigned);
- };
通过上面的操作我们就可以知道, Long 型的四则运算等操作其实都是通过二进制和位运算来实现的并没有我们想象中的那么神秘
总结
其实, 通过阅读 Long.js 库的源码你就会发现, 在 JavaScript 中实现一个 Long 型并不难, 也许还是一个听简单的事情, 不过重要的是我们可能想象不到这种的实现方式因此, 这个也证明了我们在思考一个问题问题的同时, 我们也应该多从事情的本质来考虑, 这样就有可能得到解决方案
附录
我在 Long.js 的代码中添加了一些中文的注释, 如果有需要可以到我 folk 的仓库进行阅读学习
来源: https://juejin.im/post/5a88e148f265da4e761fd400