
Objects in JavaScript are essentially collections of key-value pairs. Each key, or property name, maps to a value which can be anything from a primitive like a string or number to another object or even a function. At the core, these properties define the shape and behavior of the object.
Properties can be enumerable, writable, and configurable, and these attributes control how the property interacts with the rest of the program. For example, a property can be made read-only by setting writable: false, which prevents accidental overwrites.
Not all properties are created equal. There are data properties, which store values, and accessor properties, which use getters and setters to execute code on property access or assignment. This distinction very important when designing objects that need to encapsulate behavior rather than just state.
Here’s a quick example demonstrating a data property versus an accessor property:
const user = {
firstName: "Jane",
lastName: "Doe",
get fullName() {
return this.firstName + " " + this.lastName;
},
set fullName(name) {
const parts = name.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
};
console.log(user.fullName); // "Jane Doe"
user.fullName = "John Smith";
console.log(user.firstName); // "John"
console.log(user.lastName); // "Smith"
Notice how fullName isn’t a stored value but a dynamic property computed on the fly. This lets you write cleaner and more intuitive APIs for your objects.
Also, it’s worth pointing out that property keys are always strings or symbols under the hood. Even if you use a number as a key, JavaScript coerces it to a string. This subtlety often trips people up when working with arrays or objects interchangeably.
Here’s an example to clarify this behavior:
const obj = {};
obj[1] = "one";
obj["1"] = "ONE";
console.log(obj[1]); // "ONE"
console.log(obj["1"]); // "ONE"
Both obj[1] and obj["1"] point to the same property because the numeric key 1 is converted to the string "1". That’s a big reason why arrays have special behavior – their keys are numeric indices but treated as strings in the object model.
Understanding these fundamentals about how properties work at the language level helps in writing more predictable and bug-free code, especially when dealing with dynamic property access or working with APIs that manipulate object shapes.
Amazon Basics DisplayPort to HDMI Cable, Uni-Directional, 4K@30Hz, 1920x1200, 1080p, Gold-Plated Connectors for Enhanced Picture Quality and Sound, 6 ft, Black
$8.39 (as of June 2, 2026 22:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Using dot notation and bracket notation
When it comes to accessing properties on JavaScript objects, you have two primary methods: dot notation and bracket notation. Both serve the same purpose but have different use cases that can impact your code’s readability and functionality.
Dot notation is the most simpler way to access properties. You simply use a dot followed by the property name. This method is clean and easy to read, making it the preferred choice when property names are known and valid identifiers.
const car = {
make: "Toyota",
model: "Camry",
year: 2020
};
console.log(car.make); // "Toyota"
console.log(car.model); // "Camry"
However, dot notation has its limitations. If your property name contains spaces, special characters, or starts with a number, you cannot use dot notation. In such cases, bracket notation comes to the rescue. With bracket notation, you can access properties using a string that represents the property name, allowing for greater flexibility.
const person = {
"first name": "Alice",
"age": 30,
"1stPlace": "Gold"
};
console.log(person["first name"]); // "Alice"
console.log(person["1stPlace"]); // "Gold"
Bracket notation also allows you to use variables to access properties dynamically, which can be particularly useful in loops or when dealing with user input. For instance:
const dynamicKey = "age"; console.log(person[dynamicKey]); // 30
Understanding when to use dot versus bracket notation is important for writing clean, maintainable code. While dot notation is typically easier to read and write, bracket notation offers flexibility that can be indispensable in certain scenarios.
When updating object properties, it’s important to choose your method wisely based on the property names and your specific requirements. Dot notation is simpler for standard properties:
car.year = 2021; // Update using dot notation
For properties that have unconventional names, you must use bracket notation:
person["first name"] = "Bob"; // Update using bracket notation
It is also a good practice to check if a property exists before updating it. This can prevent errors in your code due to typos or incorrect assumptions about the object’s structure. You can use the in operator or the hasOwnProperty method:
if ("year" in car) {
car.year = 2022; // Safe to update
}
if (person.hasOwnProperty("age")) {
person.age += 1; // Increment age
}
By following these best practices, you can ensure that your code remains robust and reduces the likelihood of unintended side effects. It’s also worth considering encapsulation techniques, such as using getters and setters, which can provide additional control over how properties are accessed and modified.
Best practices for updating object properties
When updating object properties, immutability is a concept worth considering, especially in larger applications or when working with state management libraries like Redux. Instead of mutating the original object, create a new one with the updated values. This approach helps prevent bugs caused by unintended side effects.
Here’s a simple example using the spread operator to create a shallow copy of an object with an updated property:
const originalUser = {
name: "Alice",
age: 25
};
const updatedUser = {
...originalUser,
age: 26
};
console.log(originalUser.age); // 25
console.log(updatedUser.age); // 26
This pattern is especially useful when you want to preserve the original object’s state and apply changes without mutation. Note, however, that the spread operator performs a shallow copy, so nested objects will still be referenced rather than cloned.
For deeply nested objects, libraries like lodash or techniques such as deep cloning may be required to avoid mutation pitfalls:
const _ = require("lodash");
const deepUser = {
name: "Bob",
preferences: {
theme: "dark"
}
};
const updatedDeepUser = _.cloneDeep(deepUser);
updatedDeepUser.preferences.theme = "light";
console.log(deepUser.preferences.theme); // "dark"
console.log(updatedDeepUser.preferences.theme); // "light"
When mutating objects directly, always be mindful of property descriptors. For example, if a property is non-writable or non-configurable, attempts to update it will fail silently in non-strict mode or throw an error in strict mode. Use Object.getOwnPropertyDescriptor to inspect these attributes:
const obj = {};
Object.defineProperty(obj, "fixed", {
value: 42,
writable: false,
configurable: false
});
obj.fixed = 100; // Fails silently in non-strict mode
console.log(obj.fixed); // 42
console.log(Object.getOwnPropertyDescriptor(obj, "fixed"));
/*
{
value: 42,
writable: false,
enumerable: false,
configurable: false
}
*/
In performance-critical code, repeatedly updating nested properties can be costly. Minimize unnecessary updates by comparing current and new values before assignment:
function updateProperty(obj, key, newValue) {
if (obj[key] !== newValue) {
obj[key] = newValue;
}
}
This simple guard prevents needless property sets and can reduce triggering of getters, setters, or proxy traps.
Finally, when working with objects that represent data models or entities, consider using classes with methods to encapsulate property updates. This allows enforcing validation, maintaining invariants, and hiding implementation details:
class User {
constructor(name, age) {
this.name = name;
this._age = age;
}
get age() {
return this._age;
}
set age(value) {
if (value < 0) {
throw new Error("Age must be positive");
}
this._age = value;
}
celebrateBirthday() {
this.age += 1;
}
}
const user = new User("Charlie", 30);
user.celebrateBirthday();
console.log(user.age); // 31
Using setters here enforces constraints and centralizes logic related to property updates, which is important as your application grows more complex.
