Learn JS Series):Exercise 1 - an average of any count:
function average(...nums) {
if (nums.length === 0) return 0;
return nums.reduce((a, b) => a + b, 0) / nums.length;
}
console.log(average(2, 4, 6)); // 4
console.log(average()); // 0
The insight: the rest parameter gives a real array, so length and reduce are right there, and we guard the empty case to avoid dividing by zero (which would hand you back NaN, not an error).
Exercise 2 - merging objects:
function merge(...objects) {
return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {});
}
console.log(merge({ a: 1 }, { b: 2 }, { a: 9 })); // { a: 9, b: 2 }
The insight: spreading each object in turn, later keys override earlier ones, so a ends up 9. Notice the parentheses around ({ ...acc, ...obj }) -- without them the arrow body would be read as a block, not an object literal.
Exercise 3 - min and max via spread:
const nums = [3, 1, 4, 1, 5, 9];
console.log(Math.min(...nums)); // 1
console.log(Math.max(...nums)); // 9
The insight: ... spreads the array into separate arguments for Math.min/Math.max; the same ... in a parameter list would instead gather arguments into an array. Same three dots, opposite direction -- which is exactly the symmetry we hammered on last episode.
Now let's put destructuring to work, because it is the tool that makes function calls dramatically more readable, and it pairs beautifully with the defaults you just learned.
Destructuring is a syntax for unpacking values out of arrays and objects into individual variables, in one clean line. We already saw glimpses of it (episodes 11 and 12 both leaned on it), but here it is properly, front and center. Array destructuring pulls values out by position:
const point = [10, 20, 30];
const [x, y, z] = point;
console.log(x, y, z); // 10 20 30
const [first, , third] = point; // skip the middle with an empty slot
console.log(first, third); // 10 30
That empty slot in [first, , third] is not a typo -- it is a deliberate "skip this position" hole. Handy when a function or API hands you a tuple and you only care about some of its elements.
Object destructuring pulls properties out by name (order does not matter here, the names do):
const user = { name: "scipio", level: 7, city: "amsterdam" };
const { name, level } = user;
console.log(name, level); // "scipio" 7
The variable names must match the property names, because for objects it is the key that identifies what you want, not a position. This is already handy on its own, but its real power shows up the moment you move it into a function's parameter list. That is where destructuring stops being a convenience and starts being a genuine design tool.
You can destructure directly in a function's parameter list. For a function that receives an array (like a coordinate pair), this names the parts immediately, right there in the signature:
function distanceFromOrigin([x, y]) {
return Math.sqrt(x * x + y * y);
}
console.log(distanceFromOrigin([3, 4])); // 5
The parameter [x, y] says, in effect, "I expect an array, and I want its first two elements bound to x and y". No arr[0], no arr[1], no mental bookkeeping. This pairs especially well with methods that hand you pairs, like iterating over Object.entries, where each element is a [key, value] array:
const scores = { math: 90, art: 78 };
Object.entries(scores).forEach(([subject, score]) => {
console.log(`${subject}: ${score}`);
});
// math: 90
// art: 78
Each [subject, score] destructures the pair right in the callback parameter -- no pair[0]/pair[1] noise cluttering the body. Having said that, array destructuring in parameters is a smaller win than the object version, simply because positional arrays carry the same "which slot means what?" burden that positional arguments do. So let's get to the pattern that genuinely changes how you write functions.
Here is the pattern that transforms real code. When a function needs several inputs, especially optional ones, passing them positionally is fragile: the caller must remember the exact order, and skipping a middle one is awkward (you have to pass undefined as a placeholder, as we saw last episode). The fix is to accept a single options object and destructure it right in the parameter list. Now every argument is named at the call site:
function createUser({ name, age, role }) {
return `${name} (${age}), role: ${role}`;
}
console.log(createUser({ name: "scipio", role: "admin", age: 30 }));
// "scipio (30), role: admin"
Look carefully at that call: { name: "scipio", role: "admin", age: 30 }. The order is irrelevant -- I deliberately wrote role before age to prove the point -- and each value is labelled by the key sitting next to it. Compare that to the positional alternative, createUser("scipio", 30, "admin"), where a reader has no earthly idea what 30 or "admin" are supposed to mean without going and reading the function definition. Multiply that across a codebase and the difference is enormous.
The named arguments style buys you three concrete things. First, calls are self-documenting -- you read the call and you know what each value is for. Second, order stops mattering, so nobody ever swaps two same-typed arguments by accident (the classic createRect(width, height) versus createRect(height, width) bug simply cannot happen). Third, and this is the quiet hero, you can add a new option later without breaking a single existing call, because old calls just do not mention the new key. Positional signatures do not give you that for free -- add a fourth positional parameter and every call site has to reckon with it.
Destructuring combines with the defaults from episode 20 to make optional settings genuinely clean. You give each destructured property its own fallback, so callers only pass what they actually want to override:
function connect({ host, port = 8080, secure = false } = {}) {
return `${secure ? "https" : "http"}://${host}:${port}`;
}
console.log(connect({ host: "example.com" })); // "http://example.com:8080"
console.log(connect({ host: "example.com", secure: true }));// "https://example.com:8080"
There are actually two layers of defaulting at work here, and it is worth pulling them apart because newcomers often see only one. The inner defaults -- port = 8080 and secure = false -- kick in per property when that specific key is missing or undefined in the object you passed. The outer = {} at the very end is a different thing entirely: it defaults the whole parameter to an empty object when you call connect() with no argument at all.
That outer = {} is not optional politeness -- it is load-bearing. Without it, calling connect() throws a TypeError, because JavaScript would try to destructure host, port, and secure out of undefined, and you cannot read properties off undefined. With the = {} guard, a bare connect() first substitutes an empty object, then the inner defaults fill in port and secure, and only host comes back undefined. Remember this one -- forgetting the = {} on a destructured options parameter is one of the most common little crashes you will write in your first month, and now you will recognise it instantly:
function badConnect({ host, port = 8080 }) { // no = {} guard
return `${host}:${port}`;
}
// badConnect(); // TypeError: Cannot destructure property 'host' of 'undefined'
console.log(badConnect({ host: "example.com" })); // "example.com:8080" (fine WITH an arg)
So the rule of thumb is simple and mechanical: any time you destructure an object in a parameter list and you want the function to be callable with no arguments, end the pattern with = {}. It costs four characters and saves a crash.
Two more tricks you will reach for constantly. First, you can rename a property as you destructure it, using the originalName: newName form. This is invaluable when the source property name is cryptic, or when it would clash with a variable you already have in scope:
const response = { d: "2026-08-13", v: 42 };
const { d: date, v: value } = response;
console.log(date, value); // "2026-08-13" 42
Read d: date as "take the property d, and bind it to a local variable called date". Nota bene: this trips people up because it looks like the object-literal syntax for the opposite direction. In an object literal { date: d } means "make a key date from variable d"; in a destructuring pattern { d: date } means "read key d into variable date". Same colon, mirror-image meaning depending on which side of the = you are on -- keep that straight and renaming becomes second nature.
Second, you can destructure nested structures in one go, reaching down into inner objects:
const order = {
id: 1,
customer: { name: "scipio", address: { city: "amsterdam" } },
};
const { customer: { name, address: { city } } } = order;
console.log(name, city); // "scipio" "amsterdam"
Notice something subtle in that pattern: customer: { ... } does NOT create a customer variable. The customer: part is purely a path -- it tells JavaScript where to dig, and only the innermost names (name, city) actually become variables. If you also wanted the customer object itself, you would have to list it separately. And a word of caution born from experience: nested destructuring gets hard to read fast if you go too deep. One or two levels is crisp and clear; five levels is a puzzle nobody wants to decode at 2am. Use it for pulling a couple of nested values, not as a party trick.
The rest syntax from episode 20 works inside destructuring too, gathering "everything else" into a fresh array or object. On the array side it splits off a head and a tail; on the object side it grabs a few named properties and sweeps the remainder into a new object:
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail); // 1 [2, 3, 4]
const { id, ...rest } = { id: 1, name: "scipio", level: 7 };
console.log(id); // 1
console.log(rest); // { name: 'scipio', level: 7 }
The object version is especially useful for the "remove one property, immutably" move: destructure out the key you want gone, and rest is a brand-new object that has everything except that key -- without mutating the original at all. This is a pattern you will see everywhere in modern JavaScript (React state updates lean on it heavily, for one), so it is worth burning into memory now:
function withoutPassword(user) {
const { password, ...safe } = user;
return safe; // a new object with everything except password
}
console.log(withoutPassword({ name: "scipio", password: "hunter2", level: 7 }));
// { name: 'scipio', level: 7 }
The original user is untouched, safe is a fresh object, and the sensitive field never leaves the function. Clean, declarative, and no delete in sight -- which matters, because delete mutates the original and (as we will see much later in the series) can hurt performance by deoptimising the object's shape.
A lot of you came here from the Learn Python Series, and a few from Rust and Go, so a quick look sideways helps place destructuring in the bigger picture. The reassuring news: unpacking-into-variables and named-arguments are near-universal ideas -- only the spelling and the philosophy shift.
Python is the closest cousin, and if you know it the mapping is almost one-to-one. Python has sequence unpacking (a, b, c = triple), a *rest catch-all, and -- crucially -- real keyword arguments baked into the language, which is exactly what JavaScript is simulating with its options-object trick:
point = [10, 20, 30]
first, *rest = point # first = 10, rest = [20, 30]
print(first, rest)
def connect(host, port=8080, secure=False): # genuine keyword args
scheme = "https" if secure else "http"
return f"{scheme}://{host}:{port}"
print(connect("example.com", secure=True)) # named at the call site
Here is the interesting contrast: Python gives you named arguments for free at the language level (connect(host="example.com", secure=True)), so it never needs the options-object pattern. JavaScript has no built-in keyword arguments, so the community reached for "pass one object and destructure it" -- and it works so well that it became the idiomatic way to write any function with more than two or three options. Same destination, different road.
Rust unpacks through its pattern-matching system, which is far more powerful (it drives match, if let, and let bindings), and it destructures structs by field name in a way that will feel oddly familiar:
struct Config { host: String, port: u16 }
fn describe(Config { host, port }: Config) -> String {
format!("{}:{}", host, port)
}
fn main() {
let c = Config { host: "example.com".to_string(), port: 8080 };
println!("{}", describe(c)); // example.com:8080
}
That Config { host, port } in the parameter position is destructuring a struct right in the function signature -- the same shape as JavaScript's function f({ host, port }), just with the type named up front. Rust does not have optional/default parameters at all, though; the idiomatic Rust answer is the builder pattern or an explicit Option<T>, reflecting its "no hidden behaviour" values.
Go, true to form, keeps it minimal. It has multiple return values and can unpack them into several variables, but it has no object-destructuring in parameters and no default arguments whatsoever. The Go answer to "many optional settings" is, once again, to pass a config struct explicitly:
package main
import "fmt"
type Config struct {
Host string
Port int
Secure bool
}
func connect(c Config) string {
scheme := "http"
if c.Secure {
scheme = "https"
}
return fmt.Sprintf("%s://%s:%d", scheme, c.Host, c.Port)
}
func main() {
fmt.Println(connect(Config{Host: "example.com", Port: 8080}))
}
Notice that a Go Config{Host: "example.com"} literal reads almost exactly like a JavaScript options object -- named fields, order-independent -- which is precisely why the pattern feels natural coming from either direction. Four languages, one recurring lesson: once a function grows past a couple of arguments, humans want names, not positions. JavaScript just gets there by destructuring an object rather than baking keyword arguments into the grammar.
Three exercises, increasing in difficulty. Type them out, run them, and predict the output before you check -- that gap between your guess and the actual result is where the real learning lives. Full solutions open the next episode.
const rgb = [255, 128, 0], destructure it into red, green, blue and print each. Then swap two variables using array destructuring ([a, b] = [b, a]) and show it works without a temporary variable.drawBox({ width, height, char = "*" }) that returns a string describing a box, using the named char with a default. Call it once with all three properties and once relying on the char default.describe({ name, ...details }) that separates the name from all other properties, returning a string like "scipio has extras: {...}". Then explain, in one sentence, why adding = {} to that destructured parameter matters when the function is called with no arguments.[x, y]) and objects by name ({ name, level }) into variables in a single line, and an empty slot lets you skip array positions you do not want.arr[0] or obj.prop bookkeeping in the body.= {} fallback for the whole object, so a no-argument call fills in sensibly instead of throwing a TypeError.d: date), reach into nested structures, and use rest (...tail, ...rest) to gather the remainder -- the neat, non-mutating "remove a property" trick.Next episode we finally confront the this keyword head-on: five rules that, once you know them, explain every single case of what this refers to -- and clear up one of JavaScript's most notorious sources of confusion for good.