Skip to main content
โšก Calmops

Advanced Object Manipulation in JavaScript

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

  1. 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
    };
    
  2. Use descriptors for controlled properties:

    // โœ… Good
    Object.defineProperty(obj, 'id', {
      value: 123,
      writable: false
    });
    
    // โŒ Bad
    obj.id = 123;
    
  3. Deep clone when needed:

    // โœ… Good
    const cloned = deepClone(original);
    
    // โŒ Bad
    const cloned = { ...original };
    

Common Mistakes

  1. 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
    
  2. 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

Next Steps

Comments