json1-2 Exercise 3: Copies, Circles, Key Order and Big Ids ========================================================== Run with Node 24.18.0: node ex3.mjs --- ex3.mjs --- // Exercise 3: copies, circles, key order and big numbers const src = { d: new Date(0), u: undefined, n: NaN, m: new Map([[1, 2]]), nested: { x: 1 } }; console.log('JSON copy :', JSON.parse(JSON.stringify(src))); const sc = structuredClone(src); console.log('structuredClone :', sc); console.log(' Date kept as Date:', sc.d instanceof Date, '| undefined kept:', 'u' in sc, '| NaN kept:', Number.isNaN(sc.n), '| Map kept:', sc.m instanceof Map); try { structuredClone({ f() {} }); } catch (e) { console.log(' function inside :', e.constructor.name + ': ' + e.message); } console.log('\ncircular:'); const a = { name: 'a' }; a.self = a; try { JSON.stringify(a); } catch (e) { console.log(' stringify:', e.message.split('\n').map(s => s.trim()).join(' / ')); } console.log(' structuredClone keeps the circle:', structuredClone(a).self !== a && structuredClone(a).self.self !== undefined); const naiveSeen = () => { const seen = new WeakSet(); return (k, v) => { if (v && typeof v === 'object') { if (seen.has(v)) return '[Circular]'; seen.add(v); } return v; }; }; const shared = { z: 1 }; console.log(' naive replacer on the circle :', JSON.stringify(a, naiveSeen())); console.log(' naive replacer on a SHARED (not circular) object:', JSON.stringify({ a: shared, b: shared }, naiveSeen()), ' <- wrong'); const circularOnly = () => { const path = []; return function (k, v) { while (path.length && path[path.length - 1] !== this) path.pop(); if (v && typeof v === 'object') { if (path.includes(v)) return '[Circular]'; path.push(v); } return v; }; }; console.log(' ancestor-path replacer, circle :', JSON.stringify(a, circularOnly())); console.log(' ancestor-path replacer, shared :', JSON.stringify({ a: shared, b: shared }, circularOnly())); console.log('\nkey order:'); console.log(' ', JSON.stringify({ b: 1, a: 2, 10: 'x', 2: 'y', '-1': 'neg', '01': 'z' })); console.log('\nbig ids:'); const text = '{"id":12345678901234567890,"small":42}'; console.log(' plain parse :', JSON.parse(text).id); console.log(' reviver + source:', JSON.parse(text, (k, v, ctx) => k === 'id' ? BigInt(ctx.source) : v).id); console.log(' rawJSON write :', JSON.stringify({ id: JSON.rawJSON('12345678901234567890') })); try { JSON.rawJSON('{"a":1}'); } catch (e) { console.log(' rawJSON of an object:', e.constructor.name); } console.log('\n__proto__ in parsed JSON:'); const p = JSON.parse('{"__proto__":{"admin":true},"a":1}'); console.log(' own keys:', Object.keys(p).join(','), '| {}.admin:', ({}).admin, '| p.admin:', p.admin); Output: JSON copy : { d: '1970-01-01T00:00:00.000Z', n: null, m: {}, nested: { x: 1 } } structuredClone : { d: 1970-01-01T00:00:00.000Z, u: undefined, n: NaN, m: Map(1) { 1 => 2 }, nested: { x: 1 } } Date kept as Date: true | undefined kept: true | NaN kept: true | Map kept: true function inside : DOMException: f() {} could not be cloned. circular: stringify: Converting circular structure to JSON / --> starting at object with constructor 'Object' / --- property 'self' closes the circle structuredClone keeps the circle: true naive replacer on the circle : {"name":"a","self":"[Circular]"} naive replacer on a SHARED (not circular) object: {"a":{"z":1},"b":"[Circular]"} <- wrong ancestor-path replacer, circle : {"name":"a","self":"[Circular]"} ancestor-path replacer, shared : {"a":{"z":1},"b":{"z":1}} key order: {"2":"y","10":"x","b":1,"a":2,"-1":"neg","01":"z"} big ids: plain parse : 12345678901234567000 reviver + source: 12345678901234567890n rawJSON write : {"id":12345678901234567890} rawJSON of an object: SyntaxError __proto__ in parsed JSON: own keys: __proto__,a | {}.admin: undefined | p.admin: undefined Extra check (t4.mjs) of the ancestor-path replacer on a deeper structure with a shared object and two cycles: const circularOnly = () => { const path = []; return function (k, v) { while (path.length && path[path.length - 1] !== this) path.pop(); if (v && typeof v === 'object') { if (path.includes(v)) return '[Circular]'; path.push(v); } return v; }; }; const s = { z: 1 }; const a = { list: [s, { inner: s }], name: 'a' }; a.list.push({ back: a }); a.list[1].deep = { again: s, up: a.list }; console.log(JSON.stringify(a, circularOnly())); Output: {"list":[{"z":1},{"inner":{"z":1},"deep":{"again":{"z":1},"up":"[Circular]"}},{"back":"[Circular]"}],"name":"a"} WHY THIS WORKS AS AN ANSWER --------------------------- The copy comparison prints what each method kept, instead of assuming: the round-trip lost the Date type, undefined, NaN (became null) and the Map (became {}); structuredClone kept all four and threw on the function, which is the one thing the round-trip silently drops. The circular section tests the helper on the case it must catch (the circle) AND the case it must not touch (a shared object): the naive "seen" replacer fails the second test, and the ancestor-path version passes both plus a deeper structure. That is four small tests, not a proof. Key order shows integer-like keys first and numeric, then the others in insertion order ("-1" and "01" are not array indexes). The big-id lines show the exact digits survive only through context.source and rawJSON, which exist on Node 24.18 (and Chrome 154); on an older runtime, check typeof JSON.rawJSON first. The last block shows a "__proto__" key from JSON becomes an ordinary own key and does not change Object.prototype.