不同浏览器中的 JavaScript Math 差异
了解 ECMAScript 对 Math 的保证、实现保留的余地,以及如何测试可移植的数值行为而不把结果当作身份。
ECMAScript 定义了 JavaScript 的 Math 对象,但并未保证每个数学运算在所有浏览器中逐位相同。普通 JavaScript Number 使用 binary64 表示,因此舍入是数值模型的一部分。许多运算有严格规定的行为,另一些数学函数则允许实现按标准规则进行近似。应用应说明任务所需的精度并据此测试合同,而不是从一次观察推断所有浏览器的通用行为。
当计算结果用于决策、持久化或系统间传递时,这些细节很重要。十进制表达式不一定能用二进制精确表示,结果还可能经过解析、转换、格式化和存储。应把完整数据路径视为应用数值合同的一部分。适合显示的结果,若没有明确舍入规则,未必适合会计处理或边界决策。
ECMAScript 规定的行为
区别取决于具体运算。Math.exp 明确规定 NaN、无穷和有符号零的处理,其他输入才采用实现近似值。Math.round 则要求在两个整数距离相等时朝正无穷方向取整,并保留非有限值和整数输入。这与金融计算中相等时向偶数舍入的规则不同。应依据具体函数的合同区分规范允许的近似与应用预期错误,不能因为另一项 Math 运算允许近似,就扩大所有断言的容差。
ECMAScript 定义了 Math 提供的运算及其可观察行为。Math 对象规范按运算规定了具体要求。有些结果由标准严格确定;另一些函数的实数结果通常无法用 binary64 精确表示,标准可能允许实现进行有限近似。这并非允许任意输出。遇到影响应用的差异时,应找出具体运算并查阅对应要求,不要从单个例子推导普遍规则。
有限精度可以解释许多看似意外的结果。二进制浮点数以有限个 2 的幂组合来存储,许多常见十进制小数在这种系统中无法精确表示。因此,运算期间或数值转换时都可能发生舍入。看似等价的表达式也可能因中间值的求值顺序不同而产生不同舍入。这是表示法的正常结果,但应用若假定十进制可精确相等,或选用了不合适的算法,仍可能造成问题。
Math 周围的 JavaScript 代码还会引入其他数值边界。文本解析、JSON 输入、隐式类型转换、整数转 Number 和输出序列化,都可能在运算前后影响数值。若两个浏览器看起来结果不同,应固定输入与序列化方式,并检查其间执行的运算。否则,变化的测试数据或转换规则可能被误认为运行时差异。浏览器版本本身并不能解释所有数值结果。
数值合同还取决于应用领域。货币合计、图形坐标和统计评分,对范围、精度及可接受误差有不同要求。应判断用户任务中什么结果才算正确,再将规则写进测试。不要让某种十进制显示形式或单个实现末位的可表示数字,意外变成产品要求,除非应用明确依赖它们。
应用应明确数学值转化为用户可见值的规则。计算结果的精度可能高于界面显示精度;除非产品规则明确如此,显示形式不应悄然替换已保存的值。测量工具可以保留精度供后续计算,同时显示舍入后的读数。支付流程则可能需要在金额持久化前的特定步骤舍入。应说明每个边界以哪种表示为准,并让导出遵循同一策略,避免屏幕、下载记录和后续计算因隐式转换不同而不一致。具体合同取决于应用领域,因此浏览器兼容性测试应检查应用规则,而不是要求统一的小数位数。
可移植性需要留意的地方
应有意决定各个边界上的舍入策略。应用可以在内部保留精度,只在显示时舍入。存储用户输入的十进制金额时,也可以使用缩放整数或十进制运算库。这些选择服务于不同合同。过早舍入会损失信息;没有规则地一直延后舍入,也可能产生让用户觉得不一致的合计。应规定数值在存储、交换或呈现时何处舍入,并在完整流程中保持一致。
运算顺序会影响浮点结果。代数上等价的公式,用有限精度求值时,中间结果的舍入可能不同。因此,重构公式可能改变低位数字,却不违反 ECMAScript。若数值稳定性很重要,应选择适合问题的算法并用有意义的输入范围测试。只有在确认数值需求和对应运算规范后,才考虑浏览器专用修正;否则,变通措施可能保留偶然预期,却降低实际计算可靠性。
边界比较值得特别处理,因为微小舍入差异可能选择不同分支。对资格判定、计费或安全相关任务,应定义相等时如何处理,以及比较发生在显示舍入之前还是之后,并测试有意义边界两侧的数值。应用也需要为异常值制定策略:在可能出现时,明确如何处理非有限结果、无效输入和负零。应有意拒绝、规范化或报告它们,而不要让序列化或格式化偶然决定行为。
依赖库和宿主接口也带有自己的数值行为。库可能采用不同算法或舍入策略,服务器或数据库对数值的表示方式也可能不同于 JavaScript。传递数值时,应定义线上的表示方式,并测试从解析、计算、序列化到存储的完整往返。大整数和精确十进制数量可能需要明确的字符串或结构化表示,而不是通用浮点 JSON 数值。本地化的小数分隔符属于显示规则,不会改变底层数值。
数值跨越语言或服务边界时也要同样谨慎。服务端语言可能分别提供整数和十进制类型,数据库字段也可能限制精度或范围。转换成 JavaScript Number 再转回,可能丢失源系统能够表示的差异。应明确接口传递的是整数、十进制字符串还是结构化值,并在两端校验。测试支持范围的边界值以及经持久化的完整往返。如果某个值只用于显示,应明确标注,不要把格式化字符串再作为计算输入传回。显式接口合同比依赖浏览器或服务的隐式转换更可移植。记录成功保存后哪种表示是权威值,并在重新加载时继续使用。除非产品规则如此,否则用户不应看到显示用的舍入值变成下一次计算输入。合成往返样例即可覆盖此边界,无需把个人记录加入测试。若服务拒绝超出范围的值,应返回清晰校验结果,而不是静默截断。这些约定把数值精度落实到常见应用行为中,也让跨浏览器测试能帮助维护具体流程。
隐私背景
数值结果可以被观察,但单个 Math 结果不能确定用户身份或其浏览器。结果可能取决于输入、应用代码、运行时版本及周边数据处理。将数值视为稳定身份可能造成没有依据的追踪假设和脆弱的应用行为。除非功能需要,否则不要收集数值输出;也不要在缺少明确用途和保留规则时,将诊断值附加到长期用户记录。
诊断应与问题相称。用户报告计算错误时,通常优先用合成输入构造最小复现,而不是收集其底层记录。记录具体运算和相关应用版本,不要收集无关数据。如果支持确实需要真实输入,应通过适当且由用户控制的流程取得,并在收集前定义访问与删除规则。临时工程证据应与产品分析数据分开,支持用途结束后应删除。
隐私还取决于计算周围发生的事情。应说明数值是在页面中保留、发送给服务,还是在任务后继续保存。标准库不会替应用决定这些做法。不要将诊断信息与无关账户或浏览记录关联。兼容性支持可能需要浏览器版本和功能结果,但这并不意味着需要保留应用计算过的所有结果。收集的数据应只回答支持问题,不应扩展到其他用途。
这里讨论的是数值语义,不是时钟精度或确定性随机数;这些主题分别属于浏览器计时信号和确定性浏览器行为。没有证据时,不要把 Math 结果描述为个人标识、设备型号或特定平台的证明。准确说明应用自身的数据流,比绝对化的隐私承诺或没有依据的平台结论更有用。
应用收集诊断信息时也要注意这一点。包含计算结果的支持报告应说明收集结果的必要性,以及其中是否含有用户数据。很多问题可以用生成的数值复现,不必暴露个人记录。如果支持确实需要真实输入,应将请求限制在该案例并遵循既有的同意与删除流程。不要因为数值容易记录,就保留无关的计算历史;这类记录可能暴露用户的活动规律,却无助于判断具体应用合同是否满足。与具体问题关联、限时保存的诊断,比记录所有计算结果的通用历史更容易审核和删除。
编写可重现的数值测试
从应用实际使用的运算开始。为每种运算规定代表性输入、预期领域行为和比较规则。合同要求精确相等时,例如比较整数标识或检查按明确定义格式舍入的值,可以使用精确相等断言。对于近似计算,应根据任务、结果尺度和预期误差选择有依据的容差。固定容差可能适合范围窄的数值,却不适用于跨越多个数量级的结果。
在一个小型合成测试样例中,Math.round 规定的平局规则会将输入 2.5 得到 3;任何其他结果都应判定为断言失败,而不是扩大容差的理由。这只测试该项标准规则,并非支付政策:若账单合同采用银行家舍入(ties-to-even),就需要单独定义运算和预期结果。
对于要求填写金额的支付表单,空字段应显示明确的必填提示并阻止提交,而不是产生金额为零的交易。对于已保存且规范金额为十进制字符串 12.50 的发票,只更新浏览器端格式时,已保存的金额应保持不变;如果必须更改存储架构,应先用合成记录测试明确的迁移,再替换旧架构。
除典型值外,还要测试有意义的边缘情况。包括领域边界附近的输入、零、允许时的负值、接近支持范围上下限的数值,以及无效或异常情况。即使单次计算的误差可接受,重复计算也可能累积误差,因此要测试应用实际执行的序列。目的不是列举 Math 的所有可能输出,而是覆盖会改变用户可见结果或违反领域合同的数值情况。
测试整个接口中的表示方式。若数值会被序列化、存储或与服务交换,应验证往返结果,而不只测试浏览器端计算。JSON 数值、格式化字符串、数据库字段和语言绑定各有表示规则。应定义由哪个组件舍入以及如何保留精度。将区域格式化与数值本身分开,避免小数分隔符差异被误报为算术差异。若要求序列化确定,应在应用边界规定编码,只规范合同允许的内容。
测试样例应小巧、合成,并与用户可见规则相关。边界用例应说明边界为何重要;大数用例应指出它验证的支持范围。调查变化时,记录运行时和应用修订,并让预期值对应领域规则,而非某台工作站捕获的结果。将运算单元测试与解析、计算、格式化、持久化的流程测试分开。它们捕获的变化类型不同,也能给维护者更明确的下一步。 测试数据还应覆盖数值进入应用的方式,例如表单中的空白、本地化分隔符和空字段。解析器应接受已文档化的形式,或返回清晰的校验结果。解析完成后再独立测试数学函数,并在规则变化时同步更新面向用户的解释。
如果新版本改变了数值流程,还要检查已保存的数据,而不只是新计算。决定旧记录是否仍然有效、是否需要一次迁移,或是否应保留原表示。迁移应有业务理由并使用合成记录测试,只有确实必要时才要求用户采取行动。
审查浏览器版本
浏览器更新改变数值测试时,应固定应用修订、输入样例和断言,再缩减第一个不一致案例。检查相关运算是由规范严格规定,还是允许近似。归因给浏览器之前,先检查输入转换、依赖版本、应用改动和序列化。如果断言比领域合同更严格,应说明理由后修订。不要仅为消除无法解释的差异而放宽容差,因为这可能掩盖真实应用问题。
受控比较应一次只改变一个相关变量。浏览器更新可能与操作系统库、应用包、数据集或编译产物变化同时发生。记录复现流程所需的环境,但不要收集无关机器细节。若差异仅在依赖或应用更新后出现,应分别比较这些修订。不存在一种通用规则,能说明某个浏览器族在所有运算与版本中都只有一种数值行为。
性能与正确性是两个问题。若计算影响响应速度,应使用代表性数据测量用户流程并说明工作负载。不要把耗时转为浏览器或设备标签。硬件、后台活动、电源状态和运行时变化都会影响时长,而快速结果并不能证明数值正确。性能观察应放在独立测试中,功能验收则使用数值合同。
审查数值库、编译器和已存数据时,也应采用与浏览器更新相同的纪律。依赖库可能改变算法,编译器可能改变求值,持久化数值也可能经过不同表示。维护一套小型回归测试并说明各样例的目的;领域要求或支持浏览器范围变化时,重新审查它们。在发布前记录受影响的应用流程和用户需要采取的动作,不要披露用户数值记录,也不要承诺所有浏览器都逐位一致。浏览器版本审查有助于区分运行时变化和应用变化。
如果发布改变了数值流程,还要审查已存数据和新计算。如果应用过去在不同阶段舍入,已有记录可能遵循旧规则。应判断记录是否仍有效、需要一次性迁移,还是继续保留原表示;不要只为让旧值像新浏览器结果而改写持久化数据。这些数据未必由浏览器创建,存储格式变化却可能影响所有客户端。迁移应有领域理由、可审计的步骤和合成记录测试。只有确实必要时才要求用户采取行动,并保留按用途分类的回归夹具,以区分运行时、依赖和应用变化。