阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新

浮点数精度问题

经典真题

  • 为什么 console.log(0.2+0.1==0.3) 得到的值为 false

浮点数精度常见问题

在 JavaScript 中整数和浮点数都属于 number 数据类型,所有数字都是以 64 位浮点数形式储存,即便整数也是如此。 所以我们在打印 1.00 这样的浮点数的结果是 1 而非 1.00 。

在一些特殊的数值表示中,例如金额,这样看上去有点别扭,但是至少值是正确了。

然而要命的是,当浮点数做数学运算的时候,你经常会发现一些问题,举几个例子:

场景一:进行浮点值运算结果的判断

// 加法 
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.7 + 0.1); // 0.7999999999999999
console.log(0.2 + 0.4); // 0.6000000000000001
console.log(2.22 + 0.1); // 2.3200000000000003
 
// 减法
console.log(1.5 - 1.2); // 0.30000000000000004
console.log(0.3 - 0.2); // 0.09999999999999998
 
// 乘法 
console.log(19.9 * 100); // 1989.9999999999998
console.log(19.9 * 10 * 10); // 1990
console.log(9.7 * 100); // 969.9999999999999
console.log(39.7 * 100); // 3970.0000000000005
 
// 除法 
console.log(0.3 / 0.1); // 2.9999999999999996
console.log(0.69 / 10); // 0.06899999999999999

场景二:将小数乘以 10 的 n 次方取整

比如将钱币的单位,从元转化成分,经常写出来的是 parseInt(yuan*100, 10)

console.log(parseInt(0.58 * 100, 10)); // 57

场景三:四舍五入保留 n 位小数

例如我们会写出 (number).toFixed(2),但是看下面的例子:

console.log((1.335).toFixed(2)); // 1.33

在上面的例子中,我们得出的结果是 1.33,而不是预期结果 1.34。

为什么会有这样的问题

似乎是不可思议。小学生都会算的题目,JavaScript 不会?

我们来看看其真正的原因,到底为什么会产生精度丢失的问题呢?

计算机底层只有 0 和 1, 所以所有的运算最后实际上都是二进制运算。

十进制整数利用辗转相除的方法可以准确地转换为二进制数,但浮点数呢?

img

JavaScript 里的数字是采用 IEEE 754 标准的 64 位双精度浮点数。

先看下面一张图:

preview

该规范定义了浮点数的格式,对于 64 位的浮点数在内存中的表示,最高的 1 位是符号位,接着的 11 位是指数,剩下的 52 位为有效数字,具体如下:

  • 符号位 S:第 1 位是正负数符号位(sign),0 代表正数,1 代表负数
  • 指数位 E:中间的 11 位存储指数(exponent),用来表示次方数
  • 尾数位 M:最后的 52 位是尾数(mantissa),储存小数部分,超出的部分自动进一舍零

也就是说,浮点数最终在运算的时候实际上是一个符合该标准的二进制数

符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。

IEEE 754 规定,有效数字第一位默认总是 1,不保存在 64 位浮点数之中。也就是说,有效数字总是 1.xx…xx 的形式,其中 xx…xx 的部分保存在 64 位浮点数之中,最长可能为 52 位。因此,JavaScript 提供的有效数字最长为 53 个二进制位(64 位浮点的后 52 位 + 有效数字第一位的 1)。

既然限定位数,必然有截断的可能。

我们可以看一个例子:

console.log(0.1 + 0.2); // 0.30000000000000004

为了验证该例子,我们得先知道怎么将浮点数转换为二进制,整数我们可以用除 2 取余的方式,小数我们则可以用乘 2 取整的方式。

0.1 转换为二进制:

0.1 * 2,值为 0.2,小数部分 0.2,整数部分 0

0.2 * 2,值为 0.4,小数部分 0.4,整数部分 0

0.4 * 2,值为0.8,小数部分0.8,整数部分0

0.8 * 2,值为 1.6,小数部分 0.6,整数部分 1

0.6 * 2,值为 1.2,小数部分 0.2,整数部分 1

0.2 * 2,值为 0.4,小数部分 0.4,整数部分 0

从 0.2 开始循环

0.2 转换为二进制可以直接参考上述,肯定最后也是一个循环的情况

所以最终我们能得到两个循环的二进制数:

0.1:0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1100 ...

0.2:0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 ...

这两个的和的二进制就是:

sum:0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 ...

最终我们只能得到和的近似值(按照 IEEE 754 标准保留 52 位,按 0 舍 1 入来取值),然后转换为十进制数变成:

sum ≈ 0.30000000000000004

再例如:

console.log((1.335).toFixed(2)); // 1.33

因为 1.335 其实是 1.33499999999999996447286321199,toFixed 虽然是四舍五入,但是是对 1.33499999999999996447286321199 进行四五入,所以得出 1.33。

在 Javascript 中,整数精度同样存在问题,先来看看问题:

console.log(19571992547450991); // 19571992547450990
console.log(19571992547450991===19571992547450992); // true

同样的原因,在 JavaScript 中 number 类型统一按浮点数处理,整数是按最大 54 位来算,

  • 最大( 253 - 1,Number.MAX_SAFE_INTEGER、9007199254740991)
  • 最小( -(253 - 1),Number.MIN_SAFE_INTEGER、-9007199254740991)

所以只要超过这个范围,就会存在被舍去的精度问题。

当然这个问题并不只是在 Javascript 中才会出现,几乎所有的编程语言都采用了 IEEE-754 浮点数表示法,任何使用二进制浮点数的编程语言都会有这个问题。

只不过在很多其他语言中已经封装好了方法来避免精度的问题,而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。

通常这种对精度要求高的计算都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算问题。

前端也有几个不错的类库:

Math.js

Math.js 是专门为 JavaScript 和 Node.js 提供的一个广泛的数学库。它具有灵活的表达式解析器,支持符号计算,配有大量内置函数和常量,并提供集成解决方案来处理不同的数据类型。

像数字,大数字(超出安全数的数字),复数,分数,单位和矩阵。 功能强大,易于使用。

decimal.js

为 JavaScript 提供十进制类型的任意精度数值。

big.js

不仅能够支持处理 Long 类型的数据,也能够准确的处理小数的运算。

真题解答

  • 为什么 console.log(0.2+0.1==0.3) 得到的值为 false

参考答案:

因为浮点数的计算存在 round-off 问题,也就是浮点数不能够进行精确的计算。并且:

  • 不仅 JavaScript,所有遵循 IEEE 754 规范的语言都是如此;
  • 在 JavaScript 中,所有的 Number 都是以 64-bit 的双精度浮点数存储的;
  • 双精度的浮点数在这 64 位上划分为 3 段,而这 3 段也就确定了一个浮点数的值,64bit 的划分是“1-11-52”的模式,具体来说:
    • 就是 1 位最高位(最左边那一位)表示符号位,0 表示正,1 表示负;
    • 11 位表示指数部分;
    • 52 位表示尾数部分,也就是有效域部分

-EOF-

最近更新:: 2025/7/11 12:50
Contributors: AK
Prev
class 和构造函数区别
Next
严格模式