Advanced Object Manipulation in JavaScript
Advanced object manipulation enables fine-grained control over object behavior. This article covers property descriptors, freezing, sealing, cloning, and transformation patterns.
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 }; -
Use descriptors for controlled properties:
// โ Good Object.defineProperty(obj, 'id', { value: 123, writable: false }); // โ Bad obj.id = 123; -
Deep clone when needed:
// โ Good const cloned = deepClone(original); // โ Bad const cloned = { ...original };
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 -
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