踩坑记:当 JavaScript 遇上 UINT 64

作者:link

导语

写下这篇文章的缘由是因为在项目过程中,碰到了一个使用JavaScript处理 UINT64 类型数字的坑。

与大部分现代编程语言(包括几乎所有的脚本语言)一样,JavaScript中的数字类型是基于 IEEE 754 标准来实现的,该标准通常也被称为“浮点数”。JavaScript使用的是“双精度”格式(即64位二进制)。

较小的数值

不仅仅是JavaScript,所有遵循 IEEE 754 规范的语言都会碰到如下问题:

0.1 + 0.2 === 0.3; // false

从数学角度来说,上面的条件判断结果应该是true,可实际上却为false。

这是因为,二进制浮点数中的 0.1 和 0.2 并不是十分精确,它们相加的结果并非刚好等于 0.3 ,而是一个比较接近的数字 0.30000000000000004, 所以条件判断的结果为false。

那么该如何处理这种语言上的缺陷呢?

最常见的方法是设置一个误差范围,通常称为“机器精度”(machine epsilon),对JavaScript的数字类型来说,这个值通常是2^-52(2.220446049250313e-16)。

从 ES6 开始,该值定义在Number.EPSILON中,我们可以直接拿来用,也可以为 ES6 之前的版本写polyfill:

if (!Number.EPSILON) {
	Number.EPSILON = Math.pow(2, -52);
}

可以使用Number.EPSILON来比较两个数字是否相等(在指定的误差范围内):

function numbersCloseEnoughToEqual(n1, n2) {
	return Math.abs(n1 - n2) < Number.EPSILON;
}

let a = 0.1 + 0.2;
let b = 0.3;

numbersCloseEnoughToEqual(a, b); // true
numbersCloseEnoughToEqual(0.0000001, 0.0000002); // false

JavaScript中能够呈现的最大浮点数大约是 1.798e+308(这是一个相当大的数字),它定义在 Number.MAX_VALUE中。最小浮点数定义在 Number.MIN_VALUE中,大约是5e-324,它不是负数,但无限接近于0!

JavaScript中整数的安全范围

上述JavaScript数字的呈现方式决定了“整数”的安全范围远远小于 Number.MAX_VALUE。

能够被“安全”呈现的最大整数是2^53 - 1,即 9007199254740991,在 ES6 中被定义为Number.MAX_SAFE_INTEGER。最小整数是 -9007199254740991,在 ES6 中被定义为Number.MIN_SAFE_INTEGER。

实际上,在前端的应用场景中正负 2^52 - 1 是一个绝对够用的安全整数范围,然而在NodeJS的服务端开发中就不一定了,如数据库中的64位ID(现在QQ号已经需要用UINT64来存储了)。由于JavaScript的数字类型无法精确呈现64位的数值,所以比较将它们保存(转换)为字符串。

我遇到的坑

上个项目,在使用Protocol Buffer协议(下文简称PB协议)与其他语言的后台服务通信的过程中(关于Protocol Buffer协议的介绍可以参考本人的这篇文章),需要将从A服务拿到一个UINT64类型(用户帐号)的整数透传给B服务。

其实之前也在PB协议中遇到过UINT64类型定义的字段,但是当这个UINT64整型小于Number.MAX_SAFE_INTEGER时,我们将它当作正常的Number类型处理是完全没有问题的。不过,这次我遇到的UINT64字段的值全都大于Number.MAX_SAFE_INTEGER,这时我还将它当作Number类型来处理,导致B服务中根本查询不到我传过去的用户帐号。

例如,我从A服务拿到的实际用户帐号是144115197458450067,当我将它转换成Number后,变成了144115197458450080,传给B服务后,B服务告诉我系统中没有这个用户。。。没有debug之前我还以为是B服务出了bug,因为我啥都没做,就是数据透传而已啊!

解决方案

当我们确实需要在JavaScript中对大数值进行处理时,目前还是需要借助相关的工具库。

