---
title: 'Structured Cloning'
source: 'https://academia.sh/en/courses/javascript-object-model/structured-cloning'
course: 'Objects and Functions in JavaScript'
language: en
updated: '2026-08-23T07:00:59+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
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.

```js
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.

```js
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.

```js
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.

```js
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, `undefined` fields, non-enumerable and
  symbol-keyed properties; it corrupts `Set`, `Map`, and `Date` types, and throws an
  error on cycles.
- `structuredClone` preserves 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.
