上週做對帳 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 位元組,這一檔沒截斷也沒告警,更大的沒實測。