实际上在使用JavaScript进行PB通信时,我会使用ProtoBuf.js这个库帮我处理pb到json的类型转换,而ProtoBuf.js本身是依赖了一个工具库 long.js 来对 int64uint64 进行处理,long.js 会将上述两种类型转换成long类型对象实例。long.js提供了很多API供我们操作,比如将long类型对象实例转换成其他类型(Number,String,Buffer),或者将一个其它类型转换成long类型对象实例,具体的API可参考 Long.js API

例如,当我从A服务拿到一个UINT64类型的值longValueFromA,此时并需要进行处理时

const Long = require('long'),

function longHandleToString(v) {
	if(Long.isLong(v)) {
		return v.toString(); // 正确
	}
	return v;
}

function longHandleToNumber(v) {
	if(Long.isLong(v)) {
		return v.toNumber(); // 错误!v很可能大于Number.MAX_SAFE_INTEGER,转换成Number后会不精确。
	}
	return v;
}

// longValueFromA已经是一个long类型对象实例:Long { low: 2056032594, high: 33554434, unsigned: true}
longValue = longHandleToString(longValueFromA); // '144115198721823058' 正确!
// longValue = longHandleToNumber(longValueFromA); // 144115197458450080 错误!

然后再将longValue通过PB协议传给B服务时,要做一次类型转换,将string类型转换成long类型对象实例。

longValueToB = Long.fromString(longValue, true);

参考资料

广告时间

你现在看到的文章,是由搭建在 实惠好用的腾讯云服务器 上的IVWEB前端社区所提供!

原文链接:http://ivweb.io/topic/581a8e44b8bfa6564c54cff9

相关推荐

包学会之浅入浅出Vue.js:开学篇

一个只有99行代码的JS流程框架

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏python3

pymysql--插入300万数据

        (2)使用python协程(遇到I/O操作就切换任务,无需等待--提高效率)

752
来自专栏飞雪无情的博客

Go语言实战笔记(十七)| Go 读写锁

前面的有篇文章在讲资源竞争的时候,讲互斥锁,互斥锁的根本就是当一个goroutine访问的时候,其他goroutine都不能访问,这样肯定保证了资源的同步,避免...

472
来自专栏我是攻城师

浅谈Lucene中的DocValues

2913
来自专栏乐百川的学习频道

设计模式(二十四) 访问者模式

访问者模式提供了一种方法,将算法和数据结构分离。假设我们需要对一个数据结构进行不同的操作,就可以考虑使用访问者模式。访问者模式的要点在于,需要一个访问者接口,提...

2026
来自专栏Golang语言社区

多线程编程10个例子--2

// TODO: Add extra initialization here m_ctrlProgress.SetRange(0,99); m_nMilliSe...

4727
来自专栏北京马哥教育

JVM性能调优监控工具jps、jstack、jmap、jhat、jstat、hprof使用详解

DK本身提供了很多方便的JVM性能调优监控工具,除了集成式的VisualVM和jConsole外,还有jps、jstack、jmap、jhat、jstat、hp...

3115
来自专栏jeremy的技术点滴

py3_cookbook_notes_03

2643
来自专栏SDNLAB

Open vSwitch系列之openflow版本兼容

众所周知Open vSwitch支持的openflow版本从1.0到1.5版本(当前Open vSwitch版本是2.3.2)通过阅读代码,处理openflow...

45113
来自专栏维C果糖

史上最简单的 MySQL 教程(六)「SQL 基本操作 之 数据操作」

根据操作对象的不同,咱们可以将 SQL 的基本操作分为三类,分别为:库操作、表(字段)操作和数据操作。

41913
来自专栏Android源码框架分析

Android内存分配/回收的一个问题-为什么内存使用很少的时候也GC

Android应用建立在Java虚拟机之上的,Google为了保证同时多个APP运行并及时唤醒,就为每个虚拟机设置了最大可使用内存,通过adb命令可以查看相应的...

1414

扫码关注云+社区