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.

A shallow copy. The variables original and copy each point to their own outer object with id MTR-0042, but the address property of both outer objects points to one shared nested object with city Delft. original copy id: "MTR-0042" address: ● id: "MTR-0042" address: ● city: "Delft" shared by both
After 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.