Skip to content

Object Property Operations: Fine-grained Control Over Your Data

In an apartment building, every room door has different permission settings. Some doors allow free entry and exit, others only allow entry but not exit, and some are completely locked. The landlord might have set certain rooms as non-renovatable, certain facilities as non-removable. JavaScript object properties have a similar permission system—you can control whether properties can be modified, deleted, traversed, and even execute custom logic when accessing properties. These fine-grained control capabilities allow us to build safer, smarter data structures.

Property Descriptors

Every object property has a set of hidden attributes that determine the property's behavior. We can view and set these attributes through property descriptors.

Getting Property Descriptors

Use Object.getOwnPropertyDescriptor() to get the descriptor of a single property:

javascript
let user = {
  name: "Alice",
  age: 25,
};

let descriptor = Object.getOwnPropertyDescriptor(user, "name");
console.log(descriptor);
// {
//   value: "Alice",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

Get descriptors for all properties:

javascript
let allDescriptors = Object.getOwnPropertyDescriptors(user);
console.log(allDescriptors);
// {
//   name: {
//     value: "Alice",
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   age: {
//     value: 25,
//     writable: true,
//     enumerable: true,
//     configurable: true
//   }
// }

Property Attribute Details

Each data property has four attributes:

  1. value - The property's value
  2. writable - Whether the value can be modified
  3. enumerable - Whether the property can appear in enumerations
  4. configurable - Whether the property can be deleted or have its attributes modified
javascript
let product = {};

Object.defineProperty(product, "name", {
  value: "Laptop",
  writable: false, // Cannot modify
  enumerable: true, // Can enumerate
  configurable: false, // Cannot delete or reconfigure
});

console.log(product.name); // "Laptop"

// Attempt to modify - fails silently (throws error in strict mode)
product.name = "Phone";
console.log(product.name); // "Laptop" - unchanged

// Attempt to delete - fails
delete product.name;
console.log(product.name); // "Laptop" - still exists

defineProperty() - Define Single Properties

Object.defineProperty() allows us to precisely define property attributes.

Basic Usage

javascript
let account = {};

Object.defineProperty(account, "balance", {
  value: 1000,
  writable: true,
  enumerable: true,
  configurable: true,
});

console.log(account.balance); // 1000
account.balance = 1500;
console.log(account.balance); // 1500

Creating Read-only Properties

javascript
let config = {};

Object.defineProperty(config, "API_KEY", {
  value: "abc123xyz",
  writable: false, // Read-only
  enumerable: true,
  configurable: false,
});

console.log(config.API_KEY); // "abc123xyz"

// Cannot modify
config.API_KEY = "newkey";
console.log(config.API_KEY); // "abc123xyz" - unchanged

// Cannot delete
delete config.API_KEY;
console.log(config.API_KEY); // "abc123xyz" - still exists

Creating Non-enumerable Properties

Non-enumerable properties won't appear in for...in loops, Object.keys(), or Object.values():

javascript
let person = {
  name: "Bob",
  age: 30,
};

// Add a non-enumerable property
Object.defineProperty(person, "_id", {
  value: 12345,
  enumerable: false, // Non-enumerable
  writable: true,
  configurable: true,
});

console.log(person._id); // 12345 - accessible

// But not visible in traversals
console.log(Object.keys(person)); // ["name", "age"]
for (let key in person) {
  console.log(key); // Only prints "name" and "age"
}

// Use Object.getOwnPropertyNames() to see it
console.log(Object.getOwnPropertyNames(person));
// ["name", "age", "_id"]

This is useful for hiding internal implementation details.

Real-world Application: Metadata Storage

javascript
function addMetadata(obj, data) {
  Object.defineProperty(obj, "__metadata", {
    value: data,
    writable: false,
    enumerable: false, // Doesn't appear in normal traversals
    configurable: false,
  });
}

let user = {
  name: "Alice",
  email: "[email protected]",
};

addMetadata(user, {
  createdAt: new Date(),
  version: "1.0",
});

// Metadata is accessible but doesn't interfere with normal usage
console.log(user.__metadata);
// { createdAt: ..., version: "1.0" }

// Won't appear in serialization
console.log(JSON.stringify(user));
// {"name":"Alice","email":"[email protected]"}

// Won't appear in traversals
console.log(Object.keys(user)); // ["name", "email"]

defineProperties() - Define Multiple Properties

Object.defineProperties() can define multiple properties at once:

javascript
let rectangle = {};

Object.defineProperties(rectangle, {
  width: {
    value: 100,
    writable: true,
    enumerable: true,
  },
  height: {
    value: 50,
    writable: true,
    enumerable: true,
  },
  area: {
    get() {
      return this.width * this.height;
    },
    enumerable: true,
  },
  perimeter: {
    get() {
      return 2 * (this.width + this.height);
    },
    enumerable: true,
  },
});

console.log(rectangle.width); // 100
console.log(rectangle.area); // 5000
console.log(rectangle.perimeter); // 300

rectangle.width = 150;
console.log(rectangle.area); // 7500 - automatically calculated

Getters and Setters

Accessor properties use getter and setter functions to control property reading and assignment.

Basic Getter and Setter

javascript
let user = {
  firstName: "John",
  lastName: "Doe",

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  set fullName(value) {
    [this.firstName, this.lastName] = value.split(" ");
  },
};

console.log(user.fullName); // "John Doe"

user.fullName = "Jane Smith";
console.log(user.firstName); // "Jane"
console.log(user.lastName); // "Smith"
console.log(user.fullName); // "Jane Smith"

Defining Getters/Setters with defineProperty

javascript
let temperature = {
  _celsius: 25, // Internal storage
};

Object.defineProperty(temperature, "celsius", {
  get() {
    return this._celsius;
  },
  set(value) {
    if (value < -273.15) {
      throw new Error("Temperature below absolute zero!");
    }
    this._celsius = value;
  },
  enumerable: true,
});

Object.defineProperty(temperature, "fahrenheit", {
  get() {
    return (this._celsius * 9) / 5 + 32;
  },
  set(value) {
    this._celsius = ((value - 32) * 5) / 9;
  },
  enumerable: true,
});

console.log(temperature.celsius); // 25
console.log(temperature.fahrenheit); // 77

temperature.fahrenheit = 86;
console.log(temperature.celsius); // 30
console.log(temperature.fahrenheit); // 86

// Validation works
try {
  temperature.celsius = -300;
} catch (e) {
  console.log(e.message); // "Temperature below absolute zero!"
}

Real-world Application: Data Validation

javascript
function createUser(firstName, lastName, age) {
  let user = {
    _firstName: firstName,
    _lastName: lastName,
    _age: age,
  };

  Object.defineProperties(user, {
    firstName: {
      get() {
        return this._firstName;
      },
      set(value) {
        if (typeof value !== "string" || value.length === 0) {
          throw new Error("First name must be a non-empty string");
        }
        this._firstName = value;
      },
      enumerable: true,
    },

    lastName: {
      get() {
        return this._lastName;
      },
      set(value) {
        if (typeof value !== "string" || value.length === 0) {
          throw new Error("Last name must be a non-empty string");
        }
        this._lastName = value;
      },
      enumerable: true,
    },

    age: {
      get() {
        return this._age;
      },
      set(value) {
        if (typeof value !== "number" || value < 0 || value > 150) {
          throw new Error("Age must be between 0 and 150");
        }
        this._age = value;
      },
      enumerable: true,
    },

    fullName: {
      get() {
        return `${this._firstName} ${this._lastName}`;
      },
      enumerable: true,
    },
  });

  return user;
}

let user = createUser("Alice", "Johnson", 28);
console.log(user.fullName); // "Alice Johnson"

user.age = 29; // Valid
console.log(user.age); // 29

try {
  user.age = 200; // Invalid
} catch (e) {
  console.log(e.message); // "Age must be between 0 and 150"
}

Lazy Loading Properties

Getters can implement lazy loading—computing values only on first access:

javascript
let data = {
  _cache: null,

  get expensiveComputation() {
    if (this._cache === null) {
      console.log("Computing...");
      // Simulate expensive computation
      this._cache = Array.from({ length: 1000 }, (_, i) => i * i);
    }
    return this._cache;
  },
};

console.log("First access:");
console.log(data.expensiveComputation.length); // Computing... 1000

console.log("Second access:");
console.log(data.expensiveComputation.length); // 1000 (no computation)

Computed Properties

javascript
let product = {
  price: 100,
  quantity: 5,
  taxRate: 0.1,

  get subtotal() {
    return this.price * this.quantity;
  },

  get tax() {
    return this.subtotal * this.taxRate;
  },

  get total() {
    return this.subtotal + this.tax;
  },
};

console.log(`Price: $${product.price}`);
console.log(`Quantity: ${product.quantity}`);
console.log(`Subtotal: $${product.subtotal}`); // $500
console.log(`Tax: $${product.tax}`); // $50
console.log(`Total: $${product.total}`); // $550

product.quantity = 10;
console.log(`New total: $${product.total}`); // $1100 - automatically updated

Property Existence Checking

There are multiple methods to check if a property exists, each with specific uses.

in Operator

Checks if a property exists (including inherited properties):

javascript
let obj = {
  name: "Alice",
  age: 25,
};

console.log("name" in obj); // true
console.log("email" in obj); // false
console.log("toString" in obj); // true - inherited from Object.prototype

hasOwnProperty / Object.hasOwn

Only checks the object's own properties:

javascript
let obj = {
  name: "Alice",
};

console.log(obj.hasOwnProperty("name")); // true
console.log(obj.hasOwnProperty("toString")); // false

// Recommended to use Object.hasOwn (ES2022)
console.log(Object.hasOwn(obj, "name")); // true
console.log(Object.hasOwn(obj, "toString")); // false

Direct undefined Check

javascript
let obj = {
  name: "Alice",
  email: undefined,
};

console.log(obj.name !== undefined); // true
console.log(obj.email !== undefined); // false
console.log(obj.phone !== undefined); // false

// Note: This cannot distinguish between undefined values and non-existent properties
console.log("email" in obj); // true - property exists
console.log(obj.email !== undefined); // false - but value is undefined

Deleting Properties

Use the delete operator to remove properties:

javascript
let user = {
  name: "Bob",
  age: 30,
  email: "[email protected]",
};

delete user.email;
console.log(user); // { name: "Bob", age: 30 }
console.log(user.email); // undefined

// delete returns a boolean value
console.log(delete user.age); // true
console.log(delete user.phone); // true - even if property doesn't exist

// Cannot delete non-configurable properties
Object.defineProperty(user, "id", {
  value: 123,
  configurable: false,
});

console.log(delete user.id); // false - cannot delete
console.log(user.id); // 123 - still exists

Property Enumeration

Control whether properties appear in traversals.

for...in Loop

Iterates over all enumerable properties of the object itself and its prototype chain:

javascript
let parent = { inherited: "value" };
let child = Object.create(parent);
child.own = "own value";

for (let key in child) {
  console.log(key); // Prints "own" and "inherited"
}

// Only iterate over own properties
for (let key in child) {
  if (Object.hasOwn(child, key)) {
    console.log(key); // Only prints "own"
  }
}

Object.keys/values/entries

Only iterate over the object's own enumerable properties:

javascript
let obj = {
  a: 1,
  b: 2,
};

Object.defineProperty(obj, "c", {
  value: 3,
  enumerable: false,
});

console.log(Object.keys(obj)); // ["a", "b"]
console.log(Object.values(obj)); // [1, 2]
console.log(Object.entries(obj)); // [["a", 1], ["b", 2]]

// c doesn't appear in results

Object.getOwnPropertyNames

Get all own properties (including non-enumerable ones):

javascript
let obj = {
  visible: 1,
};

Object.defineProperty(obj, "hidden", {
  value: 2,
  enumerable: false,
});

console.log(Object.keys(obj)); // ["visible"]
console.log(Object.getOwnPropertyNames(obj)); // ["visible", "hidden"]

Real-world Case Study: Reactive Object

Create a simple reactive system that triggers callbacks when properties change:

javascript
function createReactive(obj, onChange) {
  let proxy = {};

  for (let key in obj) {
    if (Object.hasOwn(obj, key)) {
      let value = obj[key];

      Object.defineProperty(proxy, key, {
        get() {
          return value;
        },
        set(newValue) {
          let oldValue = value;
          value = newValue;
          onChange(key, oldValue, newValue);
        },
        enumerable: true,
        configurable: true,
      });
    }
  }

  return proxy;
}

let state = createReactive(
  {
    count: 0,
    message: "Hello",
  },
  (key, oldValue, newValue) => {
    console.log(`${key} changed from ${oldValue} to ${newValue}`);
  }
);

state.count = 1; // count changed from 0 to 1
state.message = "World"; // message changed from Hello to World
console.log(state.count); // 1

Real-world Case Study: Read-only View

Create a read-only view of an object to prevent accidental modification:

javascript
function createReadOnlyView(obj) {
  let proxy = {};

  for (let key in obj) {
    if (Object.hasOwn(obj, key)) {
      Object.defineProperty(proxy, key, {
        get() {
          return obj[key];
        },
        set() {
          throw new Error(`Cannot modify readonly property "${key}"`);
        },
        enumerable: true,
      });
    }
  }

  return proxy;
}

let data = {
  name: "Alice",
  age: 25,
};

let readOnly = createReadOnlyView(data);

console.log(readOnly.name); // "Alice"

try {
  readOnly.name = "Bob";
} catch (e) {
  console.log(e.message); // Cannot modify readonly property "name"
}

// But original object can still be modified
data.name = "Charlie";
console.log(readOnly.name); // "Charlie" - reflects changes to original

Real-world Case Study: Property Access Logging

Record property access and modification:

javascript
function createLoggingProxy(obj, logFn) {
  let proxy = {};

  for (let key in obj) {
    if (Object.hasOwn(obj, key)) {
      let value = obj[key];

      Object.defineProperty(proxy, key, {
        get() {
          logFn("GET", key, value);
          return value;
        },
        set(newValue) {
          logFn("SET", key, value, newValue);
          value = newValue;
        },
        enumerable: true,
      });
    }
  }

  return proxy;
}

let logs = [];
let user = createLoggingProxy(
  {
    name: "Alice",
    age: 25,
  },
  (action, key, ...values) => {
    logs.push({ action, key, values, timestamp: new Date() });
  }
);

user.name; // GET
user.age = 26; // SET
user.name = "Bob"; // SET

console.log(logs);
// [
//   { action: "GET", key: "name", values: ["Alice"], timestamp: ... },
//   { action: "SET", key: "age", values: [25, 26], timestamp: ... },
//   { action: "SET", key: "name", values: ["Alice", "Bob"], timestamp: ... }
// ]

Common Pitfalls and Best Practices

1. configurable: false is Irreversible

javascript
let obj = {};

Object.defineProperty(obj, "key", {
  value: "value",
  configurable: false,
});

// ❌ Cannot reconfigure
try {
  Object.defineProperty(obj, "key", {
    configurable: true,
  });
} catch (e) {
  console.log("Cannot redefine property"); // Will execute here
}

2. Properties with getters but no setters are read-only

javascript
let obj = {
  get value() {
    return 42;
  },
};

console.log(obj.value); // 42
obj.value = 100; // Fails silently
console.log(obj.value); // 42 - unchanged

3. Don't modify objects in getters

javascript
// ❌ Bad practice
let counter = {
  _count: 0,
  get count() {
    return this._count++; // Changes state every access
  },
};

console.log(counter.count); // 0
console.log(counter.count); // 1 - confusing

// ✅ Getters should be pure functions
let betterCounter = {
  _count: 0,
  get count() {
    return this._count; // Only reads, doesn't modify
  },
  increment() {
    this._count++; // Modify through methods
  },
};

4. Be careful with this binding

javascript
let obj = {
  value: 42,
  get getValue() {
    return this.value;
  },
};

console.log(obj.getValue); // 42

let fn = obj.getValue;
console.log(fn); // undefined - this is lost

// ✅ Use arrow functions or bind
let boundFn = obj.getValue.bind(obj);

Summary

Object property operations provide us with fine-grained control capabilities:

  • Property Descriptors - Control writable, enumerable, configurable attributes of properties
  • defineProperty/defineProperties - Precisely define properties and their attributes
  • Getter/Setter - Execute custom logic when accessing properties
  • Property Enumeration - Control which properties appear in traversals
  • Property Deletion - Use delete operator or configurable attribute

These features enable us to:

  • Create read-only or non-deletable properties
  • Implement computed properties and data validation
  • Hide internal implementation details
  • Build reactive systems and data proxies
  • Implement lazy loading and caching mechanisms

Mastering object property operations is key to writing robust, maintainable JavaScript code. While you may not frequently use these advanced features in daily development, they are indispensable tools when building frameworks, libraries, or scenarios requiring precise control over object behavior.