
1. 先搞清楚 Number 是什么再谈怎么用1.1 Number 对象到底是个啥很多前端同学写了好几年 JavaScript天天跟数字打交道但真被问到“Number 是什么”答不上来的不在少数。这里先给个结论在 JavaScript 里Number既是原始值的类型名也是一个内置构造函数同时还挂着一堆静态方法。同一个名字承担三个角色这是 JS 里面比较特殊的设计不像 Java 的Integer分得那么清楚。我们平时代码里写的42、3.14、-7从类型层面看都是number原始值。原始值不是对象没有自己的方法但当你对原始值调用方法的时候JS 引擎会临时把它包装成一个Number对象方法执行完再拆回原始值。这就是为什么你能写出(3.14).toFixed(2)这种代码。换句话说你写代码时直接操作的是number原始值Number对象更多是站在背后提供方法和常量。Number对象能解决什么问题说白了就是三件事数字的合法性判断、数字的格式化输出、数字的边界处理。再往深了说它牵扯出的是 JavaScript 底层如何存储数字、如何与字符串等其他类型互转、如何避免计算精度翻车。这些内容做业务开发时不一定天天碰但一旦碰上就是大坑。1.2 IEEE 754 双精度浮点天天在用却没几个人讲人话JavaScript 里的所有 number 类型底层统一采用 IEEE 754 标准的双精度浮点数存储也就是 64 位二进制。这 64 位怎么分配其实不复杂1 位符号位11 位指数位52 位尾数位。符号位决定正负指数位决定数值范围尾数位决定精度。听起来抽象拿生活中的例子打个比方你在一张 A4 纸上写数字纸的大小编程了指数能表示的上下限纸的格子密度决定了小数点后能写几位。双精度浮点就是一张“格子有限”的纸能表示的数很多但绝不是所有实数都能精确表示。这就是 0.1 0.2 不等于 0.3 的根本原因后面工作区实操部分我会详细拆。这个设计带来的直接影响有三点能表示的范围极大最大约1.7976931348623157e308日常业务很难触顶。能精确表示的整数有限超过Number.MAX_SAFE_INTEGER9007199254740991就开始丢精度。很多十进制小数比如 0.1、0.2在二进制下是无限循环小数存储时只能舍入所以浮点计算会有误差。理解了这三点再回头看 Number 对象的各种方法和常量你会突然发现它们都是为了在这张“格子有限的纸”上帮你做判断、做取舍、防翻车。理解底层模型比背 API 重要得多这也是这篇文章第一段想强调的核心。2. Number 对象的正确打开方式2.1 Number() 函数的两种形态Number可以作为构造函数用new Number()调也可以当普通函数直接调Number()。这两种用法的结果完全不同很多新手在这里踩坑。直接调用Number(value)结果是强制类型转换返回一个 number 原始值。比如Number(42); // 42 Number(); // 0 Number(true); // 1 Number(null); // 0 Number(undefined);// NaN Number(12px); // NaN而new Number(value)返回的是一个 Number 对象typeof 结果是 object。这在日常开发里几乎用不到反而容易引起判断异常。比如const a new Number(42); typeof a; // object a 42; // false我个人的原则是永远不要用new Number()。需要转换就老老实实用Number()或者直接用一元加号value两者在大多数场景下行为一致但Number()可读性更好语义也更明确。还有一个平时容易忽略的点Number()转换null和undefined的结果。Number(null)是 0Number(undefined)是 NaN。这个差异在做数据兜底的时候会坑到你。比如后端返回的字段可能是 null你想安全地转成数字直接Number(value)会得到 0听着不错但如果是 undefined就直接 NaN然后 NaN 参与运算会把整个计算结果污染成 NaN。所以做兜底转换时先判断value null往往更稳妥。2.2 挂在 Number 上的常量都是救命的Number 对象挂了一批静态属性与其说是“属性”不如说是“边界常量”。它们告诉你这张 A4 纸到底有多大、格子到底有多密。常量值含义Number.MAX_VALUE1.7976931348623157e308能表示的最大正数Number.MIN_VALUE5e-324能表示的最小正数最接近 0 的正数Number.MAX_SAFE_INTEGER9007199254740991能精确表示的整数上限Number.MIN_SAFE_INTEGER-9007199254740991能精确表示的整数下限Number.POSITIVE_INFINITYInfinity正无穷Number.NEGATIVE_INFINITY-Infinity负无穷Number.EPSILON2.220446049250313e-161 与大于 1 的最小可表示数之差Number.NaNNaN非数字值这里面MAX_SAFE_INTEGER和EPSILON是实际开发中用得最多的两个。MAX_SAFE_INTEGER主要用于判断后端返回的 ID 或者订单号是否超出安全整数范围超了就要用字符串或者 BigInt 处理。EPSILON则常用来做浮点结果的近似比较后面精度问题那一节会单独说。关键是这些常量不要死记硬背要理解它们背后的意义MAX_VALUE告诉你数字范围有多大MAX_SAFE_INTEGER告诉你哪些整数是可靠的EPSILON告诉你浮点误差的容忍下限。知道边界在哪写代码才能做出合理判断。2.3 isNaN 与 Number.isNaN差的是世界观isNaN()和Number.isNaN()是面试高频题也是实际开发中容易用错的地方。两者的差别一句话就能说清isNaN()会先做类型转换再判断Number.isNaN()不会。isNaN(abc); // true因为 Number(abc) 是 NaN Number.isNaN(abc); // false因为 abc 是个字符串压根没被转换 isNaN(42); // false因为 Number(42) 是 42 Number.isNaN(42); // false当然也是 false isNaN(undefined); // true Number.isNaN(undefined); // false看到区别了没有isNaN()的本意是“这个值转成数字后是不是 NaN”而Number.isNaN()的本意是“这个值本身是不是 NaN 类型”。写代码时我基本只用Number.isNaN()因为 JavaScript 里 NaN 是唯一一个不等于自身的值判断它是必须先确认类型再比对否则容易出逻辑错误。再补充一个隐藏知识点全局的NaN本身是一个属性属于全局对象类型是 number但它不代表任何有效的数字值。在实际项目里NaN 一旦混入数据链路会像病毒一样扩散任何包含 NaN 的运算结果都是 NaN所以早判断、早拦截比事后排查成本低得多。3. 核心方法侧重哪些能放心用哪些暗坑多3.1 parseInt 与 parseFloat 的细节差异parseInt(12px)返回 12这个大家都熟。但parseInt有个第二参数radix很多人从来不写。第二参数指定进制范围从 2 到 36默认行为比较复杂字符串以 0x 开头按 16 进制解析以 0 开头在旧引擎里可能按 8 进制在现代引擎里按 10 进制。这种历史遗留的不确定性就是 ESLint 强制要求必须写第二参数的原因。实际开发里我建议一律写成parseInt(value, 10)parseInt(08, 10); // 8 parseInt(08); // 8现代引擎 parseInt(0x10, 16); // 16 parseInt(0x10); // 16parseInt的解析规则是按字符逐个解析能转就转遇到非数字字符就停但开头是空格会跳过。parseFloat类似区别在于它能解析到一个有效的小数点比如parseFloat(3.14rem)返回 3.14。两者都不接受空字符串parseInt()是 NaN。还有一个经常被忽略的坑parseInt遇到数字超过安全整数范围时会丢失精度。比如parseInt(9007199254740993)出现在浏览器里时返回的是9007199254740992。如果后端返回的订单号超过 16 位你拿parseInt去转取回来就已经变了。处理超长数字串直接用字符串存或者用 BigInt千万别用 parseInt。3.2 toFixed、toPrecision、toExponential 的三兄弟关系这三个方法都是格式化数字成字符串的但作用不同toFixed(digits)固定小数点后位数四舍五入返回字符串。toPrecision(precision)总共保留多少位有效数字返回字符串。toExponential(digits)用科学计数法表示返回字符串。我用toFixed(2)做金额展示的频率最高但它有两个坑需要特别注意。第一个坑toFixed用的是“四舍五入”吗不完全是。在部分浏览器或 V8 引擎中(1.005).toFixed(2)的结果是 1.00 而不是 1.01原因是 1.005 在二进制浮点表示下实际是 1.00499999……所以被舍掉。这是浮点精度和 toFixed 两重因素叠加导致的。做金额舍入别依赖 toFixed自己写一个基于乘除法的四舍五入函数更可控。第二个坑toFixed的返回值是字符串不是数字。如果你拿它参与后续计算比如price.toFixed(2) 1结果会变成字符串拼接 10.001不是 11。转换回来要用Number()或者一元加号但在转换过程中又可能引入新的浮点误差所以更推荐把精确计算环节控制在“分”这个整数单位上进行展示环节再格式化。toPrecision更侧重“有效数字”适合工程计算或科学场景。比如(1234).toPrecision(2)返回 1.2e3这往往不是业务想要的展示格式。我的建议是展示类需求用toFixed计算类需求用整数运算只有图表类工具类场景才考虑toPrecision。3.3 toString 里的进制转换反而是宝藏很少有人把toString和数字对象联系起来但(255).toString(16)返回 ff(10).toString(2)返回 1010这个进制转换能力在做颜色转换、二进制处理时非常实用。注意这里有个语法细节数字字面量后面直接点.toString()会报错因为 JS 引擎把255.理解为小数部分。正确写法是255..toString()或者(255).toString()加上括号最直观。如果要反向解析配合parseInt即可parseInt(ff, 16); // 255 parseInt(77, 8); // 63这套组合在写工具函数时很常用比如颜色值 RGBA 与 HEX 互转、字节大小格式化、位运算辅助等。虽然 ES6 之后有了Number.prototype.toString的默认行为但进制转换能力属于那种“平时不用、用到就特别香”的功能值得记在脑子里。4. 工作里真实碰到的 Number 问题逐个复盘4.1 0.1 0.2 不是 0.3这个坑不止是“经典题”0.1 0.2 0.3的结果是 false稍微有点经验的开发者都知道。但真正的问题是业务里面怎么处理这种浮点误差先看一眼现象0.1 0.2; // 0.30000000000000004 0.3 - 0.1; // 0.19999999999999998 1.07 * 100; // 107.00000000000001第一种处理思路是设定一个容差范围用Number.EPSILON做近似比较function isEqual(a, b) { return Math.abs(a - b) Number.EPSILON; } isEqual(0.1 0.2, 0.3); // true这个方案适合“判断两个数是否大致相等”的场景但EPSILON是 1 附近的精度值数字变大或者变小后浮点误差的绝对值也会变化所以更稳妥的做法是Math.abs(a - b) 1e-9之类根据业务量级设定的值。第二种处理思路是从源头规避浮点误差金额计算统一用“分”做单位也就是整数运算。例如 10.01 元存成 1001 分两个金额相加就是整数相加结果精确无比。展示时再除以 100 转回元。这是电商和支付领域最通用的做法没有之一。第三种处理思路是应用 BigDecimal 风格的库比如 decimal.js、bignumber.js。如果计算复杂、金额精度要求极高不要自己造轮子直接用成熟的库。4.2 NaN 英文、Infinity 和 -0最容易被忽视的三个异类先说 NaN 的传播性。任何包含 NaN 的算术运算都返回 NaNNaN 1; // NaN NaN * 0; // NaN Math.sqrt(-1); // NaN更麻烦的是NaN 不等于任何值包括它自己所以你不能用x NaN判断。查一个值是不是 NaN 必须用Number.isNaN(x)或Object.is(x, NaN)。之前做数据清洗时我一个同事用parseInt解析了一串脏数据拿到 NaN 后没有拦截后面所有统计指标全部变成 NaN排查了半天才定位到源头。血的教训解析入口就要做 NaN 拦截。再说 Infinity。除以 0 不会报错而是得到 Infinity1 / 0; // Infinity -1 / 0; // -Infinity如果后端返回了 Infinity前端序列化 JSON 时会直接变成 null。这是很多人在接口联调时遇到的怪事明明算出了 Infinity传过去怎么没了。JSON 标准不支持 Infinity 和 NaN 的序列化这是规范层面的限制。最后说-0。你没看错JavaScript 里有-0这个值Object.is(0, -0)返回 false但0 -0返回 true。这个值通常出现在除以负数、取反、或者浮点运算近似归零的场景。很多人第一次见到会懵但实际业务里基本不需要区分只要知道JSON.stringify(-0)的结果是 0所以不会造成数据落库的差异。4.3 大整数精度丢失后端ID超过16位就全乱后端接口返回订单号、用户ID、或者第三方平台的单号这类数据经常超过 16 位。这时候 JSON.parse 的默认行为会把大数字解析成 number精度直接受损。假设 ID 是 9007199254740993解析后变成 9007199254740992前后端对账时就会出现差 1 的诡异问题。处理思路有三种后端把超长ID改成字符串返回这是最推荐的做法。前端用特殊 JSON 解析方案把大数字当字符串处理比如 JSON.parse 时自定义 reviver或者用json-bigint这类库。传给后端时用字符串后端自己保证转换能力。做数据可视化和报表项目时我特别喜欢给后端同学强调所有超过 15 位的数字统一走字符串。因为这不是前端能“兜底”的问题而是数据精度从源头就不能丢的问题。5. 数字边界安全整数、BigInt 与数值设计5.1 MAX_SAFE_INTEGER 为什么是 9007199254740991这个数字的由来跟双精度浮点结构直接相关。52 位尾数加上隐含的一位一共可以精确表示 53 位二进制有效数字。2 的 53 次方是 9007199254740992所以安全整数上限是2^53 - 1即 9007199254740991。超过这个数部分整数无法精确表示会就近舍入到最近的可用浮点数。举个例子Number.MAX_SAFE_INTEGER; // 9007199254740991 Number.MAX_SAFE_INTEGER 1; // 9007199254740992能表示 Number.MAX_SAFE_INTEGER 2; // 9007199254740992丢了1 Number.MAX_SAFE_INTEGER 3; // 9007199254740994又出现可以看到从某个点开始整数不再连续加 2 和加 1 的结果一样。这不是 bug而是浮点数表示的物理限制。日常开发判断一个数字是否安全直接这样写Number.isSafeInteger(value)它内部会判断 value 是否在安全整数范围内同时确认没有精度损失。在做数据校验时如果发现超大范围的数字就要考虑是不是该用字符串或者 BigInt。5.2 BigInt大整数场景下的救兵ES2020 引入了 BigInt用来表示任意精度的整数。声明方式是在数字后面加nconst big 9007199254740993n; typeof big; // bigintBigInt 和 number 是两种类型不能直接混合运算1n 1; // Uncaught TypeError必须显式转换但转换又可能丢精度所以对 BigInt 的运算要保持谨慎。BigInt 不支持Math库的方法也不能直接传给JSON.stringify序列化会报错。这些限制决定了它在实际项目中的使用场景比较窄大多数时候是后端返回超长整数用字符串前端不需要做算术运算只需要透传展示。如果真的遇到需要计算大整数的场景比如加密算法、大数据量统计、区块链相关开发BigInt 是标准方案。不过业务系统里轻易别用因为它会污染类型体系牵扯的面比较大。5.3 工程中我设计数值字段的几条经验在系统设计层面数值字段最容易出问题的环节不是前端算法而是“类型约定”。总结几条我自己的经验金额、单价、费率这类字段约定用“最小单位整数”传递和存储。比如元转成分前后端都是整数从根本上消灭小数计算。ID、订单号、流水号这类字段约定字符串。哪怕现在是短 ID保不齐业务后期扩展成长 ID一开始就定为字符串最省心。统计类数据比如人数、点击量、计数器一般不会超过安全整数范围用 number 没问题。坐标、百分比、汇率这类天然是浮点的字段明确约定小数位数计算时放大到整数展示时格式化。任何参与精确计算的数字都不要在中间环节用字符串和数字混用。这套约定看似简单但执行起来需要前后端联调时反复对齐。把“什么字段用什么类型”写进接口文档比在代码里到处做防御要高效得多。6. 开发自查最容易踩到的隐式转换与常见问题6.1 隐式转换才是Number问题的大头很多人以为 Number 对象的坑是精度但实际业务中出现频率更高的是隐式类型转换。JS 的运算符在字符串和数字之间按照特定规则运作一不注意就是“拼接”而不是“相加”5 3; // 53因为 遇到字符串执行拼接 5 - 3; // 2因为 - 强制转数字 5 * 3; // 15看第一行5 3的结果是 53。这在表单输入场景里特别常见input.value永远是字符串如果你直接把两个input.value用相加得到的不是求和而是字符串拼接。解决方案很简单进入数据层之前就统一转成 number。const a Number(inputA.value); const b Number(inputB.value); const sum a b;还有一类隐式转换藏在数组和对象里[] []; // 空字符串 [] {}; // [object Object] {} []; // 0这里 {} 被当作代码块这类代码在正常业务里不会主动写但做数据处理时会意外碰到。理解了隐式转换的规则遇到诡异结果时能快速判断是不是类型搞错了排查问题会快很多。6.2 金额、ID、统计三类场景的防坑清单针对高频场景我整理一张自查表方便查漏补缺场景常见坑我的应对金额计算浮点误差导致金额异常统一用“分”整数运算展示时再格式化金额展示toFixed 舍入不准确用整数/100 的方式展示必要时走 decimal.js超长 IDparseInt 丢精度全链路走字符串接口文档写明字段类型统计计数大数字超过安全范围用 BigInt 或字符串前端不做加减表单输入input 值参与加法变拼接入口先转 number转失败拦截提示接口返回JSON 序列化 Infinity/NaN 为 null后端提前规避前端解析后过滤6.3 数值安全过滤的实用函数分享两个我项目里一直在用的工具函数直接抄走就能用。第一个是安全转数字function safeNumber(value, fallback 0) { const num Number(value); return Number.isNaN(num) ? fallback : num; } safeNumber(42); // 42 safeNumber(abc); // 0 safeNumber(null); // 0这里可能不符合预期注意 safeNumber(undefined); // 0也可能不符合预期注意Number(null)是 0所以如果你想区分“空值”和“非法值”得先判断value null。第二个是精确到指定小数位的舍入函数function roundTo(num, precision) { const factor Math.pow(10, precision); return Math.round((num Number.EPSILON) * factor) / factor; } roundTo(1.005, 2); // 1.01经过 EPSILON 修正加Number.EPSILON是为了抵消一部分浮点下溢导致的误差但这个方法在极端情况下仍然有边界问题。真正高精度的预算场景用decimal.js的toDecimalPlaces最稳妥。自己写工具函数能解决 90% 的需求剩下 10% 别硬扛直接上库。7. 一些个人体会与后续延展我自己平时写代码有个习惯凡是涉及数字的地方第一反应先问“这个值会超过安全整数范围吗”“参与计算的中间值会不会变成 NaN”而不是直接写业务逻辑。这种前置的边界意识帮我在项目里避免了很多次线上事故。说一个真实的案例有一次做数据看板运营上传的 CSV 里有个字段存的是 19 位设备序号前端直接拿来当 number 用一个月的数据展示乱掉一大片。最后排查下来就是精度丢失数据错位但没报错非常难发现。从那以后我对所有超过 15 位的“数字”一律按字符串处理。如果你想进一步深挖 Number 相关的内容把这几条串起来学效率最高先理解 IEEE 754 的存储模型再摸清楚 parseInt 和 Number() 的类型转换差异然后吃透 toFixed 和精度控制最后把浮点计算、BigInt、字符串 ID 这些边界方案在工程里落一次地。JS 的数字体系不大但每个知识点都是盘根错节连在一起的搞明白一层下一层自然就通了。