为什么 JavaScript 中的 0.1 + 0.2 不等于 0.3

这篇具有很好参考价值的文章主要介绍了为什么 JavaScript 中的 0.1 + 0.2 不等于 0.3。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文作者为 360 奇舞团前端开发工程师

在使用 JavaScript 处理运算时,有时会碰到数字运算结果不符合预期的情况,比如经典的 0.1 + 0.2 不等于 0.3。当然这种问题不只存在于 JavaScript,不过编程语言的一些原理大致相通,我以 JavaScript 为例解释这种问题,并说明前端如何尽可能保证数字精确。

let a = 0.1,b=0.2,c=0.3
console.log(a + b === c) //false

1. 计算机数字如何存储

理解类似问题的基础,首先要理解计算机数字的处理方式。计算机的一切信息都是二进制,数字也不例外,所有数字都是一段二进制。

在 JavaScript 中存储数字的二进制有 64 位,即我们常说的 64 位双精度浮点型数字。每个数字对应的 64 位二进制分为三段:符号位、指数位、尾数位。

其中符号位在六十四位的第一位,0 表示正数,1 表示负数。符号位之后的 11 位是指数位,决定了数字的范围。指数位之后的 52 位是尾数位,决定了数字的精度。

在 JavaScript 中,双精度浮点型的数转化成二进制的数保存,读取时根据指数位和尾数位的值转化成双精度浮点数。

比如说存储 8.8125 这个数,它的整数部分的二进制是 1000,小数部分的二进制是 1101。这两部分连起来是 1000.1101,但是存储到内存中小数点会消失,因为计算机只能存储 0 和 1。

1000.1101 这个二进制数用科学计数法表示是 1.0001101 * 2^3,这里的 3 (二进制是 0011)即为指数。

可使用如下代码查看:

function getBinaryRepresentation(number) {
  const buffer = new ArrayBuffer(8); // 创建一个包含8字节的   ArrayBuffer
  const view = new DataView(buffer); // 创建一个DataView以便访问内存中的数据
  view.setFloat64(0, number); // 将浮点数写入到内存中

  // 读取内存中的字节,并将其转换为二进制字符串
  const binaryString = Array.from(new Uint8Array(buffer))
    .map(byte => byte.toString(2).padStart(8, '0'))
    .join('');

  // 将二进制字符串分割为符号位、指数位和尾数位
  const signBit = binaryString[0];
  const exponentBits = binaryString.substring(1, 12);
  const mantissaBits = binaryString.substring(12,64);

  return { signBit, exponentBits, mantissaBits };
}

const number = 8.8125; // 要展示的数字
const { signBit, exponentBits, mantissaBits } = 	getBinaryRepresentation(number);

console.log(`符号位: ${signBit}`);
console.log(`指数位: ${exponentBits}`);
console.log(`尾数位: ${mantissaBits}`);

符号位: 0
指数位: 10000000010
尾数位: 0001101000000000000000000000000000000000000000000000

现在我们很容易判断符号位是 0,尾数位就是科学计数法的小数部分 0001101。指数位用来存储科学计数法的指数,此处为 3。指数位有正负,11 位指数位表示的指数范围是 -1023~1024,所以指数 3 的指数位存储为 1026(3 + 1023)。

可以判断 JavaScript 数值的最大值为 53 位二进制的最大值:2^53 -1。

PS:科学计数法中小数点前的 1 可以省略,因为这一位永远是 1。比如 0.5 二进制科学计数为 1.00 * 2^-1。

2. 为什么会产生小数精度问题

首先补充一下小数的二进制的计算方法:

十进制小数转为二进制与整数相反,需要每次乘以 2
8.8125
o.8125*2 = 1.625  => 1
0.625*2 = 1.25      =>1
0.25*2 = 0.5          =>0
0.5*2 = 1                 =>1
小数部分为 1101
二进制小数转为十进制
1*2^-1 + 1*2^-2 + 0*2^-3 + 1*2^-4

在了解数字的存储后,很容易理解小数精度问题,因为十进制有 Π 这种无限循环数字,二进制也有循环数字。比如让 0.1 变为二进制,按照二进制转换永远会有余数,所以会是一个无限循环的二进制 0.0001 1001 1001 1001...(1100循环)。0.2 也是同理  0.0011 0011 0011 0011...(0011循环)。

所以当两个浮点数相加时,结果会有一些误差。比如 0.1 + 0.2 ,实际上是 0.0001 1001 1001...(1001循环) + 0.0011 0011 0011...(0011循环),如果截取于第 52 位,就会得到一个有误差的结果,转为十进制为0.30000000000000004,与 0.3 不相等。

3. 前端如何保证小数准确

首先出于安全性及准确性考虑,重要的数字计算应该交给服务端负责,相对于前端,服务端有更成熟稳定的数字处理方法,安全性也会更高。

当然前端有时也需要一些精确的数字计算,比如一些动画处理、定时器处理以及一些条件判断等。我简单列举几种方法供大家参考:

  • toFixed 指定小数位数 这种方法比较简单,不过有个点要注意,这个方法是四舍五入,但有时候看上去并不会,比如 2.55.toFixed(1) 显示的结果是 2.5 而不是 2.6。这是因为 2.55 二进制存储的值并不精确,调用 2.55.toPrecision(100) 可以看到这个数的实际值是 2.5499..... ,所以截取一位四舍五入是 2.5。再举一个例子 (2.449999999999999999).toFixed(1) = 2.5,因为这个数与 2.45 的差值小于 Number.EPSILON。

  • 将小数转为整数计算 这个方法的问题是转换会增加额外的复杂度和计算量,在某些场景下,可能会导致数值溢出问题。

  • 第三方库 精确计算推荐使用成熟的库,像 BigNumber.js、decimal.js ,进行高精度的浮点数计算。原理是把数字计算变为字符串计算。

