先把被測對象擺出來
起因是一條關係鏈:我按爸爸、爸爸,螢幕上出「爺爺」,以為再補一下就換人,連點三下才等到「奶奶」。這個頁面沒有文字框,input 與 textarea 都是 0 個,能餵給它的輸入只有 22 個按鈕,所以「點出來什麼」完全由一張關係定義表和一段 if 鏈決定。我把它當被測對象處理了:模板本體 15121 位元組、13713 個 UTF-16 碼元、418 個換行,函式體原樣切出來跑在 DOM 樁上;插樁版與頁面真實管線在兩步 100 條、三步 1000 條鏈上逐條比結果,不一致 0 條——下面這些帳都建立在這條校驗上。
36 條規則站點,判定順序就是行號
查詢函式 lookupTerm 的區塊是第 261 到 391 行、131 行、3569 碼元。區塊內 return 出現 38 次,其中 2 次在 chain.some 的回調裡、不算規則,剩下 36 次就是 36 條稱謂站點;同一段裡 if 47 個、else 2 個、三元 14 個。順序就是原始碼順序:第 272 行的單一檔位分支最先,第 388 行那條無條件兜底最後。先後很要緊,一條鏈只要被前面的站點接住,後面寫得再細也走不到。把命中的那行摘成空語句、再看它落到哪,我挑了 5 組:
| 鏈 | 命中行 | 結果 | 摘掉命中行之後 |
|---|---|---|---|
father,mother | 386 | 長輩(女性) | 388 晚輩(女性) |
father,older_brother,wife | 290 | 姑姑 | 318 伯母 |
mother,older_brother,wife | 294 | 姨媽 | 319 舅媽 |
father,father,son | 287 | 伯伯/叔叔 | 388 晚輩(男性) |
father,son,son | 388 | 晚輩(男性) | 函式末尾再無 return |
全枚舉:兜底與二義各佔多少條
把 10 個檔位依鏈長做笛卡爾積(多維組合的行數是相乘不是相加,這類帳舊文 列出所有組合 講過),正向、男性口徑:兩步 100 條只出 28 個稱謂,兜底 34 條、二義 15 條、專名 51 條攤在 19 個詞上;三步 1000 條出 30 個詞,兜底漲到 384、二義 165、專名 451;四步 10000 條出 34 個詞,兜底 4032、二義 1368。1 到 4 步合計 11110 條。這裡說的兜底就是兄弟、姐妹、長輩(男性)、長輩(女性)、晚輩(男性)、晚輩(女性)6 個詞,二義則是含斜線的那 6 個串。
圖 1:兜底那條線漲得比詞表快
isOlder 與 reverseRel:長幼在兩處被丟掉
isOlder 寫在第 406 到 412 行,從鏈尾往回掃,命中 older_ 返回 true、younger_ 返回 false,鏈裡壓根沒有兄弟檔位時走第 411 行返回 false。這個預設值直接決定同輩拿到哪個詞:兩步 100 條裡同輩的 8 個詞只剩表弟 4 條與表妹 4 條,而這 8 條全部來自預設 false——有效鏈裡真含 younger_ 的是 0 條,含哥、姐那 4 個詞也是 0 條。三步 1000 條才判得出來:堂弟 5 條、堂哥 5 條、堂妹 1 條、堂姐 1 條,判成弟與妹的 48 條全部來自鏈中真含 younger_ 的那一組,預設組在這 12 條裡佔 0 條;鏈裡完全不含兄弟檔位的有 216 條,全都走第 411 行。
圖 2:換向一次,長幼就沒了
另一處在 reverseRel,表體在第 239 到 249 行:M 口徑下哥哥、姐姐、弟弟、妹妹 4 個檔位全部映射成 younger_brother,F 口徑下全部映射成 younger_sister,長幼在換向時被抹平;而傳 male 時因為它不等於大寫 F,恆走 F 那一側,兩張表 10 個檔位全同。塌鏈是看得見的:[father,older_brother] 與 [father,younger_brother] 換向之後都是 [younger_sister,son]。反向三步枚舉出現的 23 個詞裡,伯伯、嫂子、姐夫、堂哥、表哥、堂姐、表姐出現次數都是 0。
快捷範例 6 條裡 5 條拋錯,鏈區寫出 undefined
這一頁是 親戚關係計算器,快捷範例一行 6 個按鈕,標籤是爺爺的兒子、姑姑的兒子、舅舅的女兒、叔叔的妻子、外婆、姑姑,對應 3 檔位鏈 4 條、2 檔位鏈 2 條。除外婆那條的原文是 'mother,mother' 之外,其餘 5 條的 token 串裡帶 brother 或 sister,而 relDefs 的 10 個鍵裡沒有這兩個。點下去以後 calculate() 在腳本第 105 行讀 def.side 時拋 TypeError: Cannot read properties of undefined (reading 'side')。
拋錯之後兩個出口的狀態並不一致。結果區不被改寫:先前算過,稱謂就停在上一條的舊值——實測先點出姑姑,再餵一條帶 brother 的鏈,螢幕上仍是姑姑。鏈區卻已經被改寫過,因為 addRel 先 push 再渲染,relLabels 取不到 brother,於是 innerHTML 尾部寫成字面量 undefined(9 碼元);kcChain 也照樣把非法 token 原樣留著,實測是 ["father","father","brother"]。拿 6 個陌生 token 複核同一個位置,6/6 拋的是同一個錯。同一句手工換成合法檔位 'father,father,older_brother',出來是爺爺。現場該怎麼把一條鏈拆開數人,本文不複述那篇場景文 叫不出稱呼的那頓飯,本篇只談這張表。
FAQ
Q:拿到兜底詞,是不是我鏈拼錯了?
不一定。兩步 100 條裡 34 條、三步 1000 條裡 384 條、四步 10000 條裡 4032 條走的是兜底,鏈完全合法也照樣落進去——gen 只有 3 個取值,規則只看 state 那 7 個欄位。
Q:這些條數怎麼重現?
把模板的 <script> 區塊裡 relDefs、calculate、lookupTerm、singleTerm、isOlder、reverseRel 原樣切出來,跑在只實作 getElementById、textContent、innerHTML 這幾個能力的 DOM 樁上,再依 10 個檔位逐長度枚舉。插樁版與真實管線在兩步 100 條、三步 1000 條上不一致 0 條,這 36 條站點表才被當成可信的輸入。