Appearance
Objects, Arrays, Maps, and Sets
Most JavaScript programs spend more time arranging values than inventing algorithms. Data arrives as objects and arrays, gets validated and transformed, is indexed in maps, deduplicated in sets, and eventually becomes another object suitable for an API, file, or user interface.
JavaScript's collection types overlap, but they do not have identical jobs. Choosing well makes code simpler and makes ownership and mutation easier to understand.
Plain objects are records with dynamic keys
An object literal creates a new object:
js
const transaction = {
id: "txn-1042",
category: "travel",
amount: 18.75,
approved: false,
};Property names written directly in a literal are normally strings. Read and write a known property with dot notation:
js
transaction.category; // "travel"
transaction.approved = true;Use bracket notation when the key is computed:
js
const selectedField = "amount";
transaction[selectedField]; // 18.75Brackets also support names that are not identifier syntax:
js
const headers = {
"content-type": "application/json",
};
headers["content-type"];Property shorthand uses a variable's name as the key:
js
const id = "txn-1042";
const amount = 18.75;
const transaction = { id, amount };
// Equivalent to { id: id, amount: amount }Computed property syntax creates a key from an expression:
js
const category = "travel";
const totals = {
[category]: 18.75,
};Plain objects are a natural representation for a record whose fields have domain meaning. They can also act as string-keyed dictionaries, but Map often fits that second role better.
Property presence is separate from property value
Reading a missing property yields undefined:
js
const user = { name: "Avery" };
user.nickname; // undefinedThat does not tell you whether the property is absent or explicitly contains undefined:
js
Object.hasOwn(user, "nickname"); // false
const other = { nickname: undefined };
Object.hasOwn(other, "nickname"); // trueObject.hasOwn checks an object's own properties rather than walking its prototype chain. That makes it safer and more direct than calling object.hasOwnProperty(...), especially with untrusted objects or objects created without the usual prototype.
The in operator checks the whole prototype chain:
js
"toString" in {}; // true
Object.hasOwn({}, "toString"); // falseUse presence checks only when presence itself matters. Most record code can read the property and validate its value.
Destructuring binds properties to local names
Object destructuring extracts fields:
js
const { id, amount } = transaction;It can rename a field and provide a default for undefined:
js
const { category: group, approved = false } = transaction;The default does not apply to null:
js
const { label = "Untitled" } = { label: null };
console.log(label); // nullDestructuring is common in function parameters:
js
function formatTransaction({ id, category, amount }) {
return `${id}: ${category} — $${amount.toFixed(2)}`;
}This is concise when the required shape is clear. It can make boundary errors less obvious because destructuring null or undefined throws before the function body runs. Validate genuinely untrusted input before treating it as a record.
Rest properties gather remaining own enumerable properties into a new object:
js
const { password, ...safeUser } = input;That can be useful, but “copy everything except these fields” is risky at security boundaries. Selecting an explicit allowlist is safer when new source fields must not leak automatically.
Spread copies a surface, not an object graph
Object spread creates a new object and copies own enumerable properties:
js
const updated = {
...transaction,
approved: true,
};Later properties win, so this is a common immutable-update shape. The copy is shallow:
js
const original = {
id: "txn-1042",
metadata: {
source: "card",
},
};
const copy = { ...original };
copy.metadata.source = "cash";
console.log(original.metadata.source); // "cash"copy is a new outer object, but both outer objects point to the same nested metadata object.
Copy the changed path when independent nested state is required:
js
const updated = {
...original,
metadata: {
...original.metadata,
source: "cash",
},
};Do not deep-copy reflexively. Sharing immutable nested data is efficient and safe. The important question is which code owns the right to mutate a particular object.
structuredClone can deep-clone many built-in data types and handle cycles, but it is still not a universal semantic copy. It does not preserve custom class behavior in the way application code may expect, cannot clone functions, and may copy far more than an update requires.
JSON serialization is not a deep-copy tool. It loses undefined, symbols, bigints, maps, sets, dates as dates, and other JavaScript-specific values, and it fails on cycles.
Arrays are ordered collections
Arrays use zero-based numeric indexes and have a mutable length:
js
const categories = ["food", "travel", "software"];
categories[0]; // "food"
categories.length; // 3They are objects with array-specific behavior:
js
typeof categories; // "object"
Array.isArray(categories); // trueCommon mutating methods include:
js
const values = [2, 3];
values.push(4); // [2, 3, 4]
values.pop(); // returns 4, array becomes [2, 3]
values.unshift(1); // [1, 2, 3]
values.shift(); // returns 1, array becomes [2, 3]splice removes or inserts within the existing array:
js
const values = ["a", "b", "d"];
values.splice(2, 0, "c");
console.log(values); // ["a", "b", "c", "d"]Non-mutating methods produce a new array or value:
js
const values = [3, 1, 2];
values.slice(0, 2); // [3, 1]
values.concat([4, 5]); // [3, 1, 2, 4, 5]
values.toSorted((a, b) => a - b); // [1, 2, 3]
values.toReversed(); // [2, 1, 3]Traditional sort and reverse mutate the array. The newer toSorted and toReversed variants make non-mutation explicit.
Array spread is another shallow copy:
js
const next = [...values, 4];Nested objects remain shared exactly as with object spread.
Transformations express collection intent
Array methods accept callback functions and return results based on a recognizable collection operation.
map produces one output for each input:
js
const amounts = transactions.map(transaction => transaction.amount);filter keeps elements whose predicate is truthy:
js
const approved = transactions.filter(transaction => transaction.approved);find returns the first matching element or undefined:
js
const selected = transactions.find(transaction => transaction.id === selectedId);some and every answer boolean questions:
js
const hasInvalidAmount = transactions.some(
transaction => !Number.isFinite(transaction.amount),
);
const allApproved = transactions.every(transaction => transaction.approved);flatMap maps each input to zero or more outputs and flattens one level:
js
const tags = articles.flatMap(article => article.tags);reduce carries an accumulator across the collection:
js
const total = transactions.reduce(
(sum, transaction) => sum + transaction.amount,
0,
);The initial 0 matters. Without an initial value, reduce uses the first element as the accumulator and throws for an empty array.
Do not use a method because it appears more sophisticated than a loop. This chain is concise and readable:
js
const approvedTotal = transactions
.filter(transaction => transaction.approved)
.reduce((sum, transaction) => sum + transaction.amount, 0);But a single loop may be clearer when producing several related results:
js
let approvedTotal = 0;
let rejectedCount = 0;
for (const transaction of transactions) {
if (transaction.approved) {
approvedTotal += transaction.amount;
} else {
rejectedCount += 1;
}
}Avoid using map for side effects:
js
// Misleading: the returned array is discarded.
transactions.map(transaction => console.log(transaction.id));
// Clearer.
for (const transaction of transactions) {
console.log(transaction.id);
}The method name should describe what the code is doing.
Destructuring and iteration work together
entries methods often produce key-value pairs that can be destructured:
js
for (const [index, transaction] of transactions.entries()) {
console.log(index, transaction.id);
}Array destructuring supports rest elements:
js
const [first, second, ...remaining] = values;It also works for returned tuples by convention:
js
const [key, rawValue] = line.split("=", 2);JavaScript does not enforce a fixed tuple shape at runtime. TypeScript can describe one statically later.
Sparse arrays are not arrays of explicit undefined
Arrays can contain missing indexes:
js
const sparse = [];
sparse.length = 3;
console.log(sparse); // [ <3 empty items> ] in Node's displayThat differs from:
js
const explicit = [undefined, undefined, undefined];Many array methods skip holes, while for...of produces undefined for them. Sparse arrays usually arise accidentally through index assignment, deleted elements, or array construction:
js
const values = new Array(3);
values.map(() => 1); // still sparseCreate explicit values instead:
js
const values = Array.from({ length: 3 }, () => 1);Ordinary application code should treat unexpected sparsity as a warning sign.
Map is a dictionary with arbitrary keys
A Map stores key-value entries and preserves insertion order:
js
const totals = new Map();
totals.set("food", 20);
totals.set("travel", 35);
totals.get("food"); // 20
totals.has("software"); // false
totals.size; // 2Map keys can be any value, including objects:
js
const user = { id: "u1" };
const preferences = new Map();
preferences.set(user, { theme: "dark" });
preferences.get(user); // { theme: "dark" }
preferences.get({ id: "u1" }); // undefined: different object identityMaps are directly iterable:
js
for (const [category, total] of totals) {
console.log(category, total);
}Use a map when keys are dynamic data rather than a fixed record shape, when arbitrary key types matter, or when repeated insertion, deletion, and lookup are central to the operation.
Use an object when the fields describe a known entity or when producing JSON-shaped output:
js
const report = {
generatedAt: new Date().toISOString(),
transactionCount: transactions.length,
total,
};Maps do not serialize to useful JSON automatically:
js
JSON.stringify(new Map([["food", 20]])); // "{}"Convert deliberately when needed:
js
const totalsObject = Object.fromEntries(totals);That conversion is appropriate only if the keys can be represented as object property keys without losing meaning.
Set represents unique values
A set stores each value once:
js
const categories = new Set(["food", "travel", "food"]);
console.log([...categories]); // ["food", "travel"]Membership is explicit:
js
categories.has("food"); // true
categories.add("software");
categories.delete("travel");Object values are unique by identity:
js
const records = new Set([{ id: 1 }, { id: 1 }]);
records.size; // 2To deduplicate records by ID, track the IDs or build an index:
js
const seenIds = new Set();
const unique = [];
for (const record of records) {
if (seenIds.has(record.id)) continue;
seenIds.add(record.id);
unique.push(record);
}Modern JavaScript also provides mathematical set operations in environments that support the relevant ECMAScript edition, but the basic membership model is more important than memorizing each convenience method.
Object enumeration creates bridges between records and collections
These methods expose own enumerable string-keyed properties:
js
const counts = {
food: 4,
travel: 2,
};
Object.keys(counts); // ["food", "travel"]
Object.values(counts); // [4, 2]
Object.entries(counts); // [["food", 4], ["travel", 2]]Object.fromEntries performs the inverse shape:
js
const doubled = Object.fromEntries(
Object.entries(counts).map(([category, count]) => [category, count * 2]),
);The methods do not include symbol-keyed properties, non-enumerable properties, or inherited properties. That is normally desirable for record transformation.
Property order in modern JavaScript is specified in useful ways, but an object should not replace a map merely because iteration happens to be ordered. Choose based on semantics.
Mutation is an ownership decision
JavaScript permits mutation. The engineering question is which code owns a value and whether other code can observe the change.
Local mutation can be simple and safe:
js
function groupByCategory(transactions) {
const groups = new Map();
for (const transaction of transactions) {
const group = groups.get(transaction.category) ?? [];
group.push(transaction);
groups.set(transaction.category, group);
}
return groups;
}The function mutates a map and arrays it created locally. It does not mutate the input array or transaction records. Callers cannot observe the intermediate state.
Mutation becomes risky when ownership is shared:
js
function sortByAmount(transactions) {
return transactions.sort((a, b) => a.amount - b.amount);
}This unexpectedly reorders the caller's array. A non-mutating contract can use:
js
function sortByAmount(transactions) {
return transactions.toSorted((a, b) => a.amount - b.amount);
}Do not equate “functional-looking” with “non-mutating.” A callback passed to map can still mutate each object. State the ownership policy and test it where it matters.
Objects, arrays, maps, and sets all hold values. Functions determine how those values move and how behavior is configured. The next article focuses on functions as first-class values, lexical scope, and closures—the features that give JavaScript much of its characteristic design style.