Lesson 17 / 17
Structured Cloning
The references shallow copying shares, the losses of copying through serialization, the scope of structured cloning, and a deep copy that preserves the prototype and descriptors.
Contents
The previous lesson showed the copy–view distinction at the byte level. The same question
has been open for objects since the start of the course: object spread lost the chain,
Object.assign turned accessors into plain values, JSON.stringify dropped methods and
Map content.
This lesson answers that question in full. Three copying paths — spread, a round trip through serialization, and structured cloning — are examined separately; what each one carries and what it drops is shown. Then a deep copy preserving both is written.
The Limit of Shallow Copy
Object spread and Object.assign write the source’s own enumerable properties to the
target. If a value is an object, what gets written is that object’s reference; the
object itself is not copied.
const source = { sensor: "S-01", value: 21.4, sourceInfo: { file: "measurement-2.csv", line: 17 }, tags: ["raw", "temperature"], }; const spread = { ...source }; const assigned = Object.assign({}, source); console.log(spread.sensor); console.log(spread.sourceInfo === source.sourceInfo); console.log(assigned.tags === source.tags); spread.sensor = "S-99"; spread.sourceInfo.line = 99; spread.tags.push("changed"); console.log(source.sensor); console.log(source.sourceInfo.line); console.log(source.tags.join(",")); console.log(JSON.stringify(source.sourceInfo) === JSON.stringify(spread.sourceInfo));
S-01 true true S-01 99 raw,temperature,changed true
A top-level field is independent: when sensor changes in the copy, the source is
unaffected. Inner objects, though, are shared; every change made through the copy is also
visible in the source. This is the shallow-copy–deep-copy distinction introduced in the
Value and Reference and Passing lesson of the Programming Fundamentals course.
The reason freezing stayed shallow in the Immutability Techniques lesson is the same: both operations are limited to the object’s own properties and do not descend into the objects they point to.
Copying Through Serialization
Converting an object to text and reading it back recreates every inner object anew. The losses are large, and these losses are silent.
const source = { sensor: "S-01", value: 21.4, offset: undefined, measuredAt: new Date(0), tagSet: new Set(["raw", "temperature"]), calibration: new Map([["S-01", -0.4]]), format() { return this.sensor; }, }; const jsonCopy = JSON.parse(JSON.stringify(source)); console.log(Object.keys(jsonCopy).join(",")); console.log(typeof jsonCopy.format); console.log("offset" in jsonCopy); console.log(typeof jsonCopy.measuredAt); console.log(jsonCopy.measuredAt); console.log(JSON.stringify(jsonCopy.tagSet)); console.log(JSON.stringify(jsonCopy.calibration)); const cyclic = { sensor: "S-02" }; cyclic.self = cyclic; try { JSON.stringify(cyclic); } catch (error) { console.log(`cycle: ${error.constructor.name}`); }
sensor,value,measuredAt,tagSet,calibration
undefined
false
string
1970-01-01T00:00:00.000Z
{}
{}
cycle: TypeError
Five losses can be listed. Functions and fields with an undefined value drop entirely.
A date object turns into a string, and reading it back gives text, not a date. Set and
Map content collapses to an empty object — the behavior seen in the first lesson of the
Advanced Collections topic. Non-enumerable properties and symbol keys are also skipped.
Finally, circular references throw an error.
The prototype chain is lost too: the object read back is always an ordinary object. This path is suitable only for plain, nested objects that carry data alone — which is, after all, JSON format’s own scope.
Structured Cloning
The structuredClone function offered by the runtime deep-copies a far wider set of
values than serialization does and preserves cycles.
class MeasurementRecord { constructor(sensor, value) { this.sensor = sensor; this.value = value; this.tagSet = new Set(["raw"]); this.measuredAt = new Date(0); } format() { return `${this.sensor}: ${this.value}`; } } const record = new MeasurementRecord("S-01", 21.4); record.self = record; const copy = structuredClone(record); console.log(copy.sensor); console.log(copy.self === copy); console.log(copy.tagSet instanceof Set); console.log(copy.tagSet.has("raw")); console.log(copy.measuredAt instanceof Date); console.log(copy.measuredAt.getTime()); console.log(copy.tagSet === record.tagSet); console.log(Object.getPrototypeOf(copy) === MeasurementRecord.prototype); console.log(copy instanceof MeasurementRecord); console.log(typeof copy.format); try { structuredClone({ fn: () => 1 }); } catch (error) { console.log(`function: ${error.name}`); } const typed = new Float64Array([21.4, 19.8]); const typedCopy = structuredClone(typed); console.log(typedCopy instanceof Float64Array); console.log(typedCopy.buffer === typed.buffer);
S-01 true true true true 0 false false false undefined function: DataCloneError true false
The gains are clear: circular references are preserved — the copy’s field pointing back
to itself points to the copy, not the source. Set, Map, Date, and typed arrays are
copied along with their types, and inner objects are not shared.
There is only one loss, but it is decisive for this course’s axis: the prototype chain is not carried. The eighth, ninth, and tenth lines show this in three separate ways — the copy’s prototype is not the class’s prototype, the type query is negative, and the method is not found. When a class instance is cloned, only the data remains.
Functions cannot be cloned either and throw an error. These two limits come from the same reasoning: structured cloning is defined to transfer values into another execution context; code and the chain are things that cannot be transferred.
A Copy That Preserves the Chain and Descriptors
If both depth and the chain are needed, the copy has to be written by hand. The pattern is the sibling of deep freezing in the Immutability Techniques lesson: a recursive walk, a visit table for cycles, and rebuilding the prototype at every node.
function deepCopy(value, seen = new WeakMap()) { if (value === null || typeof value !== "object") return value; if (seen.has(value)) return seen.get(value); if (value instanceof Date) return new Date(value.getTime()); if (value instanceof Set) { const set = new Set(); seen.set(value, set); for (const item of value) set.add(deepCopy(item, seen)); return set; } if (value instanceof Map) { const map = new Map(); seen.set(value, map); for (const [key, content] of value) { map.set(deepCopy(key, seen), deepCopy(content, seen)); } return map; } const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(value, copy); for (const name of Reflect.ownKeys(value)) { const descriptor = Object.getOwnPropertyDescriptor(value, name); if ("value" in descriptor) descriptor.value = deepCopy(descriptor.value, seen); Object.defineProperty(copy, name, descriptor); } return copy; } class MeasurementRecord { constructor(sensor, value) { this.sensor = sensor; this.value = value; this.sourceInfo = { file: "measurement-2.csv", line: 17 }; } format() { return `${this.sensor}: ${this.value}`; } } const record = new MeasurementRecord("S-01", 21.4); record.self = record; Object.defineProperty(record, "hiddenCounter", { value: 3, enumerable: false, writable: true, configurable: true, }); const copy = deepCopy(record); console.log(copy.format()); console.log(copy instanceof MeasurementRecord); console.log(copy.self === copy); console.log(copy.sourceInfo === record.sourceInfo); console.log(copy.sourceInfo.line); console.log(copy.hiddenCounter); console.log(Object.keys(copy).join(",")); console.log(Object.getOwnPropertyDescriptor(copy, "hiddenCounter").enumerable); copy.sourceInfo.line = 99; console.log(record.sourceInfo.line);
S-01: 21.4 true true false 17 3 sensor,value,sourceInfo,self false 17
The copy is a genuine measurement record: its method works, the type query is positive,
its cycle points back to itself, its inner object is independent, and its non-enumerable
field is preserved along with its flag. Choosing Reflect.ownKeys ensures symbol keys are
covered too; copying the descriptor keeps accessors from collapsing into plain values.
This pattern has its limits too. Private fields cannot be copied — because they cannot be read from outside, a library function has no access to them. State held in a closure is not carried for the same reason. The general rule is: what can be copied is what can be seen from outside.
Three Setups Under Copying
Throughout the course, the measurement record was built in three forms. Copying is the test that lays the difference between them bare most sharply.
function plainRecord(sensor, value) { return { sensor, value, format() { return `${this.sensor}: ${this.value}`; }, }; } function closureRecord(sensor, value) { return { format() { return `${sensor}: ${value}`; }, }; } class ClassRecord { constructor(sensor, value) { this.sensor = sensor; this.value = value; } format() { return `${this.sensor}: ${this.value}`; } } function check(name, obj) { const spread = { ...obj }; let cloneResult; try { const clone = structuredClone(obj); cloneResult = typeof clone.format === "function" ? clone.format() : "no behavior"; } catch (error) { cloneResult = `error:${error.name}`; } const spreadResult = typeof spread.format === "function" ? spread.format() : "no behavior"; console.log(`${name}: json=${JSON.stringify(obj)}, spread=${spreadResult}, clone=${cloneResult}`); } check("plain ", plainRecord("S-01", 21.4)); check("closure ", closureRecord("S-01", 21.4)); check("class ", new ClassRecord("S-01", 21.4)); const classInstance = new ClassRecord("S-01", 21.4); const chainedCopy = Object.defineProperties( Object.create(Object.getPrototypeOf(classInstance)), Object.getOwnPropertyDescriptors(classInstance), ); console.log(chainedCopy.format()); console.log(chainedCopy instanceof ClassRecord);
plain : json={"sensor":"S-01","value":21.4}, spread=S-01: 21.4, clone=error:DataCloneError
closure : json={}, spread=S-01: 21.4, clone=error:DataCloneError
class : json={"sensor":"S-01","value":21.4}, spread=no behavior, clone=no behavior
S-01: 21.4
true
The three setups show three separate behaviors. In the plain object, because data and behavior stand in the same object, serialization takes the data, spread also carries the behavior, and structured cloning throws an error because of the function. In the closure-based setup, because the data is never a property at all, serialization comes out empty; spread copies only the function, and because the function carries its own closure, the result is still correct.
In the class setup, data and behavior are in separate places: data in the instance, behavior in the chain. Because both copying paths take only the instance, the behavior drops — but no error is thrown either. If the chain is needed, it has to be set up explicitly, as in the last two lines.
The sentence from the course’s first lesson closes here: an object’s data is in itself, its identity is in its chain. Copying operations carry own properties; the chain is carried only when it is set up explicitly.
Summary
- Shallow copy separates only top-level fields; inner objects are shared with the source.
- Copying through serialization drops functions,
undefinedfields, non-enumerable and symbol-keyed properties; it corruptsSet,Map, andDatetypes, and throws an error on cycles. structuredClonepreserves cycles and copies many built-in types along with their type, but does not carry the prototype chain and throws an error on functions.- A copy preserving the chain and descriptors is written by hand: the prototype is rebuilt at every node, descriptors are carried exactly, cycles are tracked with a visit table.
- Private fields and state in a closure cannot be copied; what can be copied is what can be seen from outside.
Course Wrap-Up
The course started with a single question: how is a property that was never written at
all found? The answer was the prototype link every object carries, and this link was
measured across seventeen lessons. Property lookup started from the object itself and
proceeded up the chain; writing never climbed upward, it always shadowed. The job the
new operator does was rewritten in three lines. Which prototype operation each line of
a class declaration corresponds to was shown with the same tests — a class is the
syntactic face of the chain; there is no different object model underneath it.
The second topic took up the question the chain does not answer. A prototype determines
where a function will be found; this determines which data it will work on. Because the
two are independent, a method can be detached from its object, lent to another object, or
permanently bound. Arrow functions fall outside these four rules and take their context
from the scope they are defined in. Closures, meanwhile, offered a second construction
path that hides state without touching the chain at all.
The third topic examined collections by the same criteria: Map preserves key identity,
weak collections do not extend objects’ lifetime, the iteration protocol is a single
method found on the chain, generators produce that method themselves, typed arrays have
a separate chain. The last lesson ran all of them through the copying test.
The measurement record’s three setups — plain object, factory with closure, class — were held side by side throughout the course and compared at every new concept: sharing, visibility, binding, privacy, serialization, and copying. There is no single correct choice among them; the criteria are explicit, and the comparisons can be measured with the outputs in these lessons.
One thing is left out. Every example in this course resolved instantly: every
console.log call ran only after the line before it had finished. This assumption
breaks down once measurements come from a file or a network — the result is not ready the
moment it is requested. The Asynchronous JavaScript and the Runtime course takes up
this situation: the single-threaded execution model, the event loop’s task and microtask
queues, promise-based flows, and the memory life cycle. The chain and context rules you
learned in this course apply there too; the only thing that changes is when the code
runs.
To keep your progress and take notes, Log in
My notes
Log in to take notes.