Functions all 4 written
Parameters, closures, this, and functions as values: the machinery underneath every callback in the rest of the course.
9 Functions: parameters, defaults and returns
A function is a named, reusable piece of a program: it takes some values in, does something, and usually hands a value back. Everything from here on is built from them, and in Node — where you pass functions to other functions constantly — they matter even more than usual.
Three ways to write one
example.mjs
// A function declaration.
function area(width, height) {
return width * height;
}
// A function expression, stored in a const.
const perimeter = function (width, height) {
return 2 * (width + height);
};
// An arrow function. With one expression, the braces and "return" can go.
const diagonal = (width, height) => Math.hypot(width, height);
console.log(area(3, 4), perimeter(3, 4), diagonal(3, 4));
check("all three are just functions", [area, perimeter, diagonal].every((f) => typeof f === "function"), true);
check("3-4-5", diagonal(3, 4), 5);
What node printed, when the build ran it
node example.mjs
12 14 5 ok all three are just functions ok 3-4-5
For now the differences hardly matter. Declarations can be called from above the line where they
are written (lesson 10 explains why), and arrow functions handle this differently
(lesson 11). Most modern code uses declarations for named, top-level functions and arrows for small
functions passed to other functions.
Arguments that do not match
JavaScript does not check that a call passes the right number of arguments. Too few, and the
missing ones are undefined. Too many, and the extras are ignored.
example.mjs
function greet(first, last) {
return `Hello, ${first} ${last}`;
}
console.log(greet("Ada", "Lovelace"));
console.log(greet("Ada")); // last is undefined
console.log(greet("Ada", "Lovelace", "Byron")); // "Byron" is silently dropped
check("a missing argument is undefined, not an error", greet("Ada"), "Hello, Ada undefined");
check("the function says how many it expects", greet.length, 2);
What node printed, when the build ran it
node example.mjs
Hello, Ada Lovelace Hello, Ada undefined Hello, Ada Lovelace ok a missing argument is undefined, not an error ok the function says how many it expects
Defaults
example.mjs
function makeTag(label, colour = "grey", size = label.length > 8 ? "wide" : "normal") {
return { label, colour, size };
}
console.log(makeTag("draft"));
console.log(makeTag("needs review"));
console.log(makeTag("urgent", "red"));
console.log(makeTag("urgent", undefined, "wide")); // undefined means "use the default"
check("a default can use an earlier parameter", makeTag("needs review").size, "wide");
check("passing undefined triggers the default", makeTag("x", undefined).colour, "grey");
check("passing null does not", makeTag("x", null).colour, null);
What node printed, when the build ran it
node example.mjs
{ label: 'draft', colour: 'grey', size: 'normal' }
{ label: 'needs review', colour: 'grey', size: 'wide' }
{ label: 'urgent', colour: 'red', size: 'normal' }
{ label: 'urgent', colour: 'grey', size: 'wide' }
ok a default can use an earlier parameter
ok passing undefined triggers the default
ok passing null does not
A default applies when the argument is undefined — whether because nothing was
passed or because undefined was passed on purpose. null is a real value,
and does not trigger it.
example.mjs
function addItem(item, list = []) {
list.push(item);
return list;
}
const first = addItem("a");
const second = addItem("b");
console.log(first, second);
check("each call gets its own fresh array", first === second, false);
check("so the second call did not see the first item", second, ["b"]);
What node printed, when the build ran it
node example.mjs
[ 'a' ] [ 'b' ] ok each call gets its own fresh array ok so the second call did not see the first item
The default expression runs fresh on every call that needs it, so each call gets a new array. That sounds obvious, and it is worth saying because Python does the opposite — a default list there is created once and shared, and the course next door spends a whole lesson on the bugs that causes.
Any number of arguments: rest parameters
example.mjs
function total(...amounts) { // everything passed, collected into an array
return amounts.reduce((sum, n) => sum + n, 0);
}
function log(level, ...parts) { // the first named, the rest collected
return `[${level}] ${parts.join(" ")}`;
}
console.log(total(), total(5), total(1, 2, 3, 4));
console.log(log("info", "server", "started", "on", 8080));
const prices = [4, 12, 7];
console.log(total(...prices)); // spread: an array, passed as separate arguments
check("rest collects into a real array", total(1, 2, 3, 4), 10);
check("spread is the reverse of rest", total(...prices), 23);
What node printed, when the build ran it
node example.mjs
0 5 10 [info] server started on 8080 23 ok rest collects into a real array ok spread is the reverse of rest
What comes back
example.mjs
function noReturn() {
const x = 1 + 1;
}
function earlyReturn(n) {
if (n < 0) return "negative";
return "fine";
}
function twoThings() {
return { min: 1, max: 9 }; // return an object to hand back several values
}
console.log(noReturn(), earlyReturn(-5), twoThings());
const { min, max } = twoThings(); // and take them apart again (lesson 16)
check("no return statement means undefined", noReturn(), undefined);
check("return exits the function immediately", earlyReturn(-5), "negative");
check("several values, via an object", [min, max], [1, 9]);
What node printed, when the build ran it
node example.mjs
undefined negative { min: 1, max: 9 }
ok no return statement means undefined
ok return exits the function immediately
ok several values, via an object
The return that returns nothing
example.mjs
function config() {
return
{
port: 8080
};
}
console.log(config());
check("this returns undefined, not the object", config(), undefined);
What node printed, when the build ran it
node example.mjs
undefined ok this returns undefined, not the object
That function returns undefined. JavaScript inserts semicolons where it thinks a
statement ends, and a return followed by a line break is a complete statement: it
becomes return;. The object on the following lines is then a block that is never
reached. Keep the opening brace on the same line as return, always.
Should you write semicolons? JavaScript inserts them automatically in most
places, and some style guides omit them. The return case above goes wrong either
way — adding semicolons elsewhere would not have saved it. What saves it is a formatter such as
Prettier, which would have moved the brace up and made the bug impossible to write.
Functions are values
A function is an object like any other. You can store it, put it in an array, pass it to another function and return it from one. That last pair is how nearly all of Node's asynchronous code is written, and it is the subject of lesson 12.
example.mjs
const operations = {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
};
function apply(name, a, b) {
const op = operations[name];
return op(a, b);
}
console.log(apply("add", 3, 4), apply("multiply", 3, 4));
console.log(operations.add.name, "<- an arrow stored in a property still has a name");
check("functions can live in an object", apply("multiply", 3, 4), 12);
check("and know their own name", operations.add.name, "add");
What node printed, when the build ran it
node example.mjs
7 12 add <- an arrow stored in a property still has a name ok functions can live in an object ok and know their own name
example.mjs
const operations = { add: (a, b) => a + b };
console.log(operations.subtract(5, 2));
What node printed as it died, when the build ran it
node example.mjs
file:///app/example.mjs:3
console.log(operations.subtract(5, 2));
^
TypeError: operations.subtract is not a function
at file:///app/example.mjs:3:24
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
- Declaration, expression or arrow. Arrows can drop the braces and
returnfor a single expression. - Missing arguments are
undefined; extra ones are ignored; nothing is checked. - Defaults apply to
undefined(notnull), can use earlier parameters, and are evaluated fresh on each call. ...restcollects arguments into an array;...spreadexpands one into arguments.- No
returnmeansundefined. Never break the line straight afterreturn. - Functions are values: store them, pass them, return them.
10 Scope, closures and hoisting
Where a name can be used is called its scope. That sounds like bookkeeping, and it is — until you pass a function somewhere else and it still remembers the variables around it. That memory is a closure, and it is how every callback in Node works.
Blocks make scopes
example.mjs
const outside = "visible everywhere in this file";
if (true) {
const inside = "visible only inside these braces";
console.log(outside);
console.log(inside);
}
console.log(typeof inside, "<- gone once the block ends");
check("a const inside a block does not leak out", typeof inside, "undefined");
What node printed, when the build ran it
node example.mjs
visible everywhere in this file visible only inside these braces undefined <- gone once the block ends ok a const inside a block does not leak out
Every pair of braces — an if, a loop, a function body — makes a scope. A
const or let declared inside exists only inside. Code inside can see
outwards; code outside cannot see in. When a name is used, JavaScript looks in the current scope,
then the one enclosing it, and so on outwards until it finds it.
example.mjs
const level = "outer";
function show() {
const level = "inner"; // a new name that hides the outer one
return level;
}
console.log(show(), level);
check("the inner name shadows the outer", show(), "inner");
check("without changing it", level, "outer");
What node printed, when the build ran it
node example.mjs
inner outer ok the inner name shadows the outer ok without changing it
A function remembers where it was made
example.mjs
function makeCounter() {
let count = 0; // lives in makeCounter's scope
return function next() {
count += 1; // ...and this function can still reach it
return count;
};
}
const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), a());
console.log(b());
check("a kept its own count between calls", a(), 4);
check("b has a separate count", b(), 2);
What node printed, when the build ran it
node example.mjs
1 2 3 1 ok a kept its own count between calls ok b has a separate count
makeCounter has finished running, and yet count is still alive, because the
function it returned still refers to it. Each call to makeCounter makes a fresh
count, so a and b do not share. That is a closure: a function
together with the variables it could see when it was created.
It is also how JavaScript does private state without classes. Nothing outside
makeCounter can read or change count except by calling next.
example.mjs
function createAccount(opening) {
let balance = opening;
return {
deposit(amount) {
if (amount <= 0) throw new Error("deposit must be positive");
balance += amount;
return balance;
},
get balance() {
return balance;
},
};
}
const account = createAccount(100);
account.deposit(50);
let refused;
try {
account.balance = 1_000_000; // there is no way in from outside
} catch (err) {
refused = err.name;
}
console.log("balance:", account.balance);
console.log("setting it directly:", refused);
check("only deposit can change the balance", account.balance, 150);
check("a getter with no setter refuses writes", refused, "TypeError");
What node printed, when the build ran it
node example.mjs
balance: 150 setting it directly: TypeError ok only deposit can change the balance ok a getter with no setter refuses writes
The loop bug, and why let fixed it
Here is the bug that made let necessary. Three timers are scheduled in a loop, each
meant to print its own loop number.
example.mjs
const withVar = [];
const withLet = [];
for (var i = 0; i < 3; i++) {
setTimeout(() => withVar.push(i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => withLet.push(j), 0);
}
setTimeout(() => {
console.log("var:", withVar);
console.log("let:", withLet);
check("all three var callbacks saw the final value", withVar, [3, 3, 3]);
check("each let callback saw its own", withLet, [0, 1, 2]);
}, 10);
What node printed, when the build ran it
node example.mjs
var: [ 3, 3, 3 ] let: [ 0, 1, 2 ] ok all three var callbacks saw the final value ok each let callback saw its own
The timers run after the loop has finished. A var is scoped to the whole function (or
file), not to the loop, so there is exactly one i, and by the time any timer runs it is
3. A let in a for header gets a fresh binding on every turn, so each
callback closes over its own. This is the single best reason never to write var.
Hoisting
JavaScript reads a whole scope before running any of it, and some declarations take effect from the top of their scope regardless of where they are written. This is called hoisting, and each kind of declaration does it differently.
example.mjs
const fromAbove = double(4); // a function declaration works above its line
const earlyValue = early; // a var exists already, but holds undefined
function double(n) {
return n * 2;
}
var early = "assigned now";
console.log(fromAbove, earlyValue, early);
check("function declarations are hoisted whole", fromAbove, 8);
check("var is hoisted without its value", earlyValue, undefined);
check("which arrives when its line runs", early, "assigned now");
What node printed, when the build ran it
node example.mjs
8 undefined assigned now ok function declarations are hoisted whole ok var is hoisted without its value ok which arrives when its line runs
example.mjs
console.log(triple(4));
const triple = (n) => n * 3;
What node printed as it died, when the build ran it
node example.mjs
file:///app/example.mjs:1
console.log(triple(4));
^
ReferenceError: Cannot access 'triple' before initialization
at file:///app/example.mjs:1:13
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
A function declaration is available from the top of its scope, which is why you can put
a program's helper functions at the bottom of a file. An arrow function stored in a
const is a const, and follows the const rule from lesson 4:
it exists but cannot be read until its line runs.
Nothing leaks into the global scope
example.mjs
const settings = { debug: true };
var legacy = "also module-scoped here";
console.log(globalThis.settings, globalThis.legacy);
console.log(typeof globalThis.console, "<- real globals are still there");
check("a top-level const is not a global", globalThis.settings, undefined);
check("in a module, not even var is", globalThis.legacy, undefined);
What node printed, when the build ran it
node example.mjs
undefined undefined object <- real globals are still there ok a top-level const is not a global ok in a module, not even var is
Each module — each .mjs file — has its own top-level scope. Names declared at the
top of one file are not visible from another unless exported (lesson 20), and never become
properties of globalThis. In an old-style browser script, a top-level var
would become a global, which is how two scripts on one page used to break each other.
- Braces make scopes. Code can see outwards, not in. An inner name can shadow an outer one.
- A closure is a function plus the variables it could see when it was made. They stay alive as long as the function does.
- Closures give private state: only the returned functions can reach it.
varis function-scoped, so callbacks in a loop share one variable.letgives each turn its own.- Function declarations are hoisted whole;
varis hoisted asundefined;constandletthrow until their line. - Each module has its own scope. Nothing becomes a global by accident.
11 this, and why arrow functions do not have one
Inside a function, this refers to the object the function was
called on. The catch is in the word called: the same function gives a different
this depending on how it is invoked, and the most common way to call a function in Node
— passing it to something else — loses it entirely.
The object before the dot
example.mjs
const user = {
name: "Ada",
greet() {
return `Hi, I am ${this.name}`;
},
};
const other = { name: "Alan", greet: user.greet }; // the same function, borrowed
console.log(user.greet());
console.log(other.greet());
check("this is the object the method was called on", user.greet(), "Hi, I am Ada");
check("so the same function answers differently", other.greet(), "Hi, I am Alan");
What node printed, when the build ran it
node example.mjs
Hi, I am Ada Hi, I am Alan ok this is the object the method was called on ok so the same function answers differently
user.greet() is called on user, so this is user.
other.greet() runs exactly the same function, called on other. The rule is:
look at the call, and this is whatever is to the left of the dot.
No dot, no this
example.mjs
const user = {
name: "Ada",
greet() {
return `Hi, I am ${this.name}`;
},
};
const greet = user.greet; // take the function off the object
console.log(greet()); // and call it with nothing before the dot
What node printed as it died, when the build ran it
node example.mjs
file:///app/example.mjs:4
return `Hi, I am ${this.name}`;
^
TypeError: Cannot read properties of undefined (reading 'name')
at greet (file:///app/example.mjs:4:29)
at file:///app/example.mjs:9:13
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
Called with nothing before the dot, this is undefined (modules are always
in strict mode), and reading .name from undefined throws. You will rarely
write const greet = user.greet on purpose. You will constantly write the same thing by
accident, by passing a method to another function:
example.mjs
const counter = {
count: 0,
increment() {
this.count++;
},
};
let failure;
try {
[1, 2, 3].forEach(counter.increment); // passes the function, not the object
} catch (err) {
failure = err.name;
}
console.log("count:", counter.count);
console.log("error:", failure);
check("the method never touched counter", counter.count, 0);
check("it threw, reading count from an undefined this", failure, "TypeError");
What node printed, when the build ran it
node example.mjs
count: 0 error: TypeError ok the method never touched counter ok it threw, reading count from an undefined this
Three fixes
example.mjs
const counter = {
count: 0,
increment() {
this.count++;
},
};
[1, 2].forEach(() => counter.increment()); // 1. call it on the object yourself
[1, 2].forEach(counter.increment.bind(counter)); // 2. bind: fix this permanently
[1, 2].forEach(counter.increment, counter); // 3. some methods take a thisArg
console.log(counter.count);
check("all three kept this", counter.count, 6);
What node printed, when the build ran it
node example.mjs
6 ok all three kept this
The first is the most common in modern code, because it is the plainest: the arrow function
calls counter.increment() with counter before the dot, so there is nothing
to lose. bind returns a new function whose this is fixed forever.
Arrow functions have no this of their own
An arrow function does not get a this from its call. It uses the this
of the code around it, exactly as it would use any other outer variable. That makes arrows the
right choice inside a method, and the wrong choice as a method.
example.mjs
const team = {
name: "Platform",
members: ["Ada", "Alan"],
// A regular method, with an arrow inside it: the arrow borrows the method's this.
roster() {
return this.members.map((m) => `${m} (${this.name})`);
},
// An arrow AS the method: its this is the module's top level, which is undefined.
broken: () => typeof this,
};
console.log(team.roster());
console.log(team.broken());
check("the inner arrow saw the method's this", team.roster()[0], "Ada (Platform)");
check("an arrow used as a method has no object to see", team.broken(), "undefined");
What node printed, when the build ran it
node example.mjs
[ 'Ada (Platform)', 'Alan (Platform)' ] undefined ok the inner arrow saw the method's this ok an arrow used as a method has no object to see
call and apply
example.mjs
function describe(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const ada = { name: "Ada" };
console.log(describe.call(ada, "Hello", "!")); // this, then the arguments
console.log(describe.apply(ada, ["Hello", "?"])); // this, then an array of them
check("call sets this for one call", describe.call(ada, "Hi", "."), "Hi, Ada.");
What node printed, when the build ran it
node example.mjs
Hello, Ada! Hello, Ada? ok call sets this for one call
You will meet call and apply in older code and in libraries more than
you will write them. They show that this is just a hidden extra argument, one that
JavaScript normally fills in from whatever is before the dot.
Two Node specifics
A regular function passed to a timer is called by Node's timer machinery, and gets
its object as this. It is never the object you wanted.
example.mjs
const job = {
name: "nightly report",
start() {
setTimeout(function () {
console.log("regular function sees:", this.constructor.name);
console.log("regular function name:", this.name);
}, 0);
setTimeout(() => {
console.log("arrow function sees: ", this.name);
check("the arrow kept the job", this.name, "nightly report");
}, 5);
},
};
job.start();
What node printed, when the build ran it
node example.mjs
regular function sees: Timeout regular function name: undefined arrow function sees: nightly report ok the arrow kept the job
And at the very top of a file, outside any function, this depends on which module
system the file uses — one of several differences lesson 21 lists.
example.cjs
console.log("top-level this in CommonJS:", this);
console.log("is it module.exports?", this === module.exports);
check("in a .cjs file, top-level this is module.exports", this === module.exports, true);
What node printed, when the build ran it
node example.cjs
top-level this in CommonJS: {}
is it module.exports? true
ok in a .cjs file, top-level this is module.exports
example.mjs
console.log("top-level this in an ES module:", this);
check("in a .mjs file, top-level this is undefined", this, undefined);
What node printed, when the build ran it
node example.mjs
top-level this in an ES module: undefined ok in a .mjs file, top-level this is undefined
thisis set by the call: it is whatever is before the dot.- Passing
obj.methodas a callback detaches it, andthisbecomesundefined. - Fix it by wrapping in an arrow (
() => obj.method()) or withbind. - Arrows have no
thisof their own: right inside a method, wrong as a method. - In Node, a regular function passed to
setTimeoutgets aTimeoutasthis. Top-levelthisismodule.exportsin CommonJS andundefinedin a module.
12 map, filter, reduce, and functions as values
A function that takes another function as an argument, or returns one, is called a higher-order function. Arrays have a set of them that replace most loops you would otherwise write, and learning them now is also learning the shape of almost every Node API: you hand over a function, and it gets called for you.
map, filter, find
example.mjs
const orders = [
{ id: 1, total: 40, paid: true },
{ id: 2, total: 15, paid: false },
{ id: 3, total: 90, paid: true },
];
const totals = orders.map((o) => o.total); // transform each one
const unpaid = orders.filter((o) => !o.paid); // keep the ones that pass
const big = orders.find((o) => o.total > 50); // the first that passes
const where = orders.findIndex((o) => o.id === 2);
console.log(totals);
console.log(unpaid);
console.log(big, where);
check("map returns one result per item", totals, [40, 15, 90]);
check("filter keeps only the matches", unpaid.map((o) => o.id), [2]);
check("find returns the item itself", big.id, 3);
check("find gives undefined when nothing matches", orders.find((o) => o.total > 1000), undefined);
What node printed, when the build ran it
node example.mjs
[ 40, 15, 90 ]
[ { id: 2, total: 15, paid: false } ]
{ id: 3, total: 90, paid: true } 1
ok map returns one result per item
ok filter keeps only the matches
ok find returns the item itself
ok find gives undefined when nothing matches
None of these change the original array. map and filter build new
ones, which is why you can chain them without worrying about what came before.
some, every, and reduce
example.mjs
const scores = [72, 88, 45, 91];
console.log(scores.some((s) => s < 50), scores.every((s) => s >= 40));
const sum = scores.reduce((running, s) => running + s, 0);
const highest = scores.reduce((best, s) => (s > best ? s : best), -Infinity);
const byBand = scores.reduce((bands, s) => {
const band = s >= 70 ? "distinction" : "pass";
bands[band] = (bands[band] ?? 0) + 1;
return bands;
}, {});
console.log(sum, highest, byBand);
check("some: is anything below 50?", scores.some((s) => s < 50), true);
check("reduce folds a list into one value", sum, 296);
check("the accumulator can be an object", byBand, { distinction: 3, pass: 1 });
What node printed, when the build ran it
node example.mjs
true true
296 91 { distinction: 3, pass: 1 }
ok some: is anything below 50?
ok reduce folds a list into one value
ok the accumulator can be an object
reduce is the general one: it walks the array carrying a running value — the
accumulator — and each call returns the next version of it. The second argument is where
the accumulator starts. Leave it out and something bad happens on the one input nobody tests:
example.mjs
const refunds = []; // came back empty from the database today
const total = refunds.reduce((sum, r) => sum + r.amount);
console.log(total);
What node printed as it died, when the build ran it
node example.mjs
file:///app/example.mjs:3
const total = refunds.reduce((sum, r) => sum + r.amount);
^
TypeError: Reduce of empty array with no initial value
at Array.reduce (<anonymous>)
at file:///app/example.mjs:3:23
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
With no starting value, reduce uses the first element instead — and an empty array
has none. Always pass the initial value.
Chaining
example.mjs
const people = [
{ name: "Ada", team: "core", hours: 32 },
{ name: "Alan", team: "web", hours: 40 },
{ name: "Grace", team: "core", hours: 45 },
];
const coreHours = people
.filter((p) => p.team === "core")
.map((p) => p.hours)
.reduce((a, b) => a + b, 0);
const names = people.map((p) => p.name).join(", ");
console.log(coreHours, names);
check("filter, then map, then reduce", coreHours, 77);
What node printed, when the build ran it
node example.mjs
77 Ada, Alan, Grace ok filter, then map, then reduce
Grouping
example.mjs
const tickets = [
{ id: 1, status: "open" },
{ id: 2, status: "closed" },
{ id: 3, status: "open" },
];
const byStatus = Object.groupBy(tickets, (t) => t.status);
console.log(byStatus);
check("groupBy builds the buckets for you", byStatus.open.map((t) => t.id), [1, 3]);
What node printed, when the build ran it
node example.mjs
[Object: null prototype] {
open: [ { id: 1, status: 'open' }, { id: 3, status: 'open' } ],
closed: [ { id: 2, status: 'closed' } ]
}
ok groupBy builds the buckets for you
That [Object: null prototype] is Node telling you the result is an object with no
prototype at all — so a group whose key happens to be "constructor" or
"toString" cannot collide with anything built in. Lesson 15 shows why that matters, and
lesson 17 what a prototype is.
Sorting, which is different
example.mjs
const n = [10, 9, 1, 100];
console.log([...n].sort()); // !!
console.log([...n].sort((a, b) => a - b)); // numbers, ascending
console.log([...n].sort((a, b) => b - a)); // descending
check("the default sort compares as text", [...n].sort(), [1, 10, 100, 9]);
check("a comparator sorts numbers properly", [...n].sort((a, b) => a - b), [1, 9, 10, 100]);
What node printed, when the build ran it
node example.mjs
[ 1, 10, 100, 9 ] [ 1, 9, 10, 100 ] [ 100, 10, 9, 1 ] ok the default sort compares as text ok a comparator sorts numbers properly
With no function, sort converts every element to a string and compares those, so
"100" comes before "9". For numbers, always pass a comparator:
(a, b) => a - b returns a negative number when a should come first.
And unlike the others in this lesson, sort changes the array it is called
on. That is why the panel above sorted copies ([...n]). toSorted is
the version that does not.
example.mjs
const original = [3, 1, 2];
const copy = original.toSorted((a, b) => a - b);
console.log(original, copy);
original.sort((a, b) => a - b);
console.log(original);
check("toSorted leaves the original alone", copy, [1, 2, 3]);
check("sort rearranged it in place", original, [1, 2, 3]);
What node printed, when the build ran it
node example.mjs
[ 3, 1, 2 ] [ 1, 2, 3 ] [ 1, 2, 3 ] ok toSorted leaves the original alone ok sort rearranged it in place
Functions that make functions
example.mjs
const multiplier = (factor) => (n) => n * factor;
const double = multiplier(2);
const triple = multiplier(3);
console.log([1, 2, 3].map(double), [1, 2, 3].map(triple));
// Run something at most once, however often it is called.
function once(fn) {
let done = false;
let result;
return (...args) => {
if (!done) {
done = true;
result = fn(...args);
}
return result;
};
}
let connections = 0;
const connect = once(() => ++connections);
connect(); connect(); connect();
check("each multiplier remembers its own factor", triple(5), 15);
check("once() let the function run only once", connections, 1);
What node printed, when the build ran it
node example.mjs
[ 2, 4, 6 ] [ 3, 6, 9 ] ok each multiplier remembers its own factor ok once() let the function run only once
Both of those are closures (lesson 10): the returned function keeps factor, or
done and result, alive. once is a real pattern — Node's own
events.once is a version of it — and a good example of adding behaviour to a function
without changing the function.
forEach cannot be stopped, and does not wait. There is no way to
break out of a forEach, and if the function you give it is
async, forEach starts every call and returns immediately without waiting
for any of them. Use for...of whenever either matters. Lesson 27 shows the second
problem happening.
maptransforms,filterkeeps,findreturns the first match orundefined. None of them changes the original.someandeveryanswer yes/no questions about the whole list.reducefolds a list into one value. Always give it a starting value.sortcompares as strings without a comparator, and mutates. PrefertoSorted.- A function can return a function that remembers its arguments. That is how
once, and much of Node, is built.