上周造对账 fixture,我把订单接口的返回贴进转换器转成 PHP 数组。三天后才暴露:日志里 user_id 是 9007199254740993,种子里是 9007199254740992;金额 9223372036854775807 抄出来成了 9223372036854776000。不报错也不告警,值自己变了。
我扒了模板实现、跑完 88 组用例,结论是:绝大多数变形发生在第一步的 JSON.parse,不在拼代码那一步。
变形点:值先落进 JS 的 number
链路很短:读字符串、JSON.parse、按类型拼成 '键' => 值,数字那条分支(模板 L107)就是把 parse 完的 number 直接字符串化。值一旦落进 double,缩进再漂亮也不是原值。2^53=9007199254740992 是 JS 整数的精确上限,边界上原样,边界之上没有保证。控件只有输出格式、缩进和是否加 <?php 标签,没有大整数保真开关。
| JSON 里的值 | 转出的字面量 | PHP 侧 json_decode |
|---|---|---|
9007199254740993 | 9007199254740992 | integer |
12345678901234567890 | 12345678901234567000 | double 1.2345678901234567E+19 |
9223372036854775807 | 9223372036854776000 | integer,差 193 |
最后一行最难受:PHP 自己接是原值,只有先过 JS 才塌。订单号、雪花 ID、微秒时间戳基本都是裸数字。
浮点没有统一规律,无穷是算出来的
- 21 位的
3.14159265358979323846只剩 16 位的 3.141592653589793。 - 17 位有效数字内的
0.30000000000000004和1.2345678901234567一字不改。 - 尾随零被吃掉:
1.500成 1.5,-0成 0,1.5e3成 1500。 - 写法会换:
1e20出 100000000000000000000,1e21出 1e+21,1e-324下溢成 0。
超大指数会算出无穷,1e999 出 Infinity、-1e999 出 -Infinity。PHP 里没有 Infinity 字面量、对应常量是 INF,这轮没跑 php -l,我不断言那行能不能编译。
null、空容器、重复键、键序
{} 与 [] 转出来都是 [];切到 PHP 对象那档,空对象连 (object) 前缀都没有,{"a":[{},[]]} 出 'a' => [ [], [], ]。重复键静默合并,{"a":1,"a":2,"a":3} 只剩 'a' => 3,19 字节塌成 14。数字样的键名会重排,{"01":"a","10":"b"} 里 '10' 排在 '01' 前。{"z":null} 出 'z' => null 不塌;{"s":"007"} 也不转成数字、不 trim。
中文键、\u 转义与引号
中文没有障碍:{"用户名称":"张三"} 出 '用户名称' => '张三';保留字、带空格和中划线的键照常输出。\u 在 parse 阶段就被解掉,{"s":"\u4e2d\u6587"} 出来是 2 个汉字。转义只管反斜杠和单引号,{"a'b":"x"} 变 'a\'b',双引号原样落进单引号串。
复制回来的不等于生成的
输出区用 innerHTML 渲染高亮,不转义尖括号和 &,复制按钮拿的却是 textContent。{"s":"a<b>c</b>"} 复制回来是 's' => 'ac',这条未经 DOM 时长度是 24;{"<?php":"x"} 的键名整个消失,只剩 ' => 'x'。所以 diff 要 diff 原文和最终数组。
规模与深度:从慢到崩
图 1:三档同一模板连跑,err 全是空串,没有静默失败也没有截断告警。
层数带来的不是慢而是崩:depth=100 输出 20401 字符,depth=1000 出 2004001,depth=2000 出 8008001,depth=5000 起就是 Maximum call stack size exceeded。缩进每层多 2×depth 个空格,总长按平方顶上去,可用栈深落在 1000 与 5000 之间;对象嵌套阈值给不出,那几条用例本身非法。
图 2:深度每上一档,输出字符按平方涨。
报错只有一行英文
红框标题固定是 JSON 解析错误,正文就是 e.message,输出区与统计一起清空。{"n":007} 报 Unexpected number...position 6,{"a":1,} 报 Expected double-quoted property name...position 7;{"a":1}{"b":2} 报 Unexpected non-whitespace character...position 7,一次只认一个文档。空白输入只显示空态,带 BOM 的照常出;报错是否本地化我没有样本。一贴就报 position 1 的,我先怀疑原文,拿 JSON 格式化 试一遍确认它本身合不合法。
我现在的核对姿势
整段接口返回落成 PHP 数组我用的是 JSON 转 PHP 数组,上面的数字都出自它同一份模板。之后固定三个动作:
- 2^53 以上的字段别过 JS,在 PHP 里读原文 json_decode 再自己导出。
- 转完比长度不比样子,莫名变短的八成被显示层吃掉了。
- 键序和空容器单独验,光看代码看不出来;核原文层级我按 树形视图折叠定位那篇 的做法。
PHP 对象那档只给走对象分支的层加 (object) 前缀,顶层数组和标量都不加,输出里没有 stdClass 字样。
Q:为什么 20 位整数一定会变,0.30000000000000004 反而一字不改?
A:取决于能不能被 double 精确表示,不取决于长得怪不怪。超出 2^53 的整数只能落到最近的可表示值上;而 0.30000000000000004 是 double 打印出来的样子。
Q:{} 和 [] 转出来一样,怎么知道原文是哪个?
A:转出的代码里问不出来:两条分支连字节数都相同,对象那档也救不了空对象,只能回原文核。
Q:只转出一半,是被截断了吗?
A:先分清少的是尾巴还是中间,带尖括号少中间那种跟体积无关。最大样本 100000 项、输出 5877811 字节,这一档没截断也没告警,更大的没实测。