Advanced object manipulation enables fine-grained control over object behavior. This article covers property descriptors, freezing, sealing, cloning, and transformation patterns. See Javascript Guide for more context. See Javascript Guide for more context.
Introduction
Advanced object manipulation allows:
- Fine-grained property control
- Object immutability
- Deep cloning
- Property transformation
- Object composition
Understanding these techniques helps you:
- Create immutable data structures
- Prevent accidental modifications
- Implement complex transformations
- Build robust APIs
- Optimize performance
Property Descriptors
Understanding Descriptors
// ✅ Good: Inspect property descriptors
const obj = { name: 'John' };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
// {
// value: 'John',
// writable: true,
// enumerable: true,
// configurable: true
// }
// Descriptor properties:
// - value: the property value
// - writable: can the value be changed?
// - enumerable: shows up in for...in loops?
// - configurable: can the descriptor be changed?
Creating Properties with Descriptors
// ✅ Good: Define properties with specific descriptors
const obj = {};
Object.defineProperty(obj, 'id', {
value: 123,
writable: false,
enumerable: true,
configurable: false
});
console.log(obj.id); // 123
obj.id = 456; // Silently fails (or throws in strict mode)
console.log(obj.id); // 123 (unchanged)
// ✅ Good: Define multiple properties
Object.defineProperties(obj, {
name: {
value: 'John',
writable: true,
enumerable: true,
configurable: true
},
email: {
value: '[email protected]',
writable: false,
enumerable: true,
configurable: false
}
});
console.log(obj.name); // 'John'
console.log(obj.email); // '[email protected]'
Accessor Descriptors
// ✅ Good: Use accessor descriptors for computed properties
const obj = {};
Object.defineProperty(obj, 'fullName', {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
[this.firstName, this.lastName] = value.split(' ');
},
enumerable: true,
configurable: true
});
obj.firstName = 'John';
obj.lastName = 'Doe';
console.log(obj.fullName); // 'John Doe'
obj.fullName = 'Jane Smith';
console.log(obj.firstName); // 'Jane'
console.log(obj.lastName); // 'Smith'
Object Freezing and Sealing
Object.freeze()
// ✅ Good: Freeze objects to prevent modifications
const user = {
name: 'John',
age: 30
};
Object.freeze(user);
user.name = 'Jane'; // Silently fails
user.email = '[email protected]'; // Silently fails
delete user.age; // Silently fails
console.log(user); // { name: 'John', age: 30 }
// Check if frozen
console.log(Object.isFrozen(user)); // true
Object.seal()
// ✅ Good: Seal objects to prevent property addition/deletion
const config = {
host: 'localhost',
port: 3000
};
Object.seal(config);
config.host = '127.0.0.1'; // OK - can modify
config.timeout = 5000; // Fails - can't add new properties
delete config.port; // Fails - can't delete properties
console.log(config); // { host: '127.0.0.1', port: 3000 }
// Check if sealed
console.log(Object.isSealed(config)); // true
Object.preventExtensions()
// ✅ Good: Prevent adding new properties
const obj = { a: 1 };
Object.preventExtensions(obj);
obj.a = 2; // OK - can modify
obj.b = 3; // Fails - can't add new properties
console.log(obj); // { a: 2 }
// Check if extensible
console.log(Object.isExtensible(obj)); // false
Deep Freezing
// ✅ Good: Recursively freeze nested objects
function deepFreeze(obj) {
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop] !== null &&
(typeof obj[prop] === 'object' || typeof obj[prop] === 'function') &&
!Object.isFrozen(obj[prop])) {
deepFreeze(obj[prop]);
}
});
return obj;
}
const config = {
database: {
host: 'localhost',
port: 3000
},
cache: {
ttl: 3600
}
};
deepFreeze(config);
config.database.host = 'remote'; // Fails
config.cache.ttl = 7200; // Fails
console.log(config);
// { database: { host: 'localhost', port: 3000 }, cache: { ttl: 3600 } }
Object Cloning
Shallow Cloning
// ✅ Good: Shallow clone using Object.assign
const original = {
name: 'John',
hobbies: ['reading', 'coding']
};
const clone1 = Object.assign({}, original);
const clone2 = { ...original };
console.log(clone1 === original); // false
console.log(clone1.hobbies === original.hobbies); // true (same reference)
// Modifying nested objects affects both
clone1.hobbies.push('gaming');
console.log(original.hobbies); // ['reading', 'coding', 'gaming']
Deep Cloning
// ✅ Good: Deep clone with recursion
function deepClone(obj, seen = new WeakSet()) {
// Handle primitives
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Handle circular references
if (seen.has(obj)) {
return obj;
}
seen.add(obj);
// Handle arrays
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item, seen));
}
// Handle dates
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// Handle maps
if (obj instanceof Map) {
const cloned = new Map();
obj.forEach((value, key) => {
cloned.set(deepClone(key, seen), deepClone(value, seen));
});
return cloned;
}
// Handle sets
if (obj instanceof Set) {
const cloned = new Set();
obj.forEach(value => {
cloned.add(deepClone(value, seen));
});
return cloned;
}
// Handle objects
const cloned = {};
for (const key of Object.keys(obj)) {
cloned[key] = deepClone(obj[key], seen);
}
return cloned;
}
// Usage
const original = {
name: 'John',
hobbies: ['reading', 'coding'],
metadata: {
created: new Date(),
tags: new Set(['js', 'web'])
}
};
const cloned = deepClone(original);
console.log(cloned === original); // false
console.log(cloned.hobbies === original.hobbies); // false
console.log(cloned.metadata === original.metadata); // false
Structured Clone
// ✅ Good: Use structuredClone for modern browsers
const original = {
name: 'John',
hobbies: ['reading', 'coding'],
date: new Date()
};
const cloned = structuredClone(original);
console.log(cloned === original); // false
console.log(cloned.hobbies === original.hobbies); // false
console.log(cloned.date === original.date); // false
Object Transformation
Mapping Objects
// ✅ Good: Transform object properties
function mapObject(obj, fn) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = fn(value, key);
}
return result;
}
const numbers = { a: 1, b: 2, c: 3 };
const doubled = mapObject(numbers, (value) => value * 2);
console.log(doubled); // { a: 2, b: 4, c: 6 }
// ✅ Good: Transform keys and values
function transformObject(obj, keyFn, valueFn) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
result[keyFn(key)] = valueFn(value);
}
return result;
}
const transformed = transformObject(
{ firstName: 'John', lastName: 'Doe' },
(key) => key.toUpperCase(),
(value) => value.toLowerCase()
);
console.log(transformed);
// { FIRSTNAME: 'john', LASTNAME: 'doe' }
Filtering Objects
// ✅ Good: Filter object properties
function filterObject(obj, predicate) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (predicate(value, key)) {
result[key] = value;
}
}
return result;
}
const user = {
name: 'John',
age: 30,
email: '[email protected]',
password: 'secret'
};
const publicData = filterObject(user, (value, key) => key !== 'password');
console.log(publicData);
// { name: 'John', age: 30, email: '[email protected]' }
Merging Objects
// ✅ Good: Deep merge objects
function deepMerge(target, source) {
const result = { ...target };
for (const [key, value] of Object.entries(source)) {
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
result[key] = deepMerge(result[key] || {}, value);
} else {
result[key] = value;
}
}
return result;
}
const defaults = {
database: {
host: 'localhost',
port: 3000,
timeout: 5000
},
cache: {
enabled: true
}
};
const config = {
database: {
host: 'remote.server.com',
port: 5432
}
};
const merged = deepMerge(defaults, config);
console.log(merged);
// {
// database: {
// host: 'remote.server.com',
// port: 5432,
// timeout: 5000
// },
// cache: { enabled: true }
// }
Advanced Patterns
Immutable Updates
// ✅ Good: Create immutable updates
function updateImmutable(obj, path, value) {
const keys = path.split('.');
const lastKey = keys.pop();
let current = { ...obj };
let target = current;
for (const key of keys) {
target[key] = { ...target[key] };
target = target[key];
}
target[lastKey] = value;
return current;
}
const user = {
name: 'John',
address: {
city: 'New York',
zip: '10001'
}
};
const updated = updateImmutable(user, 'address.city', 'Boston');
console.log(updated);
// { name: 'John', address: { city: 'Boston', zip: '10001' } }
console.log(user.address.city); // 'New York' (original unchanged)
Proxy-based Immutability
// ✅ Good: Enforce immutability with Proxy
function makeImmutable(obj) {
return new Proxy(obj, {
set(target, property, value) {
throw new Error(`Cannot set property ${String(property)}`);
},
deleteProperty(target, property) {
throw new Error(`Cannot delete property ${String(property)}`);
}
});
}
const config = makeImmutable({
host: 'localhost',
port: 3000
});
console.log(config.host); // 'localhost'
config.host = 'remote'; // Error: Cannot set property host
Object Composition
// ✅ Good: Compose objects from multiple sources
function compose(...objects) {
return new Proxy(Object.assign({}, ...objects), {
get(target, property) {
const value = target[property];
return typeof value === 'function' ? value.bind(target) : value;
}
});
}
const canEat = {
eat() {
return `${this.name} is eating`;
}
};
const canWalk = {
walk() {
return `${this.name} is walking`;
}
};
const person = compose(
{ name: 'John' },
canEat,
canWalk
);
console.log(person.eat()); // 'John is eating'
console.log(person.walk()); // 'John is walking'
Practical Examples
Configuration Manager
// ✅ Good: Immutable configuration manager
class ConfigManager {
constructor(defaults = {}) {
this.config = Object.freeze(deepClone(defaults));
}
get(path) {
const keys = path.split('.');
let value = this.config;
for (const key of keys) {
value = value?.[key];
}
return value;
}
merge(updates) {
const merged = deepMerge(this.config, updates);
return new ConfigManager(merged);
}
toJSON() {
return deepClone(this.config);
}
}
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) return obj.map(deepClone);
const cloned = {};
for (const key in obj) {
cloned[key] = deepClone(obj[key]);
}
return cloned;
}
function deepMerge(target, source) {
const result = { ...target };
for (const [key, value] of Object.entries(source)) {
if (value !== null && typeof value === 'object') {
result[key] = deepMerge(result[key] || {}, value);
} else {
result[key] = value;
}
}
return result;
}
// Usage
const config = new ConfigManager({
database: { host: 'localhost', port: 3000 },
cache: { enabled: true }
});
console.log(config.get('database.host')); // 'localhost'
const newConfig = config.merge({
database: { host: 'remote.server.com' }
});
console.log(newConfig.get('database.host')); // 'remote.server.com'
console.log(config.get('database.host')); // 'localhost' (original unchanged)
Data Validator
// ✅ Good: Validate and transform objects
class DataValidator {
constructor(schema) {
this.schema = schema;
}
validate(data) {
const errors = [];
for (const [key, rules] of Object.entries(this.schema)) {
if (!(key in data)) {
errors.push(`Missing required field: ${key}`);
continue;
}
const value = data[key];
if (rules.type && typeof value !== rules.type) {
errors.push(
`Field ${key}: expected ${rules.type}, got ${typeof value}`
);
}
if (rules.validate && !rules.validate(value)) {
errors.push(`Field ${key}: ${rules.message}`);
}
}
return {
valid: errors.length === 0,
errors,
data: errors.length === 0 ? data : null
};
}
}
// Usage
const userSchema = {
name: {
type: 'string',
validate: (v) => v.length > 0,
message: 'Name cannot be empty'
},
email: {
type: 'string',
validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
message: 'Invalid email format'
},
age: {
type: 'number',
validate: (v) => v >= 0 && v <= 150,
message: 'Age must be between 0 and 150'
}
};
const validator = new DataValidator(userSchema);
const result = validator.validate({
name: 'John',
email: '[email protected]',
age: 30
});
console.log(result);
// { valid: true, errors: [], data: { ... } }
Best Practices
- Use Object.freeze for constants:
// ✅ Good const CONSTANTS = Object.freeze({ MAX_SIZE: 100, MIN_SIZE: 10 }); // ❌ Bad const CONSTANTS = { MAX_SIZE: 100, MIN_SIZE: 10 }; ```javascript - Use descriptors for controlled properties:
// ✅ Good Object.defineProperty(obj, 'id', { value: 123, writable: false }); // ❌ Bad obj.id = 123; ```javascript - Deep clone when needed:
// ✅ Good const cloned = deepClone(original); // ❌ Bad const cloned = { ...original }; ```javascript
Common Mistakes
- Shallow freeze doesn’t freeze nested objects:
// ❌ Bad const obj = { nested: { value: 1 } }; Object.freeze(obj); obj.nested.value = 2; // Works! // ✅ Good deepFreeze(obj); obj.nested.value = 2; // Fails ```javascript - Modifying cloned objects affects original:
// ❌ Bad const clone = { ...original }; clone.nested.value = 2; // Affects original // ✅ Good const clone = deepClone(original); clone.nested.value = 2; // Doesn't affect original
Summary
Advanced object manipulation provides fine-grained control. Key takeaways:
- Property descriptors control behavior
- Freezing prevents modifications
- Deep cloning creates independent copies
- Transformations enable data manipulation
- Immutability improves reliability
- Composition enables flexibility
- Validation ensures data integrity
Related Resources
- Object.defineProperty - MDN
- Object.freeze - MDN
- structuredClone - MDN
- Proxy - MDN
- Immutable Data Patterns
Next Steps
- Learn about Design Patterns & Architecture
- Explore Metaprogramming Patterns
- Study Proxy Objects
- Practice immutable updates
- Build configuration managers
Comments