Objects and JSON
Objects as collections of named properties: creating, reading and updating them, destructuring, copying shallow and deep, Map and Set, and converting to and from JSON.
Object literals
Arrays keep values in order by position; objects keep them by name.
The quickest way to make one is an
object literal: curly braces
around a comma-separated list of key: value pairs.
const meter = {
id: "MTR-0042",
city: "Delft",
readingKwh: 415.8,
smart: true,
"install date": "2024-03-12", // a key with a space needs quotes
};
console.log(meter);
console.log(typeof meter);
{
id: 'MTR-0042',
city: 'Delft',
readingKwh: 415.8,
smart: true,
'install date': '2024-03-12'
}
object
Each pair is a property: a key (the property name) with a value. Keys are strings (or symbols), even when you write them without quotes; values can be anything, including arrays, other objects and functions. A property whose value is a function is called a method.
Two shortcuts are common in modern code. With
shorthand properties, { city } means
{ city: city }. With computed keys,
[expression]: value uses the value of an expression as
the key.
const city = "Leiden";
const field = "outageMinutes";
const ticket = {
id: "INC-20482",
city, // shorthand for city: city
[field]: 40, // computed key
summary() { // method shorthand
return `${this.id} in ${this.city}: ${this[field]} min`;
},
};
console.log(ticket.outageMinutes);
console.log(ticket.summary());
40 INC-20482 in Leiden: 40 min
Reading properties
There are two notations.
Dot notation (meter.city) is shorter
and works when the key is a valid identifier.
Bracket notation (meter["install date"]) takes any string expression, so it is also what you use when the
key is in a variable.
const meter = { id: "MTR-0042", city: "Delft", "install date": "2024-03-12" };
const key = "city";
console.log(meter.city);
console.log(meter["install date"]);
console.log(meter[key]); // the value of key, "city"
console.log(meter.key); // a property literally named "key"
console.log(meter.owner); // missing property
Delft 2024-03-12 Delft undefined undefined
Nested objects and missing values
Objects can contain objects. Chaining the access works as long as
every step exists. Reading a missing property gives
undefined, but reading a property of
undefined is an error. The optional chaining operator
?. (see
Operators, Comparison and Type Coercion) stops and returns undefined instead, and
?? supplies a fallback.
const customer = {
name: "R. Jansen",
address: { street: "Oude Delft 12", city: "Delft" },
};
console.log(customer.address.city);
console.log(customer.billing?.iban ?? "no IBAN on file");
console.log(customer.billing.iban);
Delft no IBAN on file TypeError: Cannot read properties of undefined (reading 'iban')
Adding, changing and removing properties
Assigning to a property creates it if it does not exist and replaces
its value if it does. The delete operator removes a
property. Two ways to ask whether a property exists:
Object.hasOwn(obj, key) checks the object itself;
key in obj also finds inherited properties.
const ticket = { id: "INC-20482", status: "open" };
ticket.status = "resolved"; // change
ticket.resolvedBy = "Mei Chen"; // add
console.log(ticket);
console.log(delete ticket.resolvedBy);
console.log(ticket);
console.log(Object.hasOwn(ticket, "status"), "status" in ticket);
console.log(Object.hasOwn(ticket, "toString"), "toString" in ticket);
console.log(ticket.priority !== undefined);
{ id: 'INC-20482', status: 'resolved', resolvedBy: 'Mei Chen' }
true
{ id: 'INC-20482', status: 'resolved' }
true true
false true
false
Testing obj.key !== undefined is common and usually
fine, but it cannot tell "missing" from "present with the value
undefined". Object.hasOwn can.
As with arrays, const fixes the variable, not the
object: the properties of a const object can still
change. Object.freeze(obj) makes an object read-only,
but only at the top level — nested objects stay changeable.
Iterating over objects
Three static methods turn an object into an array you can loop over
or transform with the array methods from
Arrays: Object.keys(obj),
Object.values(obj) and
Object.entries(obj) (pairs of
[key, value]). Object.fromEntries() goes
the other way, from pairs back to an object.
const usageKwh = { Delft: 1240, Leiden: 980, Rotterdam: 2310 };
console.log(Object.keys(usageKwh));
console.log(Object.values(usageKwh));
for (const [city, kwh] of Object.entries(usageKwh)) {
console.log(`${city.padEnd(10)} ${kwh} kWh`);
}
// Transform: pairs → map → back to an object
const inMwh = Object.fromEntries(
Object.entries(usageKwh).map(([city, kwh]) => [city, kwh / 1000]),
);
console.log(inMwh);
[ 'Delft', 'Leiden', 'Rotterdam' ]
[ 1240, 980, 2310 ]
Delft 1240 kWh
Leiden 980 kWh
Rotterdam 2310 kWh
{ Delft: 1.24, Leiden: 0.98, Rotterdam: 2.31 }
The for...in loop (see
Control Flow and Loops)
also visits keys, but it includes inherited enumerable properties as
well; Object.entries with for...of is the
safer habit.
Property order is defined: keys that are integer indexes such as
"2" or "10" (but not "01" or
"-1") come first, in ascending numeric order; all other
string keys follow in the order they were added.
const order = { b: 1, 10: "ten", a: 2, 2: "two" };
console.log(Object.keys(order));
[ '2', '10', 'b', 'a' ]
Destructuring and spread
Destructuring works on
objects too. The pattern names the properties to unpack; the
variables get the same names unless you rename them with
key: newName. Defaults fill in missing properties.
const ticket = {
id: "INC-20482",
city: "Delft",
assignee: { name: "Tomasz Nowak", team: "NOC" },
};
const { id, city: location, priority = 3 } = ticket;
console.log(id, location, priority);
const { assignee: { team } } = ticket; // nested
console.log(team);
const { id: _, ...details } = ticket; // everything except id
console.log(details);
// Destructuring in a parameter list: named options with defaults
function formatReading({ meter, kwh, unit = "kWh" }) {
return `${meter}: ${kwh} ${unit}`;
}
console.log(formatReading({ meter: "MTR-0042", kwh: 415.8 }));
INC-20482 Delft 3
NOC
{ city: 'Delft', assignee: { name: 'Tomasz Nowak', team: 'NOC' } }
MTR-0042: 415.8 kWh
The spread syntax in an
object literal, { ...obj }, copies the object's own
properties into a new object. Later properties win, which makes it
the usual way to apply settings on top of defaults:
const defaults = { unit: "kWh", decimals: 1, showCity: true };
const userSettings = { decimals: 2 };
const settings = { ...defaults, ...userSettings };
console.log(settings);
const updated = { ...settings, showCity: false }; // a changed copy
console.log(updated.showCity, settings.showCity);
{ unit: 'kWh', decimals: 2, showCity: true }
false true
Copying objects
A variable holds a
reference
to an object. Assigning it to another variable copies the reference,
not the object, and === compares references: two
separately created objects are never equal, even with identical
properties.
{ ...obj } and Object.assign({}, obj)
make a
shallow copy: a new outer object whose property values are copied as they are.
If a value is itself an object, the copy gets a reference to the
same nested object.
const copy = { ...original } there are two
outer objects, but only one address object.
const original = { id: "MTR-0042", address: { city: "Delft" } };
const copy = { ...original };
copy.id = "MTR-0043"; // top level: only the copy changes
copy.address.city = "Leiden"; // nested: shared!
console.log(original);
console.log(copy);
console.log(copy.address === original.address);
{ id: 'MTR-0042', address: { city: 'Leiden' } }
{ id: 'MTR-0043', address: { city: 'Leiden' } }
true
Deep copies with structuredClone
A deep copy copies an object and,
recursively, every object inside it, so the copy shares nothing with
the original. The built-in way is
structuredClone(), a global function in browsers and in Node.js (since version 17).
It is defined in the HTML standard rather than in ECMAScript. It
copies plain objects, arrays, Dates, Maps, Sets and more, and keeps
shared and circular references intact.
const original = {
id: "MTR-0042",
address: { city: "Delft" },
installed: new Date("2024-03-12T00:00:00Z"),
tags: new Set(["smart", "3-phase"]),
};
const deep = structuredClone(original);
deep.address.city = "Leiden";
console.log(original.address.city, deep.address.city);
console.log(deep.installed instanceof Date, deep.tags.has("smart"));
Delft Leiden true true
It cannot copy functions (it throws a
DataCloneError), and class instances come back as plain
objects without their methods. An older trick,
JSON.parse(JSON.stringify(obj)), also makes a deep
copy, but it loses more: Dates become strings, Maps and Sets become
empty objects, and undefined values disappear (see
below).
Map and Set
Map
A Map is a built-in collection of
key-value pairs, like an object, but designed for lookups. Keys can
be of any type (numbers, objects), the entries keep their insertion
order, size tells how many there are, and a new Map
contains no inherited keys at all. Engines implement it as a
hash table, so get, set and has stay
fast even for many entries.
const outagesPerStation = new Map();
const reports = ["Delft-Noord", "Leiden-Oost", "Delft-Noord", "Delft-Noord"];
for (const station of reports) {
outagesPerStation.set(station, (outagesPerStation.get(station) ?? 0) + 1);
}
console.log(outagesPerStation);
console.log(outagesPerStation.get("Delft-Noord"), outagesPerStation.size);
console.log(outagesPerStation.has("Zwolle"));
for (const [station, count] of outagesPerStation) {
console.log(station, count);
}
const byKwh = new Map([[1240, "Delft"], [980, "Leiden"]]); // number keys
console.log(byKwh.get(980));
Map(2) { 'Delft-Noord' => 3, 'Leiden-Oost' => 1 }
3 2
false
Delft-Noord 3
Leiden-Oost 1
Leiden
Use a plain object for a record with fixed, known property names (a
ticket, a meter); use a Map for a lookup table whose keys are data.
Object keys in a Map are compared by reference, not by contents.
Convert between the two with
new Map(Object.entries(obj)) and
Object.fromEntries(map).
Set
A Set holds unique values: adding a value that is
already there does nothing. It is the JavaScript version of a
set, and the
shortest way to remove duplicates from an array.
const visited = new Set(["Delft", "Leiden"]);
visited.add("Delft"); // already there: no change
visited.add("Zwolle");
console.log(visited, visited.size);
console.log(visited.has("Leiden"));
const cities = ["Delft", "Leiden", "Delft", "Assen", "Leiden"];
console.log([...new Set(cities)]);
Set(3) { 'Delft', 'Leiden', 'Zwolle' } 3
true
[ 'Delft', 'Leiden', 'Assen' ]
Set operations such as union,
intersection and difference became methods
in ES2025 (Node.js 22 and later, current browsers):
// Node.js 22+ / current browsers — not available in Node.js 20
const north = new Set(["Assen", "Zwolle", "Groningen"]);
const visited = new Set(["Zwolle", "Delft"]);
console.log(north.intersection(visited)); // Set(1) { 'Zwolle' }
JSON
JSON (JavaScript Object Notation) is a text format for data, based on JavaScript's literal syntax but stricter: keys must be in double quotes, strings too, and there are no comments, functions or trailing commas. The format itself and its pitfalls are explained in JSON and Other Data Formats; this section is about the two JavaScript functions that read and write it.
JSON.stringify
JSON.stringify(value)
converts a value into a JSON string — this is
serialization. By default the result is compact; a third argument (a number of
spaces) indents it for people to read. The second argument, the
replacer, can be an array of the keys to keep.
const ticket = {
id: "INC-20482",
city: "Delft",
minutes: 40,
tags: ["outage", "lv-grid"],
internalNote: "check cable joint",
};
console.log(JSON.stringify(ticket));
console.log(JSON.stringify(ticket, ["id", "minutes"]));
console.log(JSON.stringify({ id: ticket.id, tags: ticket.tags }, null, 2));
{"id":"INC-20482","city":"Delft","minutes":40,"tags":["outage","lv-grid"],"internalNote":"check cable joint"}
{"id":"INC-20482","minutes":40}
{
"id": "INC-20482",
"tags": [
"outage",
"lv-grid"
]
}
What does not survive
JSON has only strings, numbers, booleans, null, arrays
and objects. Everything else is converted or left out, silently:
const data = {
reading: 415.8,
note: undefined, // key dropped
format: () => "kWh", // key dropped
ratio: NaN, // becomes null
read: new Date("2026-10-01T06:00:00Z"), // becomes an ISO string
flags: new Map([["smart", true]]), // becomes {}
list: [undefined, 1], // undefined in arrays: null
};
console.log(JSON.stringify(data));
{"reading":415.8,"ratio":null,"read":"2026-10-01T06:00:00.000Z","flags":{},"list":[null,1]}
A BigInt makes JSON.stringify throw a
TypeError. Objects can control their own JSON form with
a toJSON() method; that is how a Date turns into its
ISO string. To keep a Map, convert it first:
Object.fromEntries(map).
JSON.parse
JSON.parse(text)
reads a JSON string and builds the corresponding JavaScript value —
this is
parsing. It only accepts valid JSON and throws a
SyntaxError for anything else, so wrap it in
try...catch when the text comes from outside (see
Error Handling).
const body = '{"meter":"MTR-0042","kwh":415.8,"read":"2026-10-01T06:00:00.000Z"}';
const reading = JSON.parse(body);
console.log(reading.kwh + 1);
console.log(typeof reading.read); // still a string, not a Date
// A reviver function can convert values while parsing
const withDate = JSON.parse(body, (key, value) =>
key === "read" ? new Date(value) : value,
);
console.log(withDate.read.getUTCHours());
for (const text of ['{"kwh": 415.8}', "{'kwh': 415.8}"]) {
try {
console.log(JSON.parse(text));
} catch (error) {
console.log(`Invalid JSON: ${error.name}`);
}
}
416.8
string
6
{ kwh: 415.8 }
Invalid JSON: SyntaxError
JSON.parse only builds data; it never runs code, so it
is safe to use on untrusted text. (Very old code sometimes used
eval() to read JSON — never do that.) What the parsed
data contains is still untrusted: check the fields you need
before you use them.