How to Use Objects in JavaScript: Creating, Accessing, and Managing Key-Value Pairs
Objects are the most important data structure in JavaScript. Nearly everything in the language is an object or behaves like one: arrays, functions, dates, regular expressions, and even errors. When you fetch data from an API, it arrives as an object. When you configure a library, you pass an object. When you describe a user, a product, or a setting, you use an object.
An object is a collection of key-value pairs where keys are strings (or Symbols) and values can be anything: numbers, strings, arrays, functions, or even other objects. This guide covers everything you need to know about creating and working with objects, from basic property access to computed property names, property existence checks, iteration, and the critical mutation pitfall that catches every JavaScript developer at some point.
What Is an Object?
An object is an unordered collection of properties, where each property has a key (also called a name) and a value. Think of it as a real-world dictionary: each word (key) has a definition (value).
const user = {
name: 'Alice', // key: "name", value: "Alice"
age: 30, // key: "age", value: 30
isActive: true, // key: "isActive", value: true
hobbies: ['reading', 'hiking'], // key: "hobbies", value: an array
};
This object has four properties. The keys are name, age, isActive, and hobbies. The values are a string, a number, a boolean, and an array. Values can be of any type, including other objects.
Objects Represent Real-World Entities
Objects are the natural way to group related data:
// A product in an e-commerce system
const product = {
id: 1042,
name: 'Wireless Headphones',
price: 79.99,
inStock: true,
categories: ['electronics', 'audio'],
dimensions: {
width: 18,
height: 20,
depth: 8,
unit: 'cm',
},
};
// A configuration object
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
debug: false,
};
Creating Objects: Literal Syntax
The object literal is the most common and recommended way to create objects. You list key-value pairs inside curly braces, separated by commas.
Basic Object Literal
const car = {
make: 'Toyota',
model: 'Camry',
year: 2024,
color: 'blue',
};
Empty Object
const empty = {};
console.log(empty); // {}
// Properties can be added later
empty.name = 'Alice';
console.log(empty); // { name: "Alice" }
Multiline vs. Single Line
For objects with one or two properties, single-line is fine. For anything more, use multiline with trailing commas:
// Single line: acceptable for very short objects
const point = { x: 10, y: 20 };
const size = { width: 100, height: 50 };
// Multiline: preferred for 3+ properties
const user = {
name: 'Alice',
age: 30,
email: 'alice@example.com',
role: 'admin',
};
The trailing comma after 'admin' is recommended. It makes adding new properties cleaner in version control diffs and prevents syntax errors when reordering properties.
Nested Objects
Objects can contain other objects, creating hierarchical data structures:
const company = {
name: 'Acme Corp',
address: {
street: '123 Main St',
city: 'Springfield',
state: 'IL',
zip: '62701',
},
ceo: {
name: 'Jane Smith',
email: 'jane@acme.com',
},
};
console.log(company.address.city); // "Springfield"
console.log(company.ceo.name); // "Jane Smith"
Property Keys Are Strings
Object keys are always strings (or Symbols). If you use a non-string as a key in an object literal, JavaScript converts it to a string:
const obj = {
1: 'one', // Key is actually the string "1"
true: 'yes', // Key is the string "true"
null: 'nothing', // Key is the string "null"
'hello world': 42, // Keys with spaces must be quoted
'my-prop': 100, // Keys with hyphens must be quoted
};
console.log(obj['1']); // "one"
console.log(obj['true']); // "yes"
console.log(obj['hello world']); // 42
Keys that are valid JavaScript identifiers (no spaces, no hyphens, do not start with a digit) do not need quotes. Keys that are not valid identifiers must be quoted.
Accessing Properties: Dot Notation vs. Bracket Notation
JavaScript provides two ways to read and write object properties.
Dot Notation
Dot notation is the most common and most readable way to access properties:
const user = {
name: 'Alice',
age: 30,
email: 'alice@example.com',
};
console.log(user.name); // "Alice"
console.log(user.age); // 30
console.log(user.email); // "alice@example.com"
Dot notation requires the property name to be a valid identifier (no spaces, no hyphens, does not start with a digit) and to be known at write time (not stored in a variable).
Bracket Notation
Bracket notation uses a string expression inside square brackets:
const user = {
name: 'Alice',
age: 30,
email: 'alice@example.com',
};
console.log(user['name']); // "Alice"
console.log(user['age']); // 30
Bracket notation is required when:
1. The property name is not a valid identifier:
const data = {
'first-name': 'Alice',
'last name': 'Smith',
'2fast': true,
};
// Dot notation would cause SyntaxError for these keys
console.log(data['first-name']); // "Alice"
console.log(data['last name']); // "Smith"
console.log(data['2fast']); // true
2. The property name is stored in a variable:
const user = {
name: 'Alice',
age: 30,
email: 'alice@example.com',
};
const key = 'name';
console.log(user[key]); // "Alice" (reads user.name)
console.log(user.key); // undefined (looks for a property literally called "key")
// Dynamic access based on user input
function getUserField(user, fieldName) {
return user[fieldName];
}
console.log(getUserField(user, 'email')); // "alice@example.com"
console.log(getUserField(user, 'age')); // 30
3. The property name is computed from an expression:
const index = 2;
const prop = 'item' + index;
console.log(obj[prop]); // Reads obj.item2
const prefix = 'get';
const method = 'Name';
console.log(obj[prefix + method]); // Reads obj.getName
When to Use Which
| Situation | Use | Example |
|---|---|---|
| Known property name | Dot notation | user.name |
| Property name in a variable | Bracket notation | user[key] |
| Property name with special characters | Bracket notation | obj['my-prop'] |
| Dynamic/computed property name | Bracket notation | obj['item' + i] |
| Everything else | Dot notation | More readable |
Default to dot notation. It is shorter, cleaner, and more readable. Only use bracket notation when you have a specific reason (variable keys, special characters, computed names).
Adding, Modifying, and Deleting Properties
JavaScript objects are mutable. You can freely add, change, and remove properties after creation.
Adding Properties
const user = {
name: 'Alice',
};
// Add new properties with dot notation
user.age = 30;
user.email = 'alice@example.com';
// Add new properties with bracket notation
user['phone'] = '555-0123';
console.log(user);
// { name: "Alice", age: 30, email: "alice@example.com", phone: "555-0123" }
Modifying Properties
const user = {
name: 'Alice',
age: 30,
};
user.name = 'Bob';
user.age = 31;
console.log(user); // { name: "Bob", age: 31 }
Deleting Properties
The delete operator removes a property from an object:
const user = {
name: 'Alice',
age: 30,
tempToken: 'abc123',
};
delete user.tempToken;
console.log(user); // { name: "Alice", age: 30 }
console.log(user.tempToken); // undefined (property no longer exists)
delete returns true if the operation succeeds (even if the property did not exist):
const user = {
name: 'Alice',
age: 30,
tempToken: 'abc123',
};
console.log(delete user.name); // true (property removed)
console.log(delete user.nonExistent); // true (nothing to remove, but no error)
const Does Not Mean Immutable
A common point of confusion: declaring an object with const prevents reassignment of the variable, not modification of the object:
const user = { name: 'Alice' };
// Modifying properties: ALLOWED
user.name = 'Bob';
user.age = 30;
delete user.age;
// Reassigning the variable: FORBIDDEN
user = { name: 'Charlie' };
// TypeError: Assignment to constant variable
The variable user always points to the same object. But the contents of that object are freely modifiable. This distinction is covered more deeply in the next chapter on object references.
Computed Property Names
ES6 introduced computed property names, which let you use expressions as property keys inside object literals.
Basic Syntax
const key = 'color';
const obj = {
[key]: 'blue', // Same as: color: "blue"
};
console.log(obj.color); // "blue"
The expression inside [ ] is evaluated, converted to a string, and used as the property name.
Dynamic Keys from Variables
function createField(name, value) {
return {
[name]: value,
};
}
console.log(createField('username', 'alice'));
// { username: "alice" }
console.log(createField('score', 100));
// { score: 100 }
Expressions as Keys
const prefix = 'user';
const obj = {
[`${prefix}Name`]: 'Alice',
[`${prefix}Age`]: 30,
[`${prefix}Email`]: 'alice@example.com',
};
console.log(obj.userName); // "Alice"
console.log(obj.userAge); // 30
console.log(obj.userEmail); // "alice@example.com"
Practical Use Cases
// Building an object from an array of key-value pairs
const entries = [['name', 'Alice'], ['age', 30], ['city', 'NYC']];
const user = {};
for (const [key, value] of entries) {
user[key] = value;
}
// Or use Object.fromEntries (covered later):
const entries = [['name', 'Alice'], ['age', 30], ['city', 'NYC']]
const user = Object.fromEntries(entries);
console.log(user); // { name: "Alice", age: 30, city: "NYC" }
// Creating a lookup object from an array
const colors = ['red', 'green', 'blue'];
const colorIndex = {};
colors.forEach((color, i) => {
colorIndex[color] = i;
});
console.log(colorIndex); // { red: 0, green: 1, blue: 2 }
// Updating state in a React-like pattern
function updateField(state, fieldName, value) {
return {
...state,
[fieldName]: value,
};
}
let form = { name: '', email: '', age: '' };
form = updateField(form, 'name', 'Alice');
form = updateField(form, 'email', 'alice@example.com');
console.log(form); // { name: "Alice", email: "alice@example.com", age: "" }
Property Shorthand
When a variable name matches the desired property name, ES6 lets you omit the value:
Basic Shorthand
const name = 'Alice';
const age = 30;
const email = 'alice@example.com';
// Without shorthand (repetitive)
const user = {
name: name,
age: age,
email: email,
};
// With shorthand (clean)
const user = {
name,
age,
email,
};
// Both create: { name: "Alice", age: 30, email: "alice@example.com" }
Mixing Shorthand and Regular Properties
const name = 'Alice';
const age = 30;
const user = {
name, // Shorthand
age, // Shorthand
role: 'admin', // Regular (different key/value names)
createdAt: new Date(), // Regular (computed value)
};
Shorthand with Function Return Values
This pattern is extremely common in modern JavaScript:
function getCoordinates() {
const x = Math.random() * 100;
const y = Math.random() * 100;
return { x, y }; // Shorthand: { x: x, y: y }
}
const { x, y } = getCoordinates();
console.log(x, y); // 27.55489709024396 12.712481535724873 (random values)
Method Shorthand
ES6 also provides shorthand for methods (functions as property values):
// Without shorthand
const calculator = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
},
};
// With method shorthand
const calculator = {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
},
};
console.log(calculator.add(5, 3)); // 8
console.log(calculator.subtract(10, 4)); // 6
Method shorthand is not just shorter. Methods defined with shorthand can use super in class inheritance, which regular function properties cannot. This distinction matters when working with classes.
Property Existence Check
When you access a property that does not exist, JavaScript returns undefined. But undefined can also be the intentional value of a property. You need reliable ways to distinguish between "property exists with value undefined" and "property does not exist at all."
The in Operator
The in operator returns true if the property exists in the object, regardless of its value:
const user = {
name: 'Alice',
age: undefined, // Intentionally set to undefined
};
console.log('name' in user); // true (property exists)
console.log('age' in user); // true (property exists (even though value is undefined))
console.log('email' in user); // false (property does not exist)
Note that the left operand of in must be a string (or Symbol):
// CORRECT: string key
console.log('name' in user);
// WRONG: unquoted identifier would be a variable reference
console.log(name in user); // ReferenceError if 'name' variable doesn't exist
// or checks for the VALUE of the variable 'name'
Checking Against undefined
The simplest (but less precise) check:
const user = {
name: 'Alice',
age: undefined,
};
console.log(user.name !== undefined); // true
console.log(user.age !== undefined); // false (but the property DOES exist!)
console.log(user.email !== undefined); // false (property doesn't exist)
This fails when a property intentionally holds undefined. In practice, storing undefined as a value is uncommon, so this check works for most situations.
hasOwnProperty and Object.hasOwn
These check only the object's own properties, ignoring inherited ones from the prototype chain:
const user = { name: 'Alice' };
// hasOwnProperty (traditional)
console.log(user.hasOwnProperty('name')); // true
console.log(user.hasOwnProperty('toString')); // false (inherited, not own)
// Object.hasOwn (modern, ES2022 - preferred)
console.log(Object.hasOwn(user, 'name')); // true
console.log(Object.hasOwn(user, 'toString')); // false
Object.hasOwn() is preferred over hasOwnProperty() because it works on objects created with Object.create(null), which do not have hasOwnProperty on their prototype.
Comparison of Existence Checks
const obj = {
a: 1,
b: undefined,
c: null,
};
// Missing property
console.log('d' in obj); // false
console.log(obj.d !== undefined); // false
console.log(Object.hasOwn(obj, 'd')); // false
// Property with undefined value
console.log('b' in obj); // true ✓
console.log(obj.b !== undefined); // false ✗ (misleading!)
console.log(Object.hasOwn(obj, 'b')); // true ✓
// Property with null value
console.log('c' in obj); // true
console.log(obj.c !== undefined); // true (null !== undefined)
console.log(Object.hasOwn(obj, 'c')); // true
Use Object.hasOwn(obj, key) for reliable existence checks. It handles all edge cases correctly and is the modern standard. Use in when you want to check for inherited properties as well.
The for...in Loop
The for...in loop iterates over the enumerable string-keyed properties of an object, including inherited ones.
Basic Usage
const user = {
name: 'Alice',
age: 30,
email: 'alice@example.com',
};
for (const key in user) {
console.log(`${key}: ${user[key]}`);
}
// name: Alice
// age: 30
// email: alice@example.com
Filtering Own Properties
for...in includes inherited properties from the prototype chain. Use Object.hasOwn() to filter them out:
const parent = { inherited: true };
const child = Object.create(parent);
child.own = true;
child.name = 'Alice';
// Without filter: includes inherited properties
for (const key in child) {
console.log(key);
}
// own, name, inherited
// With filter: own properties only
for (const key in child) {
if (Object.hasOwn(child, key)) {
console.log(key);
}
}
// own, name
Modern Alternatives to for...in
Modern JavaScript provides cleaner ways to iterate over object properties:
const user = {
name: 'Alice',
age: 30,
email: 'alice@example.com',
};
// Object.keys() - array of keys
Object.keys(user).forEach((key) => {
console.log(`${key}: ${user[key]}`);
});
// Object.values() - array of values
Object.values(user).forEach((value) => {
console.log(value);
});
// Object.entries() - array of [key, value] pairs
Object.entries(user).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
// for...of with Object.entries() - cleanest syntax
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
These methods return only own enumerable properties, so you do not need to filter for inherited ones.
Practical Example: Object Transformation
const prices = {
apple: 1.5,
banana: 0.75,
cherry: 3.0,
date: 8.5,
};
// Create a new object with doubled prices
const doublePrices = {};
for (const [fruit, price] of Object.entries(prices)) {
doublePrices[fruit] = price * 2;
}
console.log(doublePrices);
// { apple: 3, banana: 1.5, cherry: 6, date: 17 }
// Or more concisely with Object.fromEntries:
const doublePrices = Object.fromEntries(
Object.entries(prices).map(([fruit, price]) => [fruit, price * 2]),
);
console.log(doublePrices);
// { apple: 3, banana: 1.5, cherry: 6, date: 17 }
Property Order in Objects
JavaScript objects are technically unordered collections, but modern engines follow specific rules for property iteration order (standardized in ES2015+).
The Rules
- Integer-like keys (keys that look like array indices) come first, sorted numerically in ascending order
- String keys come next, in insertion order
- Symbol keys come last, in insertion order
const obj = {
banana: 2,
10: 'ten',
1: 'one',
apple: 1,
5: 'five',
cherry: 3,
};
console.log(Object.keys(obj));
// ["1", "5", "10", "banana", "apple", "cherry"]
// ↑ integers sorted ↑ ↑ strings in insertion order ↑
Why This Matters
If you use numeric keys in an object and iterate over it, the order may not match the insertion order:
const steps = {};
steps[3] = 'Third';
steps[1] = 'First';
steps[2] = 'Second';
for (const [num, label] of Object.entries(steps)) {
console.log(`${num}: ${label}`);
}
// 1: First
// 2: Second
// 3: Third
// Sorted numerically, NOT insertion order!
If you need guaranteed insertion order with non-string keys, use a Map instead of an object (covered in a later chapter).
Practical Impact
For most use cases (string keys like name, email, age), properties maintain insertion order. The integer-sorting rule only affects keys that parse as non-negative integers ("0", "1", "42", etc.).
// String keys: insertion order preserved
const config = {};
config.theme = 'dark';
config.language = 'en';
config.fontSize = 14;
console.log(Object.keys(config));
// ["theme", "language", "fontSize"] (insertion order ✓)
Common Mistake: Object Mutation When You Do Not Expect It
This is one of the most frequent sources of bugs in JavaScript. Objects are reference types. When you assign an object to a new variable or pass it to a function, you are not creating a copy. You are creating another reference to the same object in memory.
The Problem
const original = {
name: 'Alice',
settings: {
theme: 'dark',
notifications: true,
},
};
// This does NOT create a copy
const copy = original;
// Modifying 'copy' also modifies 'original'
copy.name = 'Bob';
copy.settings.theme = 'light';
console.log(original.name); // "Bob" (changed!)
console.log(original.settings.theme); // "light" (changed!)
console.log(original === copy); // true (same object in memory)
Both original and copy point to the same object. There is only one object in memory with two labels attached to it.
The Same Problem with Functions
function updateUserRole(user) {
user.role = 'admin'; // Modifies the ORIGINAL object!
return user;
}
const alice = { name: 'Alice', role: 'viewer' };
const updated = updateUserRole(alice);
console.log(alice.role); // "admin" (the original was mutated!)
console.log(alice === updated); // true (same object)
The function received a reference to the same object, not a copy. Any modifications inside the function affect the original.
Fix 1: Shallow Copy with Spread Operator
const original = { name: 'Alice', age: 30 };
const copy = { ...original };
copy.name = 'Bob';
console.log(original.name); // "Alice" (unchanged!)
console.log(copy.name); // "Bob"
console.log(original === copy); // false (different objects)
Fix 2: Shallow Copy with Object.assign
const original = { name: 'Alice', age: 30 };
const copy = Object.assign({}, original);
copy.name = 'Bob';
console.log(original.name); // "Alice" (unchanged!)
The Shallow Copy Limitation
Both spread and Object.assign create shallow copies. Nested objects are still shared:
const original = {
name: 'Alice',
settings: {
theme: 'dark',
},
};
const shallow = { ...original };
shallow.name = 'Bob'; // Safe: primitive value copied
shallow.settings.theme = 'light'; // NOT safe: nested object is shared!
console.log(original.name); // "Alice" (unchanged ✓)
console.log(original.settings.theme); // "light" (CHANGED! ✗)
The settings object inside original and shallow is the same object. The spread operator only copied the reference to settings, not the object itself.
Fix 3: Deep Copy with structuredClone
For truly independent copies including nested objects, use structuredClone() (available in all modern browsers and Node.js 17+):
const original = {
name: 'Alice',
settings: {
theme: 'dark',
colors: ['red', 'blue'],
},
};
const deep = structuredClone(original);
deep.settings.theme = 'light';
deep.settings.colors.push('green');
console.log(original.settings.theme); // "dark" (unchanged ✓)
console.log(original.settings.colors); // ["red", "blue"] (unchanged ✓)
Fix 4: Non-Mutating Functions
Write functions that return new objects instead of modifying their arguments:
// BAD: mutates the input
function addRole(user) {
user.role = 'admin';
return user;
}
// GOOD: returns a new object
function addRole(user) {
return {
...user,
role: 'admin',
};
}
const alice = { name: 'Alice' };
const adminAlice = addRole(alice);
console.log(alice.role); // undefined (untouched)
console.log(adminAlice.role); // "admin"
console.log(alice === adminAlice); // false (different objects)
Quick Reference: Copying Methods
| Method | Depth | Use Case |
|---|---|---|
= | Reference only | When you WANT shared references |
{ ...obj } | Shallow | Simple flat objects |
Object.assign({}, obj) | Shallow | Same as spread, older syntax |
structuredClone(obj) | Deep | Nested objects, arrays |
JSON.parse(JSON.stringify(obj)) | Deep (limited) | No functions, no undefined, no Date precision |
Accidental mutation is the source of some of the most frustrating bugs in JavaScript. When you pass an object to a function, always consider whether that function might modify it. When you assign an object to a new variable, remember that both variables share the same underlying object. When in doubt, spread ({ ...obj }) or deep clone (structuredClone(obj)) before modifying.
Summary
Objects are the fundamental data structure of JavaScript, and mastering them is essential for everything that follows:
- Objects are collections of key-value pairs. Keys are strings (or Symbols), values can be any type.
- Create objects with literal syntax
{}. Use trailing commas in multiline objects. - Access properties with dot notation (
obj.key) for known keys and bracket notation (obj[variable]) for dynamic keys, keys with special characters, or keys stored in variables. - Objects are mutable. Properties can be added, modified, and deleted at any time with
=anddelete. - Computed property names (
{ [expression]: value }) let you use variables and expressions as property keys in object literals. - Property shorthand (
{ name, age }) eliminates repetition when the variable name matches the property key. Method shorthand ({ greet() {} }) provides cleaner syntax for functions in objects. - Check property existence with
Object.hasOwn(obj, key)(recommended),inoperator (includes inherited), or!== undefined(fails for intentionally undefined values). for...initerates over all enumerable string-keyed properties including inherited ones. UseObject.keys(),Object.values(), orObject.entries()for own properties only.- Property order follows specific rules: integer-like keys are sorted numerically first, then string keys in insertion order.
- Objects are reference types. Assigning an object to a new variable creates a shared reference, not a copy. Use spread (
{ ...obj }) for shallow copies andstructuredClone()for deep copies to avoid accidental mutation.