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:
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:
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:
value- The property's valuewritable- Whether the value can be modifiedenumerable- Whether the property can appear in enumerationsconfigurable- Whether the property can be deleted or have its attributes modified
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 existsdefineProperty() - Define Single Properties
Object.defineProperty() allows us to precisely define property attributes.
Basic Usage
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); // 1500Creating Read-only Properties
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 existsCreating Non-enumerable Properties
Non-enumerable properties won't appear in for...in loops, Object.keys(), or Object.values():
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
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:
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 calculatedGetters and Setters
Accessor properties use getter and setter functions to control property reading and assignment.
Basic Getter and Setter
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
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
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:
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
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 updatedProperty 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):
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.prototypehasOwnProperty / Object.hasOwn
Only checks the object's own properties:
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")); // falseDirect undefined Check
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 undefinedDeleting Properties
Use the delete operator to remove properties:
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 existsProperty Enumeration
Control whether properties appear in traversals.
for...in Loop
Iterates over all enumerable properties of the object itself and its prototype chain:
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:
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 resultsObject.getOwnPropertyNames
Get all own properties (including non-enumerable ones):
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:
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); // 1Real-world Case Study: Read-only View
Create a read-only view of an object to prevent accidental modification:
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 originalReal-world Case Study: Property Access Logging
Record property access and modification:
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
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
let obj = {
get value() {
return 42;
},
};
console.log(obj.value); // 42
obj.value = 100; // Fails silently
console.log(obj.value); // 42 - unchanged3. Don't modify objects in getters
// ❌ 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
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,configurableattributes 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
deleteoperator orconfigurableattribute
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.