JavaScript Objects: Object Literals, Properties, Object.keys and structuredClone
We learn object literals, property access with dots and brackets, nested objects, methods and this, in and Object.keys/values/entries, for...in, shallow copies with spread and deep copies with structuredClone.
Version note: The code in this post targets ES2026 and current browsers (Chrome, Edge, Firefox, Safari).
Predict First
In the post on arrays we said that copying with spread is "shallow" and left the details for objects. Now it's time. Look at this code:
const user = { name: "Ada", address: { city: "Izmir" } };
const copy = { ...user };
copy.name = "Grace";
copy.address.city = "Ankara";
console.log(user.name, user.address.city);
We changed the copy. Was the original user affected? Does the console show Ada Izmir, Grace Ankara, or some mix of the two? Make a guess before you keep reading. We'll explain the answer in the section on copying.
Object Literals: Keeping Related Data Together
You could keep a user's name, age and email in separate variables, but after a while you'd lose track of which variable belongs to which user. An object gathers related data under one roof. Think of it as a filing cabinet with labeled drawers: each drawer has a name (the key) and a value inside it. Paste this into your browser's console and try it:
const user = {
name: "Ada",
age: 28,
email: "ada@example.com",
isAdmin: false,
skills: ["HTML", "CSS", "JavaScript"]
};
console.log(user);
console.log(typeof user); // "object"
This structure written with curly braces is called an object literal. Each line is a property: the key on the left, a colon, the value on the right. Properties are separated by commas. A value can be anything: a string, a number, a boolean, an array, even another object or a function. Keys, on the other hand, are actually strings; being able to write them without quotes is just a convenience.
Reading and Writing Properties
There are two ways to reach a property: the dot and square brackets.
const user = { name: "Ada", age: 28 };
console.log(user.name); // "Ada"
console.log(user["age"]); // 28
console.log(user.phone); // undefined
user.age = 29; // update
user.city = "Izmir"; // add a new property
delete user.city; // remove the property
console.log(user); // { name: "Ada", age: 29 }
Reading a property that doesn't exist doesn't throw an error; you get undefined. Assign a value to an existing property and it's updated; assign to one that doesn't exist and a new drawer opens. delete pulls the drawer out completely.
The dot notation is short and readable, and it's what you'll use most in everyday code. Square brackets are required in two cases. First, when the key contains characters like spaces or hyphens. Second, and more important, when the key's name is stored in a variable:
const product = { "product name": "Keyboard", price: 750 };
console.log(product["product name"]); // "Keyboard"
const field = "price";
console.log(product[field]); // 750
console.log(product.field); // undefined
The last line is a common mistake. product.field looks for a property named "field". product[field] first looks at the value of the field variable, which is "price", and opens that drawer. When the property you want to read is only known while the code runs, you use square brackets.
Nested Objects and Arrays of Objects
An object can contain another object, and an array can contain objects. Most real-world data looks exactly like this:
const order = {
id: 1024,
customer: {
name: "Grace",
address: { city: "Bursa", zip: "16000" }
},
items: [
{ title: "Pen", price: 25 },
{ title: "Notebook", price: 60 }
]
};
console.log(order.customer.name); // "Grace"
console.log(order.customer.address.city); // "Bursa"
console.log(order.items[1].title); // "Notebook"
console.log(order.items.length); // 2
To read it you follow the path step by step: first order, from there customer, then address, and finally city. When you reach an array, you pick an element by index and then carry on with the dot. The loops and forEach we saw earlier get along very well with these structures; calculating an order's total is just one pass over items.
Methods and a First Touch of this
When a property's value is a function, we call it a method. The log in console.log is a method, and so is toUpperCase on strings. We can add methods to our own objects too:
const counter = {
count: 0,
increase: function () {
this.count = this.count + 1;
return this.count;
}
};
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.count); // 2
The this inside a method points to the object that called the method at that moment. When you write counter.increase(), counter is to the left of the dot, so this is counter. That's how an object points to its own drawers and says "mine." There's also a shorter syntax for writing methods, which we'll see with modern syntax in the Intermediate series. How this is determined, and why it behaves differently in arrow functions, is a topic of its own; we'll go into it in the Intermediate series as well.
Does a Property Exist? in and Object.keys/values/entries
You ask whether an object has a certain property with the in operator:
const settings = { theme: "dark", fontSize: 16, sidebar: undefined };
console.log("theme" in settings); // true
console.log("language" in settings); // false
console.log("sidebar" in settings); // true
console.log(settings.sidebar !== undefined); // false
Watch the last two lines. The sidebar drawer exists but it's empty; its value is undefined. in checks whether the drawer exists and says true. Comparing with undefined looks at the content and can mislead you.
There are three helpers for getting all of an object's keys, values, or both as an array. Object.keys has been in the language for a long time; Object.values and Object.entries arrived with ES2017:
const scores = { alan: 85, ada: 92, grace: 78 };
console.log(Object.keys(scores)); // ["alan", "ada", "grace"]
console.log(Object.values(scores)); // [85, 92, 78]
console.log(Object.entries(scores));
// [["alan", 85], ["ada", 92], ["grace", 78]]
console.log(Object.keys(scores).length); // 3
The beauty of these is that the result is an array. Now you can use everything you learned about arrays: finding the number of properties with length, searching with includes, walking through with for...of or forEach. Each element of Object.entries is a two-element array: the first is the key, the second is the value.
Walking an Object with for...in
In the post on loops we said that for...in is meant for objects. On each round it gives you the next key:
const scores = { alan: 85, ada: 92, grace: 78 };
for (const key in scores) {
console.log(`${key}: ${scores[key]}`);
}
// alan: 85
// ada: 92
// grace: 78
for (const entry of Object.entries(scores)) {
console.log(entry[0], entry[1]);
}
Inside the loop we used square brackets to reach the value, because the key is stored in a variable; that's exactly the rule from a moment ago. Besides an object's own properties, for...in can also walk through properties it inherits; we'll see prototypes in the Advanced series. That's why many teams prefer the Object.entries plus for...of pair from the second example. If writing entry[0] and entry[1] feels clunky, you're right; we'll see a much neater way to write this with destructuring in the Intermediate series.
Copying Objects: Spread and structuredClone
The answer to the opening question: Ada Ankara. The name was kept, the city changed.
Object spread, { ...user }, added in ES2018, creates a new object and copies user's properties into it one by one. name is a string, a value, so the value itself goes into the copy. That's why changing copy.name didn't affect the original. But address is an object, and what goes into the copy is not the object itself but a reference to it. In the new filing cabinet's address drawer sits the key to the same room as in the old cabinet. When you changed copy.address.city, you moved furniture in that shared room. This is called a shallow copy: only the first layer is copied.
To copy nested objects independently as well, in other words to take a deep copy, the browser gives you structuredClone:
const user = { name: "Ada", address: { city: "Izmir" } };
const deep = structuredClone(user);
deep.address.city = "Ankara";
console.log(user.address.city); // "Izmir"
console.log(deep.address.city); // "Ankara"
A small detail: structuredClone is not part of ECMAScript but of the web platform. It comes from the APIs the browser provides rather than from the language itself, and it has been available in all major browsers since 2022. It correctly copies built-in types like Date, Map and Set, and objects that reference themselves. It has limits too:
const withMethod = { name: "Ada", greet: function () { return "Hello"; } };
try {
structuredClone(withMethod);
} catch (error) {
console.log(error.name); // "DataCloneError"
}
It can't copy an object that contains a function, and it throws an error. It can't copy DOM elements either. When copying objects created from classes, it loses the class information and returns a plain object; we'll see classes in the Intermediate series. For objects made of plain data, though, this is the cleanest way to take a deep copy. In the next post we'll also see the JSON way of copying and talk about why structuredClone is the better choice.
Common Pitfalls
Assignment doesn't copy, and === doesn't look at contents
const original = { score: 10 };
const alias = original;
alias.score = 99;
console.log(original.score); // 99
console.log(alias === original); // true
console.log({ a: 1 } === { a: 1 }); // false
Same rule as with arrays. alias = original doesn't create a new object; it gives the same object a second name. === doesn't compare the contents of two objects; it checks whether they're the same object. That's why two separate object literals with exactly the same contents aren't considered equal.
const doesn't freeze an object
When you write const user = {...}, you can't assign a different object to user, but you can change, add and delete the properties inside it. The difference between reassignment and mutation that we saw in the post on variables applies here too. We'll see ways to make an object truly unchangeable in the Advanced series.
Looking inside an object that isn't there
const user = { name: "Ada" };
console.log(user.address); // undefined
try {
console.log(user.address.city);
} catch (error) {
console.log(error.name); // "TypeError"
}
Reading a property that doesn't exist gives undefined, no problem there. But if you try to look for something inside undefined, you get a TypeError. This happens a lot with incomplete data coming from a server. You can check first with an if; we'll see the ?. operator, which does this safely in one line, in the Intermediate series.
A Small Experiment
★☆☆ Create a me object that describes you: name, city, birth year and an array of your three favorite programming topics. Then add a job property, delete the birth year, and print the object to the console after each step.
★★☆ Predict before running: given const a = { x: 1, y: { z: 2 } }; and const b = { ...a };, after b.x = 5; b.y.z = 9; runs, what are a.x, a.y.z and a.y === b.y? Try the same with structuredClone and explain the difference.
★★☆ Walk through const stock = { pen: 40, notebook: 0, eraser: 12, ruler: 0 }; with Object.entries and a loop; collect the names of the out-of-stock products in an array and print them with join(", ").
★★★ Write a cart object: it should have an items array, an add method that adds a product, and a getTotal method that returns the total amount. Use this.items inside the methods. Add two products and print the total.
Objects are now in your hands. But what if you want to send this object to a server, store it in the browser, or share it with another application? In the next post we move on to JSON, the common language of data on the web: turning an object into text, turning text back into an object, and what silently disappears along the way.