JavaScript 的计算比较复杂,由于没有细分数字类型,底层计算以二进制进行,存储值、计算值都有可能因为精度丢失而不准确,而显示值可能会因为浏览器等宿主环境不同而有差别,所以一定要注意经常产生精度丢失的地方。

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

为什么 JavaScript 中的 0.1 + 0.2 不等于 0.3,javascript,开发语言,ecmascript,前端文章来源地址https://www.toymoban.com/news/detail-839176.html

到了这里,关于为什么 JavaScript 中的 0.1 + 0.2 不等于 0.3的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 为什么两个向量的内积等于模长乘夹角?

      已知两个向量 a = [ a 1 , a 2 ] a=[a_1,a_2] a = [ a 1 ​ , a 2 ​ ] 和 b = [ b 1 , b 2 ] b=[b_1,b_2] b = [ b 1 ​ , b 2 ​ ] ,他们的内积为 a b = a 1 b 1 + a 2 b 2 ab=a_1b_1+a_2b_2 ab = a 1 ​ b 1 ​ + a 2 ​ b 2 ​ ,看书上的定义该内积的值是一个标量,并且等于两个向量的模长的乘积再乘夹角,即:

    2024年01月23日
    浏览(22)
  • 标准化拉普拉斯矩阵特征值范围为什么小于等于2?(证明)

    谱图使用标准化拉普拉斯矩阵 L n o r m L^{norm} L n or m 的一个重要原因就是, L n o r m L^{norm} L n or m 比拉普拉斯矩阵 L L L 稳定。很多资料只是简单地介绍了 L n o r m L^{norm} L n or m ,在kipfGCN中也只是简单地提到 L n o r m L^{norm} L n or m 的特征值不大于2。本文搜集了相关lecture,并推导

    2024年02月11日
    浏览(23)
  • 为什么特征值的重数大于等于线性无关特征向量的个数

    关系就是,特征值的重数 ≥ 该特征值的线性无关向量的个数 ≥ 1 量化关系有 特征值的重数,称为 代数重数 ,等于Jordan矩阵中特征值为λ的Jordan块的阶数之和 特征向量的个数,称为 几何重数 ,等于Jordan矩阵中特征值为λ的Jordan块的个数 证明 先说结论 每个矩阵 等价 于一个

    2024年02月11日
    浏览(28)
  • C++ 为什么double类型不能直接判断等于0 两个double类型怎么判断相等

    精度丢失, 十进制小数部分在转换成2进制的时候经常会出现无限位的二进制小数,计算机存储小数有长度限制,所以会进行截取部分小数进行存储,计算机只能存储大概的值,而不是精确的值 。 例如: 判断一个单精度浮点数:则是 if( abs(f) = 1e-6); 要判断一个双精度浮点数

    2024年02月12日
    浏览(21)
  • TypeScript:为什么JavaScript需要类型检查?

    JavaScript是当今最为流行的编程语言之一。它是一种高级的、解释性的编程语言,用于Web应用程序的开发。然而,JavaScript的灵活性也是它的弱点之一。JavaScript中的变量、函数、类等都是动态类型,这意味着它们的类型可以在运行时发生变化。虽然这种灵活性为JavaScript开发人员

    2024年02月04日
    浏览(22)
  • 为什么要去了解javascript的底层?

    JavaScript的基本数据类型包括:数字、字符串、布尔值、null、undefined。其中,数字类型可以是整数或浮点数,字符串类型用单引号或双引号表示,布尔值只有true和false两个取值,null表示一个空值,undefined表示一个未定义的值。 在JavaScript底层实现中,每种数据类型都有相应的

    2024年02月01日
    浏览(17)
  • JavaScript——为什么静态方法不能调用非静态方法

    个人简介 👀 个人主页: 前端杂货铺 🙋‍♂️ 学习方向: 主攻前端方向,正逐渐往全干发展 📃 个人状态: 研发工程师,现效力于中国工业软件事业 🚀 人生格言: 积跬步至千里,积小流成江海 🥇 推荐学习:🍍前端面试宝典 🍉Vue2 🍋Vue3 🍓Vue2/3项目实战 🥝Node.js🍒

    2024年02月11日
    浏览(25)
  • Golang 中的 slice 为什么是并发不安全的?

      在Go语言中,slice是并发不安全的,主要有以下两个原因:数据竞争、内存重分配。   数据竞争:slice底层的结构体包含一个指向底层数组的指针和该数组的长度,当多个协程并发访问同一个slice时,有可能会出现数据竞争的问题。例如,一个协程在修改slice的长度,而

    2024年02月05日
    浏览(22)
  • Golang 中的 map 为什么是并发不安全的?

      golang 中的 map 是并发不安全的,多个 go 协程同时对同一个 map 进行读写操作时,会导致数据竞争(data race)问题,程序会 panic。   如果一个协程正在写入 map,而另一个协程正在读取或写入 map,那么就有可能出现一些未定义的行为,例如:读取到的值可能是过期的、不

    2024年02月05日
    浏览(23)
  • Java中的实体类为什么要 implements Serializable?

    1. 序列化和反序列化 首先来解释一下什么是序列化和反序列化: 序列化 :把对象转换为字节序列的过程称为对象的序列化。 反序列化 :把字节序列恢复为对象的过程称为对象的反序列化。 在 Java 和其他语言进行通信的时候,需要将对象转化成一种通用的格式例如Json( 转换

    2023年04月20日
    浏览(17)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包