⚖️ JSON Diff 对比

深度对比两个 JSON 数据,输出新增、删除、修改的字段,支持嵌套对象与数组。

等待对比...
已复制
const SAMPLE_LEFT = JSON.stringify({name:"Alice",age:30,tags:["a","b"],address:{city:"SH",zip:"200000"}}, null, 2); const SAMPLE_RIGHT = JSON.stringify({name:"Alice",age:31,tags:["a","c"],address:{city:"SH",zip:"200001"},phone:"123"}, null, 2); function deepDiff(a, b, path){ path = path || ''; const diffs = []; if(a === b) return diffs; const ta = Array.isArray(a) ? 'array' : (a === null ? 'null' : typeof a); const tb = Array.isArray(b) ? 'array' : (b === null ? 'null' : typeof b); if(ta !== tb){ diffs.push({op:'change', path, from:a, to:b}); return diffs; } if(ta === 'object'){ const ak = Object.keys(a), bk = Object.keys(b); for(const k of ak){ if(!(k in b)) diffs.push({op:'remove', path: path ? path+'.'+k : k, value:a[k]}); } for(const k of bk){ if(!(k in a)) diffs.push({op:'add', path: path ? path+'.'+k : k, value:b[k]}); else diffs.push(...deepDiff(a[k], b[k], path ? path+'.'+k : k)); } return diffs; } if(ta === 'array'){ const len = Math.max(a.length, b.length); for(let i = 0; i < len; i++){ if(i >= a.length) diffs.push({op:'add', path: path+'['+i+']', value:b[i]}); else if(i >= b.length) diffs.push({op:'remove', path: path+'['+i+']', value:a[i]}); else diffs.push(...deepDiff(a[i], b[i], path+'['+i+']')); } return diffs; } diffs.push({op:'change', path, from:a, to:b}); return diffs; } function escapeHtml(s){return String(s).replace(/&/g,'&').replace(//g,'>');} function doDiff(){ setErr(''); try{ const a = JSON.parse(document.getElementById('left').value); const b = JSON.parse(document.getElementById('right').value); const diffs = deepDiff(a, b, ''); const mode = document.getElementById('mode').value; if(mode === 'json'){ setOut(JSON.stringify(diffs, null, 2)); } else { if(!diffs.length){ setOut('✅ 两个 JSON 完全相同'); return; } setOut(diffs.map(d => { const icon = d.op === 'add' ? '+ 🟢' : d.op === 'remove' ? '- 🔴' : '~ 🟡'; const path = d.path || '(root)'; if(d.op === 'add') return icon + ' [新增] ' + path + ' = ' + JSON.stringify(d.value); if(d.op === 'remove') return icon + ' [删除] ' + path + ' = ' + JSON.stringify(d.value); return icon + ' [修改] ' + path + ': ' + JSON.stringify(d.from) + ' → ' + JSON.stringify(d.to); }).join('\n')); } setErr('✅ 共 ' + diffs.length + ' 处差异'); }catch(e){ setErr('❌ ' + e.message); } } function loadSample(){document.getElementById('left').value = SAMPLE_LEFT; document.getElementById('right').value = SAMPLE_RIGHT; doDiff();} function setOut(s){document.getElementById('output').textContent = s;} function setErr(s){const e = document.getElementById('err'); e.textContent = s || ''; e.className = 'err' + (s && s.startsWith('✅') ? ' ok' : '');} function clearAll(){document.getElementById('left').value = ''; document.getElementById('right').value = ''; setOut('等待对比...'); setErr('');} function getIndent(){const v=document.getElementById('indent').value;return v==='tab'?'\t':' '.repeat(parseInt(v));} function setOut(s){document.getElementById('output').textContent=s;} function setErr(s){const e=document.getElementById('err');e.textContent=s||'';e.className='err'+(s?'':' ok');} function clearAll(){document.getElementById('input').value='';setOut('等待输入...');setErr('');} function loadSample(){document.getElementById('input').value=SAMPLE;doBeautify();}