JavaScript fundamentals Foundation
Objects and JSON
Group related values into objects, read and change their properties, copy and take them apart safely, and turn them into JSON, the text format every API speaks.
What an object is
A user of your app has several pieces of information: a name, an e-mail address, an age. You could keep them in three separate variables, but they belong together. An object keeps related values together under one name.
Each piece of an object is a property: a name (also called a key), a colon, and a value. The value can be anything: a string, a number, a boolean, an array, even another object.
const user = {
name: "Ada",
email: "ada@example.com",
age: 36,
active: true,
skills: ["math", "writing"],
};
console.log(user);
console.log(typeof user);
node object.js and of the browser terminal{
name: 'Ada',
email: 'ada@example.com',
age: 36,
active: true,
skills: [ 'math', 'writing' ]
}
objectThe object is too long for one line, so Node.js prints one property per line. It shows strings with single quotes. That is only how Node.js displays them: the value is still the text Ada.
You already know arrays from the arrays lesson. Use an array when you have a list of similar things in order. Use an object when you have one thing with named parts.
Methods and nested objects
A property whose value is a function is called a method. A property whose value is another object makes a nested object. Real data is full of both:
const user = {
name: "Ada",
address: {
city: "London",
country: "UK",
},
greet() {
return "Hello, I am " + this.name;
},
};
console.log(user.address);
console.log(user.address.city);
console.log(user.greet());
node nested.js and of the browser terminal{ city: 'London', country: 'UK' }
London
Hello, I am Adauser.address.cityreads from left to right: takeuser, then itsaddress, then that object'scity.greet() { ... }is the short way to write a method. You call it with brackets:user.greet().- Inside a method,
thismeans "the object the method was called on", hereuser. The lesson this, prototypes and classes explainsthisfully.
Reading properties
There are two ways to read a property:
- The dot:
user.name. Use it when you know the name while writing the code. - Square brackets with a string:
user["name"]. Use it when the name is stored in a variable, or is not a valid variable name (for example it contains a dash).
const user = { name: "Ada", "last-login": "2026-09-01", age: 36 };
console.log(user.name);
console.log(user["last-login"]);
const field = "age";
console.log(user[field]);
console.log(user.phone);
console.log("phone" in user, "age" in user);
node read.js and of the browser terminalAda 2026-09-01 36 undefined false true
Reading a property that does not exist gives undefined. It does not crash. That is convenient, and it is also how typos slip through: user.nmae is just undefined. The in operator answers the question "does this object have a property with this name?".
Reading a property of undefined does crash. If a user has no address, then user.address.city fails with TypeError: Cannot read properties of undefined (reading 'city'). You will learn a safe way to write it, user.address?.city, in Modern JavaScript.
Updating, adding and deleting
Assign to a property to change it. Assign to a name that does not exist yet to add it. Use delete to remove a property completely:
const user = { name: "Ada", email: "ada@example.com", age: 36 };
user.age = 37;
user.city = "London";
delete user.email;
console.log(user);
console.log(user.email);
node change.js and of the browser terminal{ name: 'Ada', age: 37, city: 'London' }
undefinedThis works even though user is a const. const only stops the name user from pointing at a different object. It does not freeze the object itself. If you want an object that nobody can change, call Object.freeze(user).
Objects are shared, not copied
This is the most important idea in the lesson. A variable does not hold the object itself. It holds a reference: an arrow pointing at the object. When you assign an object to a second variable, you copy the arrow, not the object:
const user = { name: "Ada", age: 36 };
const sameUser = user;
sameUser.age = 99;
console.log(user.age);
console.log(user === sameUser);
console.log({ name: "Ada" } === { name: "Ada" });
node shared.js and of the browser terminal99 true false
Changing sameUser changed user, because both names point at the same object. And === on objects asks "is this the same object?", not "do they look the same?". Two objects written separately are never equal, even with identical contents.
The same happens when you pass an object to a function: the function receives the arrow, so it can change the caller's object. Many bugs in real backends come from a function quietly changing an object that someone else is still using. The next sections show how to make real copies.
Shorthand and computed properties
Two short ways to build objects appear in almost every JavaScript file:
- Shorthand: when the property name and the variable name are the same, write the name once.
{ name }means{ name: name }. - Computed property: put an expression in square brackets to decide the property's name when the code runs.
{ [field]: value }uses the value offieldas the name.
const name = "Ada";
const email = "ada@example.com";
const user = { name, email };
console.log(user);
function setting(key, value) {
return { [key]: value, updatedBy: name };
}
console.log(setting("theme", "dark"));
console.log(setting("language", "en"));
node shorthand.js and of the browser terminal{ name: 'Ada', email: 'ada@example.com' }
{ theme: 'dark', updatedBy: 'Ada' }
{ language: 'en', updatedBy: 'Ada' }The same function made an object with a theme property and one with a language property, because the name came from the key argument.
Looping over an object
An object is not a list, so it has no map or forEach. Instead, the built-in Object helpers turn it into arrays you already know how to use:
Object.keys(obj): an array of the property names.Object.values(obj): an array of the values.Object.entries(obj): an array of[name, value]pairs.Object.fromEntries(pairs): the reverse, pairs back into an object.
const settings = { theme: "dark", language: "en", pageSize: 20 };
console.log(Object.keys(settings));
console.log(Object.values(settings));
for (const [key, value] of Object.entries(settings)) {
console.log(`${key} = ${value}`);
}
const onlyText = Object.fromEntries(
Object.entries(settings).filter(([, value]) => typeof value === "string"),
);
console.log(onlyText);
node entries.js and of the browser terminal[ 'theme', 'language', 'pageSize' ]
[ 'dark', 'en', 20 ]
theme = dark
language = en
pageSize = 20
{ theme: 'dark', language: 'en' }const [key, value] takes each pair apart into two variables. That is array destructuring, which you will see more of in a moment. The last part is a pattern worth remembering: entries, then an array method such as filter or map, then fromEntries. It lets you transform an object with the array tools from the arrays lesson.
Copying and merging: spread and Object.assign
The spread syntax ... inside { } copies every property of an object into a new object. Properties written after it replace the copied ones. That makes it the standard way to "change one field" without touching the original:
const defaults = { theme: "light", language: "en", pageSize: 20 };
const chosen = { theme: "dark" };
const settings = { ...defaults, ...chosen };
console.log(settings);
console.log(defaults);
const merged = Object.assign({}, defaults, { pageSize: 50 });
console.log(merged);
node spread.js and of the browser terminal{ theme: 'dark', language: 'en', pageSize: 20 }
{ theme: 'light', language: 'en', pageSize: 20 }
{ theme: 'light', language: 'en', pageSize: 50 }When two objects have the same property, the one that comes later wins. Object.assign(target, ...sources) does the same thing, but it writes into its first argument. Passing a fresh {} first keeps defaults untouched. In new code, spread is more common.
Spread copies only one level
Spread makes a shallow copy: the top-level properties are new, but a nested object is still shared. For a full, deep copy, use structuredClone:
const user = { name: "Ada", address: { city: "London" } };
const shallow = { ...user };
shallow.address.city = "Paris";
console.log(user.address.city);
const deep = structuredClone(user);
deep.address.city = "Rome";
console.log(user.address.city, deep.address.city);
node deep.js and of the browser terminalParis Paris Rome
Changing the shallow copy's address changed the original's too, because both share one address object. The deep copy has its own.
Taking objects apart: destructuring
Destructuring pulls properties out into variables in one line. Write the property names inside { } on the left of =. Add ...rest at the end to collect every property you did not name into a new object:
const user = {
id: 7,
name: "Ada",
email: "ada@example.com",
passwordHash: "<hash>",
};
const { name, email } = user;
console.log(name, email);
const { passwordHash, ...publicUser } = user;
console.log(publicUser);
node destructure.js and of the browser terminalAda ada@example.com
{ id: 7, name: 'Ada', email: 'ada@example.com' }The second pattern is one a backend uses every day: it takes the fields you must never send to a client out of the object, and keeps everything else. A real app stores a password hash made by a library, never the password itself. You will do that in the authentication lesson.
Destructuring can do more: defaults, renaming, nested objects and arrays. Modern JavaScript covers all of it.
JSON: how data travels
A backend cannot send a JavaScript object over the network. It sends text. The text format almost every API uses is JSON, short for JavaScript Object Notation. It looks almost exactly like the objects you just wrote.
JSON.stringify turns a value into JSON text. JSON.parse turns JSON text back into a value:
const task = { id: 1, title: "Buy milk", done: false, tags: ["home"] };
const text = JSON.stringify(task);
console.log(text);
console.log(typeof text);
const back = JSON.parse(text);
console.log(back.title, back.tags[0]);
console.log(JSON.stringify(task, null, 2));
node json.js and of the browser terminal{"id":1,"title":"Buy milk","done":false,"tags":["home"]}
string
Buy milk home
{
"id": 1,
"title": "Buy milk",
"done": false,
"tags": [
"home"
]
}JSON has a few rules that JavaScript objects do not: property names must be in double quotes, and there are no comments and no trailing commas. The extra null, 2 tells JSON.stringify to indent with two spaces, which is easier to read.
What JSON cannot carry
JSON only knows strings, numbers, booleans, null, arrays and plain objects. Everything else changes or disappears on the way:
const task = {
title: "Buy milk",
note: undefined,
due: new Date("2026-10-01T09:00:00Z"),
describe() {
return this.title;
},
};
const text = JSON.stringify(task);
console.log(text);
const back = JSON.parse(text);
console.log(typeof back.due);
node json-limits.js and of the browser terminal{"title":"Buy milk","due":"2026-10-01T09:00:00.000Z"}
stringThe undefined property and the method vanished. The date became a string, and JSON.parse does not turn it back into a date: it stays a string. When your API receives JSON, you get plain data and must check and convert it yourself.
When a client sends data to your API, it arrives as JSON text and your code calls JSON.parse on it. Text that is not valid JSON makes JSON.parse throw an error. You will learn to handle that in Handling errors.
Build: a user profile system
Time to put it together. A profile system needs three things:
createProfile(input): fill in defaults for anything the user left out.updateProfile(profile, changes): apply the changes a user sends, and return a new profile.toPublic(profile): the version that is safe to send back as JSON.
Here is a first try at updateProfile. It has a serious bug:
function updateProfile(profile, changes) {
return { ...profile, ...changes };
}
const ada = { id: 1, name: "Ada", role: "user", theme: "light" };
const changes = JSON.parse('{"theme":"dark","role":"admin"}');
const updated = updateProfile(ada, changes);
console.log(updated);
node profile-bug.js and of the browser terminal{ id: 1, name: 'Ada', role: 'admin', theme: 'dark' }Insecure on purpose
The user asked to change their theme, and also sent"role":"admin". Spreading everything the client sent made them an administrator. This is a real class of security bug, called mass assignment. Never copy outside input into a stored object as it is.The fix is an allow-list: a fixed list of fields a user may change. Anything else is ignored. Here is the whole system, with the fix:
const DEFAULTS = { theme: "light", language: "en", bio: "" };
const EDITABLE = ["name", "theme", "language", "bio"];
let nextId = 1;
function createProfile(input) {
const profile = { ...DEFAULTS, id: nextId, role: "user", name: input.name };
nextId += 1;
return updateProfile(profile, input);
}
function updateProfile(profile, changes) {
const allowed = Object.entries(changes).filter(([key]) => EDITABLE.includes(key));
return { ...profile, ...Object.fromEntries(allowed) };
}
function toPublic(profile) {
const { role, ...rest } = profile;
return { ...rest, isAdmin: role === "admin" };
}
const ada = createProfile({ name: "Ada", theme: "dark", role: "admin" });
console.log(ada);
const changed = updateProfile(ada, JSON.parse('{"bio":"Math fan","id":99}'));
console.log(changed);
console.log(ada.bio === "");
console.log(JSON.stringify(toPublic(changed)));
node profiles.js and of the browser terminal{
theme: 'dark',
language: 'en',
bio: '',
id: 1,
role: 'user',
name: 'Ada'
}
{
theme: 'dark',
language: 'en',
bio: 'Math fan',
id: 1,
role: 'user',
name: 'Ada'
}
true
{"theme":"dark","language":"en","bio":"Math fan","id":1,"name":"Ada","isAdmin":false}Read the output line by line:
createProfilekepttheme: 'dark'but ignoredrole: 'admin'. The role is always set by the server, never by the user.updateProfileaccepted the newbiobut ignored the attempt to changeid.ada.biois still empty: every function returned a new object and never changed its input.toPublicused destructuring with...restto droprole, and added a computedisAdminfield instead.
Later in the course, @zudojs/schema and @zudojs/validation check incoming data for you. The idea stays the same: decide which fields you accept, and ignore or reject the rest.
Practice
TRY IT YOURSELF
Count the filled-in fields
Write filledFields(profile) that returns how many properties have a value that is not an empty string. Use Object.values.
Show a solution
function filledFields(profile) {
return Object.values(profile).filter((value) => value !== "").length;
}
console.log(filledFields({ name: "Ada", bio: "", city: "London" }));
console.log(filledFields({ name: "", bio: "" }));
node filled.js and of the browser terminal2 0
TRY IT YOURSELF
Rename keys for an old client
An old mobile app expects user_name instead of name, and user_email instead of email. Write withPrefix(obj, prefix) that returns a new object where every key starts with the prefix. Use Object.entries, map and Object.fromEntries.
Show a solution
function withPrefix(obj, prefix) {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [prefix + key, value]),
);
}
const user = { name: "Ada", email: "ada@example.com" };
console.log(withPrefix(user, "user_"));
console.log(user);
node prefix.js and of the browser terminal{ user_name: 'Ada', user_email: 'ada@example.com' }
{ name: 'Ada', email: 'ada@example.com' }TRY IT YOURSELF
Find the shared object
This code tries to give each new user their own copy of the default settings, but changing Grace's settings also changes Ada's. Explain why, and fix it.
const defaultSettings = { theme: "light", alerts: { email: true } };
const ada = { name: "Ada", settings: { ...defaultSettings } };
const grace = { name: "Grace", settings: { ...defaultSettings } };
grace.settings.alerts.email = false;
console.log(ada.settings.alerts.email);
node shared-bug.js and of the browser terminalfalse
Show a solution
Spread copies only one level. Both users got their own settings object, but both of those point at the same alerts object. Use structuredClone to copy every level:
const defaultSettings = { theme: "light", alerts: { email: true } };
const ada = { name: "Ada", settings: structuredClone(defaultSettings) };
const grace = { name: "Grace", settings: structuredClone(defaultSettings) };
grace.settings.alerts.email = false;
console.log(ada.settings.alerts.email, grace.settings.alerts.email);
node shared-fix.js and of the browser terminaltrue false
Recap
- An object groups named values. A property can hold any value, including a function (a method) or another object.
- Read with
obj.nameorobj[expression]. Assign to change or add a property, anddeleteto remove one. - Variables hold references. Two names can point at one object, and
===compares identity, not contents. { name }is shorthand,{ [key]: value }is a computed property.Object.keys,values,entriesandfromEntrieslet you use array tools on objects.{ ...a, ...b }copies and merges one level deep.structuredClonecopies every level.- Never spread outside input into stored data: keep an allow-list of the fields a user may change.
- JSON is the text format APIs use.
JSON.stringifywrites it andJSON.parsereads it. Dates become strings, and functions andundefineddisappear.
Test yourself
Five questions, picked at random from this lesson's question bank. Some ask you to choose an answer, some to predict what code prints, and some to write code and run it in the terminal. Get 4 of 5 right to pass. If you don't, read the explanations and try again: you get 5 different questions.