Lập trình · 19/10/2026

JavaScript Object.freeze: Why Data Inside Frozen Objects Can Still Change

Object.isFrozen(config) returns true, yet data inside config still changes. This need not be a runtime defect. Object.freeze restricts the supplied object's own properties; it does not traverse every related object and freeze all state.

Object.freeze trong JavaScript: vì sao object đã khóa vẫn có dữ liệu thay đổi?

Object.isFrozen(config) returns true, yet data inside config still changes. This need not be a runtime defect. Object.freeze restricts the supplied object's own properties; it does not traverse every related object and freeze all state.

Understanding the boundary helps catch accidental mutations. Before introducing a generic deep-freeze helper, identify the schema and who retains references.

1. Freeze affects the original object

'use strict';
const config = { retries: 3 };
const locked = Object.freeze(config);

console.log(locked === config);       // true
console.log(Object.isFrozen(config)); // true
try {
  config.retries = 5;
} catch (error) {
  console.log(error.name); // TypeError
}
console.log(config.retries); // 3

No copy is created. Existing references observe the same frozen object. Ordinary data properties cannot be reassigned, and properties cannot be added or removed. Strict mode makes the invalid assignment throw rather than potentially failing silently.

const prevents rebinding a variable; it does not freeze the referenced object. These are distinct controls.

2. Nested objects retain their own lifecycle

'use strict';
const config = Object.freeze({
  network: { timeoutMs: 1000 }
});
config.network.timeoutMs = 2000;

console.log(Object.isFrozen(config));         // true
console.log(Object.isFrozen(config.network)); // false
console.log(config.network.timeoutMs);       // 2000

The network reference cannot be replaced, but its target is not frozen. Object.isFrozen checks its argument, not the complete reachable graph.

For a small known schema, construct fresh data and freeze the necessary layers explicitly. This also avoids unexpectedly freezing an object owned by a caller.

3. Build configuration with a specific schema

function createConfig(timeoutMs) {
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
    throw new RangeError('Expected a positive safe integer');
  }
  return Object.freeze({
    network: Object.freeze({ timeoutMs }),
    modes: Object.freeze(['read', 'write'])
  });
}

const config = createConfig(1500);
console.log(Object.isFrozen(config.network)); // true
console.log(Object.isFrozen(config.modes));   // true
console.log(config.network.timeoutMs);       // 1500

This schema contains known objects, an array of strings and primitive values. It is not a general deep-freezing algorithm. Large or cyclic graphs need explicit traversal, special-type handling and cost analysis before recursive freezing.

Freezing does not validate input. The factory checks the timeout separately; application-specific upper bounds remain another requirement.

4. Freezing an array does not freeze its elements

'use strict';
const rows = Object.freeze([{ name: 'Draft' }]);
rows[0].name = 'Ready';
try {
  rows.push({ name: 'Another' });
} catch (error) {
  console.log(error.name); // TypeError
}
console.log(rows.length);  // 1
console.log(rows[0].name); // Ready

The array's membership stays unchanged, but its nested object changes. Test both array operations and element-object mutations rather than checking push alone.

5. Map entries are a separate kind of state

const counters = Object.freeze(new Map([['views', 1]]));
counters.set('views', 2);

console.log(Object.isFrozen(counters)); // true
console.log(counters.get('views'));     // 2

Freezing a Map object does not block entry updates through set. Do not promise read-only data while exposing a mutable Map. Consider a controlled read interface or a data representation matching the contract.

Accessors also require care: freezing does not remove setters or guarantee that getters always return the same value. Check the behavior of complex types directly.

6. Update explicitly and test the boundary

  • Choose objects that may be frozen without surprising shared-data owners.
  • Test invalid assignments in strict mode.
  • Check nested objects, array elements and collections separately.
  • Create and validate a new configuration when changes are required.
  • Measure costs before freezing large graphs in frequent operations.
  • Retain server validation and authorization; client freezing is not a security boundary.

The snippets ran locally with Node.js. Framework integration and application performance were not tested. References: Object.freeze and Object.isFrozen.

Effective freezing has an explicit scope: which properties, which layers and which data types. That clarity makes it useful for catching accidental mutation.

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.