Objects, Arrays, and Destructuring in JavaScript
JavaScript objects store named properties, arrays store ordered values, and destructuring assigns their contents to variables. If a variable is a luggage tag, an object is a labeled shelf and an array is a numbered row of pigeonholes. Assigning one to a second variable ties a second tag to the same shelf rather than building a new one. That sharing is the difference between code that works and code that changes something across the room.
These examples build on JavaScript language basics, especially values and references.
What Is a JavaScript Object Literal?
An object literal is a set of named slots written in braces. Values can be any type, including other objects and functions:
const product = {
name: "Desk lamp",
"list price": 49.5,
stock: { warehouse: 3 },
};
console.log(product.name);
console.log(product.stock.warehouse); // node output:
// Desk lamp
// 3 Keys are strings or symbols, and anything else you write as a key is converted, normally to a string. Use quotes for property names containing spaces or hyphens, as with "list price". API responses frequently use keys with hyphens or spaces, and those keys need bracket access rather than a dot.
Two shorthands appear constantly in modern code. When a variable name matches the key you want, { name } means { name: name }. Methods can drop the function keyword, so { save() { } } is a method named save.
How Do You Read JavaScript Object Properties?
Dot access uses a fixed name written into the code. Bracket access takes any expression, which is what you need when the key is in a variable:
const product = { name: "Desk lamp", "list price": 49.5 };
const field = "name";
console.log(product[field]);
console.log(product["list price"]);
console.log(product.colour); // node output:
// Desk lamp
// 49.5
// undefined The last line is the behavior that defines JavaScript objects. When a lookup finds nothing on the object or anything it inherits from, it returns undefined rather than raising an error, so a typo in a property name fails silently at the point it is made and loudly somewhere else entirely.
That is what makes ?. from the values and equality guide so useful on nested data. Reading product.stock.warehouse when stock is missing throws immediately, while product.stock?.warehouse evaluates to undefined and lets you supply a fallback.
Objects are not arrays, so they have no map or filter of their own. Three built-in helpers convert an object into something you can loop over:
const product = { name: "Desk lamp", price: 49.5, stock: 3 };
console.log(Object.keys(product));
console.log(Object.values(product));
for (const [key, value] of Object.entries(product)) {
console.log(key, "=", value);
} // node output:
// [ 'name', 'price', 'stock' ]
// [ 'Desk lamp', 49.5, 3 ]
// name = Desk lamp
// price = 49.5
// stock = 3 Object.entries is the one you reach for most, because it hands you a key and a value together and everything you know about array methods then applies. Object.fromEntries goes the other way, turning a list of pairs back into an object after you have filtered or transformed it.
Which JavaScript Array Methods Should You Learn First?
Start with map() to transform items, filter() to select them, reduce() to calculate a result and find() to locate a match. Each takes a callback:
const orders = [
{ id: "A1", total: 40, status: "paid" },
{ id: "A2", total: 12, status: "refunded" },
{ id: "A3", total: 95, status: "paid" },
];
console.log(orders.filter((order) => order.status === "paid").map((order) => order.id));
console.log(orders.reduce((sum, order) => sum + order.total, 0));
console.log(orders.find((order) => order.total > 500)); // node output:
// [ 'A1', 'A3' ]
// 147
// undefined filter keeps the elements whose callback returns something truthy and always returns an array. map transforms every element and returns an array of the same length. reduce folds the whole list into one value, starting from the second argument, which is 0 here.
find returns the first matching element, or undefined when nothing matches, as the last line shows. That undefined is the origin of a great many "cannot read properties of undefined" errors, because the next line usually reads a property of the result.
Two more are worth adding early. some and every answer yes-or-no questions about a list without building anything, and forEach runs a callback purely for its side effects and returns undefined. Reaching for forEach when you meant map is why some code ends up pushing into an array by hand.
For plain membership there is no callback at all. list.includes(value) returns a boolean, and list.indexOf(value) returns the position or -1 when the value is absent. Older code tends to use indexOf with a !== -1 check, which includes replaced.
Because filter and map both return arrays, they chain. The first line in the example above reads as one sentence, left to right: keep the paid orders, then take their ids. Chaining stays readable for two or three steps, and a chain longer than that usually wants intermediate variables with names.
How Does JavaScript Destructuring Work?
Destructuring pulls values out of an object or array into named variables in one line. Object patterns select properties by name, while array patterns select values by position:
const order = { id: "A1", total: 40, status: "paid" };
const { id, total, status: state } = order;
console.log(id, total, state);
const [first, ...rest] = ["A1", "A2", "A3"];
console.log(first, rest); // node output:
// A1 40 paid
// A1 [ 'A2', 'A3' ] Object destructuring matches by key name, so order does not matter. The colon in status: state renames on the way out, which resolves collisions when two objects both have an id. Array destructuring matches by position instead.
The most useful place for it is a function signature, where it names exactly which fields the function uses and supplies defaults in the same breath:
function describe({ id, total = 0, currency = "AUD" }) {
return `${id}: ${total} ${currency}`;
}
console.log(describe({ id: "A9" })); // node output:
// A9: 0 AUD The caller passed one field and the other two came from defaults. Destructuring defaults apply to missing or undefined values, not null. This is exactly how React components receive props, so the pattern is worth being comfortable with before any framework code arrives.
Destructuring also nests, which is how people dig into an API response in one line. It is powerful and it gets unreadable quickly, so it is worth knowing how to read even if you write it sparingly:
const response = { data: { items: [{ id: "A1" }] }, meta: {} };
const { data: { items: [firstItem] }, meta: { page = 1 } } = response;
console.log(firstItem, page); // node output:
// { id: 'A1' } 1 Read it from the outside in: reach into data, then into items, then take position zero and call it firstItem. The default on page filled in because meta had no such key. A default cannot save you from a missing intermediate level, so destructuring data when data itself is absent still throws.
What Is the Difference Between Spread and Rest?
The three dots do two opposite jobs, and which one you get depends on where they sit grammatically. Spread appears in argument lists and inside array or object literals, unpacking a value into them. Rest appears in parameter lists and in binding patterns, collecting whatever is left:
const base = { theme: "dark", pageSize: 20 };
const merged = { ...base, pageSize: 50 };
console.log(merged); // node output:
// { theme: 'dark', pageSize: 50 } Later keys win, which makes this the standard way to apply an override without touching the original. The same syntax works on arrays, where [...listA, ...listB] concatenates and [...list] makes a copy.
Rest is the form you already saw in the destructuring example and in the rest parameters of the functions guide. Same three dots, opposite direction: in a pattern that receives values they gather, and in a literal or a call that produces values they scatter.
Does Copying a JavaScript Object Keep Its Data Separate?
Assignment does not copy an object, and spread makes only a shallow copy. Assigning an object hands over a reference to the shelf, not its contents. Two variables can therefore identify the same shelf, and writing through one is visible through the other, while pointing one variable somewhere else leaves the other alone. Spread makes a copy, but only of the top level:
const original = { theme: "dark", filters: { tag: "js" } };
const shallow = { ...original };
shallow.filters.tag = "css";
console.log(original.filters.tag);
const deep = structuredClone(original);
deep.filters.tag = "html";
console.log(original.filters.tag, deep.filters.tag); // node output:
// css
// css html Editing the nested object through the copy changed the original, because both copies pointed at the same nested object. structuredClone, built into modern browsers and Node, copies the whole structure, and the second edit stayed contained.
You will also meet Object.assign(target, source), which copies the same own enumerable properties one level deep. It writes into the target you pass, so Object.assign(original, patch) modifies original rather than returning something new.
The two are not interchangeable beyond that. Object.assign assigns, so it triggers any setter already on the target, while spread always creates plain data properties. Neither copies the source's prototype or its non-enumerable properties.
structuredClone is not a universal answer either. It copies plain data, arrays, dates, maps, and sets, and handles cycles, but it throws on functions. A class instance comes back as a plain object, losing its prototype, its getters, and settings such as read-only.
For plain data it is still the shortest correct option. The older JSON.parse(JSON.stringify(value)) trick silently loses dates and undefined values.
Array methods split the same way, and the split is not obvious from the names. sort, reverse, splice, push, and pop change the array in place. map, filter, slice, and concat return a new one and leave the original alone.
const tags = ["b", "a"];
console.log(tags.toSorted());
console.log(tags);
console.log(tags.sort());
console.log(tags); // node output:
// [ 'a', 'b' ]
// [ 'b', 'a' ]
// [ 'a', 'b' ]
// [ 'a', 'b' ] The newer toSorted, toReversed, and toSpliced methods do the non-mutating version of each. Check your minimum browser or Node version before relying on these copying methods.
When Should You Use Map or Set?
Use Map for changing key-value collections or keys that are not strings or symbols. Use Set for unique values and membership checks; keep plain objects for fixed fields. A Map accepts any value as a key without converting it to a string, and reports its own size:
const counts = new Map();
counts.set("js", 1).set("css", 2);
console.log(counts.get("js"), counts.size, counts.has("php"));
const key = { id: 1 };
counts.set(key, "object key");
console.log(counts.get(key), counts.get({ id: 1 })); // node output:
// 1 2 false
// object key undefined The last line shows why object keys are compared by identity, not contents. The second lookup used a different object that happens to look identical, and found nothing. A plain object would have converted both keys to the string "[object Object]" and collided instead.
Key matching is not quite ===, in two useful ways. A Map can find a NaN key, which === could never match, and it stores -0 as 0.
Set is the same idea for values with no duplicates. new Set(list) followed by [...unique] is the shortest way to deduplicate an array of primitives, and set.has(value) checks membership without a loop in your code.
Plain objects still win in two places. They are what JSON.parse hands back for a JSON object, and what JSON.stringify serializes directly, whereas a Map or Set needs conversion first because its entries are not ordinary properties. They also read better for fixed, known-in-advance shapes such as a configuration block.
Pitfalls and Debugging
TypeError: Cannot read properties of undefined after a find. The classic sequence is orders.find(...) returning undefined, then the next line reading .total from it. Check the result before using it, or use ?. plus a fallback when a miss is legitimate.
Object keys silently becoming strings. Setting obj[1] and reading obj["1"] gives the same slot, and Object.keys reports ['1']. When numeric or object keys must stay distinct, use a Map.
A sorted list appearing somewhere you did not sort. sort mutates, so sorting an array you were handed changes it for whoever else holds it. Copy first with [...list].sort() or use toSorted.
Numbers sorted in the wrong order. [10, 9, 100].sort() gives [10, 100, 9], because the default comparison converts elements to strings. Pass a comparator: list.sort((a, b) => a - b).
Nested state changing in two places at once. Almost always a shallow copy where a deep one was needed. In React and other frameworks that detect updates by comparing references, this shows up as a UI that does not update, when the component compares a shared nested reference even though its contents changed.
Comparing two objects with ===. Identical contents are not equal, since the comparison is by identity. Compare the fields that matter. Comparing serialized forms is a shortcut that only holds for fully JSON-safe data with matching key order, since functions, dates, Map, Set, and undefined are dropped or transformed on the way through.
Frequently Asked Questions
When should you use a JavaScript Map instead of an object?
Use a JavaScript Map when keys change frequently, you need a size count, or keys must be arbitrary values. Map keeps the number 1 distinct from the string "1" and can use objects as keys. Plain objects work well for records with known fields.
Why does editing a JavaScript object copy change the original?
Editing a shallow JavaScript object copy can change the original because spread and Object.assign() leave nested objects shared. Rebuild the nested objects you need to change, or use structuredClone() for supported data that needs a deep copy.
What is the difference between JavaScript forEach() and map()?
JavaScript forEach() runs callbacks for their effects and returns undefined. map() collects callback results into a new array. Both skip empty slots in sparse arrays, and map() preserves the original array length. Use map() when you need the transformed list.
Does JavaScript sort() change the original array?
JavaScript sort() changes the original array and returns that same array. Use toSorted() for a sorted copy, or copy with spread before calling sort(). The copied array still shares any object elements with the original.
Sources
-
[1]
Working with Objects(developer.mozilla.org)
-
[2]
Indexed Collections(developer.mozilla.org)
-
[3]
Keyed Collections(developer.mozilla.org)
Read Next
Splitting JavaScript across files: named against default exports, import paths that actually resolve, ES modules against CommonJS, script type module, and load-order surprises.
Declarations against expressions, arrow functions and what this binds to, default and rest parameters, callbacks, closures, and functions that take or return functions.
The four pieces of JavaScript every other page assumes: values and equality, functions and closures, objects and arrays, and modules that split code across files.