Skip to content

JavaScript Objects: Cloning, Immutability, and Safe Patterns 🟠

JavaScript objects are the backbone of state management, configuration, and API payloads. Every developer needs to deeply understand how to clone, protect, and iterate over objects so that prevent subtle bugs, performance issues, or even security risks.

Lets look at some useful Javascript patterns when dealing with objects.

1. Shallow vs Deep Cloning

It is worthy knowing whether to deep copy or shallow copy when dealing with complex objects.

js
// Imagine an API payload representing a user's dashboard state
const dashboard = {
  user: { id: 1, name: "Alice" },
  widgets: [{ id: "w1", type: "chart" }, { id: "w2", type: "table" }],
  lastLogin: new Date(),
  meta: undefined,
};

// Shallow copy - only top-level properties are copied
const shallowCopy = { ...dashboard };
shallowCopy.widgets[0].type = "graph"; 
console.log(dashboard.widgets[0].type); // "graph" shared reference

// Deep copy using structuredClone - preserves nested objects and Dates
const deepCopy = structuredClone(dashboard);
deepCopy.widgets[0].type = "heatmap";
console.log(dashboard.widgets[0].type); // "graph" original unaffected
console.log(deepCopy.lastLogin instanceof Date); // true date preserved

// Deep copy using JSON - loses Dates and undefined
const jsonCopy = JSON.parse(JSON.stringify(dashboard));
console.log(jsonCopy.lastLogin instanceof Date); // false converted to string
console.log("meta" in jsonCopy); // false undefined lost
  • Use shallow copies when top-level props are sufficient.
  • Use structuredClone for deep cloning that preserves Dates, Maps, Sets, and circular references.
  • Avoid JSON cloning if your object contains Dates, functions, or undefined values.

2. Immutability: Freeze & Seal

Controlling object mutability can prevent unintended side effects in large applications.

js

// Freeze - shallow only
const config = Object.freeze({ theme: "dark", options: { showSidebar: true } });
config.theme = "light"; //  blocked
config.options.showSidebar = false; //  nested mutation succeeds silently

// Seal - prevents adding new properties, but values can be updated
const template = Object.seal({ role: "user", access: true });
template.access = false; //  allowed
template.newProp = 123; //  cannot add new properties
  • Freeze protects top-level structure.
  • Seal is useful for SDKs or API templates where some fields can be updated but the shape must remain fixed.
  • Remember that nested objects remain mutable — consider recursive freeze or immutable libraries for deep immutability.

3. Property Descriptors & Accessors

Property descriptors give you control over how a property behaves: writable, enumerable, configurable.

js
const sdk = {};
Object.defineProperty(sdk, "apiKey", {
  value: "SECRET",
  writable: false,
  enumerable: true,
  configurable: false,
});

console.log(sdk.apiKey); // "SECRET" 
sdk.apiKey = "HACKED"; //  blocked

// Accessors for computed values
let _balance = 1000;
const account = {
  get balance() { return `$${_balance}`; },
  set balance(val) {
    if (val < 0) throw new Error("Invalid balance");
    _balance = val;
  }
};
account.balance = 1500;
console.log(account.balance); // $1500
  • Protect sensitive data and define read-only or computed fields.
  • Use getters/setters to encapsulate logic for properties that depend on other state.

4. Iteration & Prototype Safety

Iteration over objects must be done safely, especially in multi-tenant or user-driven systems.

js
const userConfig = Object.create({ admin: true });
userConfig.theme = "dark";

// Unsafe iteration with for...in
for (let key in userConfig) {
  console.log(key); // logs: theme, admin inherited property
}

// Safe iteration
Object.keys(userConfig).forEach(key => console.log(key)); // logs: theme own property only
Object.getOwnPropertyNames(userConfig).forEach(key => console.log(key)); // logs: theme
  • Avoid for...in on user objects to prevent prototype pollution issues.
  • Use Object.keys, Object.entries, or Object.getOwnPropertyNames depending on whether you want enumerable or all own properties.

5. ES6+ Enhancements

Modern syntax makes object handling cleaner, but with some gaps.

js

// Shallow merge using spread
const defaults = { retries: 3, timeout: 5000 };
const userOpts = { timeout: 2000 };
const finalOpts = { ...defaults, ...userOpts }; // { retries: 3, timeout: 2000 } 

// Iterate with entries
for (const [k, v] of Object.entries(finalOpts)) {
  console.log(`${k}: ${v}`); // 
}

// Caveat: spread is shallow
const nested = { a: { b: 1 } };
const copyNested = { ...nested };
copyNested.a.b = 42;
console.log(nested.a.b); // 42  shared reference

javascript book

If this interested you, check out my Javascript Book

Enjoy every byte.