Destructuring and spread
Two pieces of syntax, both written with familiar brackets, that take collections apart and put them back together. They appear in almost every modern JavaScript file, so reading them fluently matters more than most features.
const student = { name: "Ayesha", marks: 72 };
const { name, marks } = student;
console.log(name, marks);
Ayesha 72
That is destructuring: pull named properties out into variables of the same name, in one line instead of two.
Object destructuring
const order = { item: "chai", qty: 2, price: 50 };
const { item, qty } = order;
console.log(item, qty);
chai 2
The names on the left must match the keys. Order is irrelevant — these are looked up by name, not position — and you may take only the properties you want.
Ask for something that is not there and you get undefined, exactly as with
dot notation:
const order = { item: "chai" };
const { item, discount } = order;
console.log(item, discount);
chai undefined
Renaming and defaults
const order = { item: "chai", qty: 2 };
const { item: productName, qty, size = "regular" } = order;
console.log(productName, qty, size);
chai 2 regular
item: productName renames on the way out — useful when the incoming key
clashes with a name you already have. size = "regular" supplies a default.
The default applies only when the value is undefined, not when it is
falsy:
const a = { count: 0 };
const b = {};
const { count: countA = 10 } = a;
const { count: countB = 10 } = b;
console.log(countA, countB);
0 10
A genuine zero survives. That is ?? behaviour rather than || behaviour, and
it is the right choice — the same trap that
the logical operators lesson covers.
Array destructuring
Arrays destructure by position, using square brackets.
const marks = [72, 45, 88];
const [first, second] = marks;
console.log(first, second);
72 45
Skip items with a bare comma:
const marks = [72, 45, 88];
const [, , third] = marks;
console.log(third);
88
Swapping without a temporary falls out of it:
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b);
2 1
And it is why Object.entries loops read the way they do:
const stock = { chai: 12, coffee: 4 };
for (const [item, count] of Object.entries(stock)) {
console.log(`${item}: ${count}`);
}
chai: 12 coffee: 4
Destructuring parameters
This is where it earns its keep. A function taking an options object can name what it needs in the signature:
function describe({ name, marks, grade = "ungraded" }) {
return `${name}: ${marks} (${grade})`;
}
console.log(describe({ name: "Ayesha", marks: 72 }));
console.log(describe({ name: "Bilal", marks: 88, grade: "A" }));
Ayesha: 72 (ungraded) Bilal: 88 (A)
The signature now documents itself, and the caller cannot get the argument order wrong because there is no order.
One sharp edge: calling it with nothing throws, because you cannot destructure
undefined. Default the whole parameter to fix it:
function describe({ name = "unknown" } = {}) {
return name;
}
console.log(describe());
console.log(describe({ name: "Sana" }));
unknown Sana
That = {} at the end is the part people leave out.
Rest: collecting what is left
Three dots on the left of an assignment gather the remainder.
const marks = [72, 45, 88, 30];
const [top, ...others] = marks;
console.log(top);
console.log(others);
72 [ 45, 88, 30 ]
const student = { name: "Ayesha", marks: 72, city: "Karachi" };
const { name, ...rest } = student;
console.log(name);
console.log(rest);
Ayesha
{ marks: 72, city: 'Karachi' }That second pattern is the idiomatic way to remove a property without delete
— take out what you do not want, keep rest.
Rest must come last, and there can be only one.
Spread: expanding
The same three dots on the right do the opposite: they spread a collection out into its parts.
const a = [1, 2];
const b = [3, 4];
console.log([...a, ...b]);
console.log([0, ...a, 99]);
console.log([..."chai"]);
[ 1, 2, 3, 4 ] [ 0, 1, 2, 99 ] [ 'c', 'h', 'a', 'i' ]
For objects, spread copies properties in, and later wins:
const defaults = { size: "regular", sugar: 1 };
const chosen = { sugar: 2 };
console.log({ ...defaults, ...chosen });
console.log({ ...chosen, ...defaults });
{ size: 'regular', sugar: 2 }
{ sugar: 1, size: 'regular' }Order is the entire behaviour. Defaults first, overrides second — reverse it and your defaults overwrite the user's choices.
Updating one field without mutating the original is the everyday use:
const student = { name: "Ayesha", marks: 72 };
const updated = { ...student, marks: 88 };
console.log(student.marks);
console.log(updated.marks);
72 88
Spread as arguments
const marks = [72, 45, 88];
console.log(Math.max(...marks));
console.log(Math.max(marks));
88 NaN
Math.max takes separate arguments, not an array. Spread turns the array into
those arguments; without it the array is converted to a number, which fails.
Rest works in a function definition too — same dots, opposite direction:
function total(...prices) {
return prices.reduce((sum, p) => sum + p, 0);
}
console.log(total(250, 120, 80));
450
Spread copies one level only
The limitation worth knowing before you rely on it.
const original = { name: "Ayesha", marks: { maths: 72 } };
const copy = { ...original };
copy.name = "Bilal";
copy.marks.maths = 100;
console.log(original.name);
console.log(original.marks.maths);
Ayesha 100
The top level is independent. Anything nested is shared, because spread copied
the reference to marks, not the object. structuredClone(original) is the
built-in answer when you need the whole thing independent.
Arrays behave identically:
const rows = [[1, 2], [3, 4]];
const copied = [...rows];
copied[0].push(99);
console.log(rows[0]);
[ 1, 2, 99 ]
Reading the dots
The dots always mean the same thing; the side tells you the direction.
- Left of
=, or in a parameter list → rest, collecting into one. - Right of
=, or in a call, or inside brackets → spread, expanding out.
Next: JSON, which is how these objects travel between a browser and a server.
Prefer learning with a teacher?
WasiLearn Academy runs small-group classes covering this material, online and in Karachi.
See the classes