Warm-up · Activity 1 of 7
Warm-up from lesson B4.4: what does this print?
function makeAdder(x) {
return (y) => x + y;
}
const add2 = makeAdder(2);
console.log(typeof add2, add2(5));// B4.5 · ~38 min · Beginner
After this build you write function factories: counters that keep their own private count, formatters that remember their settings, and small functions joined with pipe.
Lesson 5 of 5 in B4 Functions
You will be able to
Warm-up · Activity 1 of 7
function makeAdder(x) {
return (y) => x + y;
}
const add2 = makeAdder(2);
console.log(typeof add2, add2(5));Predict · Activity 2 of 7
let count = 0;
function makeCounter() {
return () => {
count += 1;
return count;
};
}
const a = makeCounter();
const b = makeCounter();
a();
a();
console.log(b());Practice · Activity 3 of 7
function makeCounter(start = 0, step = 1) {
let count = ____;
const increment = () => {
count += step;
return count;
};
return {increment};
}
const c = makeCounter(10, 5);
console.log(c.increment());Practice · Activity 4 of 7
function makeFormatter(options = {}) {
const prefix = options.prefix ?? "";
const suffix = options.suffix ?? "";
const decimals = options.decimals ?? 2;
return (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`;
}
const percent = makeFormatter({suffix: "%", decimals: 1});
console.log(percent(12.34));Practice · Activity 5 of 7
const pipe = (first, second) => (value) => second(first(value));
const addOne = (n) => n + 1;
const double = (n) => n * 2;
console.log(pipe(addOne, double)(5), pipe(double, addOne)(5));Brain teaser · Activity 6 of 7
function makeCounter(start = 0, step = 1) {
let count = start;
const increment = () => {
count += step;
return count;
};
const decrement = () => {
count -= step;
return count;
};
const value = () => count;
const reset = () => {
count = start;
return count;
};
return {increment, decrement, value, reset};
}
const c = makeCounter(10, 5);
c.increment();
c.increment();
c.decrement();
console.log(c.value(), c.reset(), c.value());Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
makeCounter returns four functions that share one private count; two counters keep their own counts. makeFormatter reads its settings once and returns a formatter that uses them on every call. pipe joins withVat and euro into a price tag. Run it, then move let count = start; out of makeCounter (writing let count = 0; at the top of the file) and watch the two counters start to share one count.
main.js
// A small closure library: counters, formatters and pipe.
function makeCounter(start = 0, step = 1) {
let count = start;
const increment = () => {
count += step;
return count;
};
const decrement = () => {
count -= step;
return count;
};
const value = () => count;
const reset = () => {
count = start;
return count;
};
return {increment, decrement, value, reset};
}
function makeFormatter(options = {}) {
const prefix = options.prefix ?? "";
const suffix = options.suffix ?? "";
const decimals = options.decimals ?? 2;
return (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`;
}
const pipe = (first, second) => (value) => second(first(value));
const visits = makeCounter();
const tickets = makeCounter(100, 10);
visits.increment();
visits.increment();
tickets.increment();
tickets.increment();
tickets.decrement();
console.log("visits:", visits.value(), "tickets:", tickets.value());
console.log("after reset:", visits.reset(), tickets.reset());
const euro = makeFormatter({suffix: " EUR"});
const dollar = makeFormatter({prefix: "$"});
const percent = makeFormatter({suffix: "%", decimals: 1});
console.log(euro(4.5), dollar(19.9), percent(12.34));
const withVat = (net) => net * 1.19;
const priceTag = pipe(withVat, euro);
console.log(priceTag(10), priceTag(25));
Run it with
node main.jsOutput
visits: 2 tickets: 110
after reset: 0 100
4.50 EUR $19.90 12.3%
11.90 EUR 29.75 EURTab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Exercise 1 of 3
makeCounter(start = 0, step = 1) should return {increment, decrement, value, reset}. Each counter starts at start, moves by step, and reset goes back to start and returns it. In the starter, every counter shares one count at the top of the file, and start and step are ignored. Fix it so that each counter keeps its own private count.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Run the tests: the second counter goes on from where the first stopped. How many count variables are there?
Move the count declaration inside makeCounter and start it at start: let count = start;. Then every call makes its own.
Use step in increment and decrement, and start in reset.
One way to solve it. Yours can look different and still pass the checks.
// Each counter must keep its own count, starting at start and moving by step.
export function makeCounter(start = 0, step = 1) {
let count = start;
const increment = () => {
count += step;
return count;
};
const decrement = () => {
count -= step;
return count;
};
const value = () => count;
const reset = () => {
count = start;
return count;
};
return {increment, decrement, value, reset};
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Each counter must keep its own count, starting at start and moving by step.
let count = 0;
export function makeCounter(start = 0, step = 1) {
const increment = () => {
count += 1;
return count;
};
const decrement = () => {
count -= 1;
return count;
};
const value = () => count;
const reset = () => {
count = 0;
return count;
};
return {increment, decrement, value, reset};
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {makeCounter} from './main.js';
test('two counters keep their own count', () => {
const a = makeCounter();
const b = makeCounter();
a.increment();
a.increment();
b.increment();
assert.equal(a.value(), 2, `after two increments a.value() is ${a.value()}, expected 2`);
assert.equal(b.value(), 1, `after one increment b.value() is ${b.value()}, expected 1`);
});
test('a counter starts at start and moves by step', () => {
const c = makeCounter(10, 5);
assert.equal(c.value(), 10, `makeCounter(10, 5).value() is ${c.value()}, expected 10`);
const up = c.increment();
assert.equal(up, 15, `the first increment returned ${up}, expected 15`);
});
test('decrement moves back by step', () => {
const c = makeCounter(10, 5);
c.decrement();
const down = c.decrement();
assert.equal(down, 0, `two decrements from 10 by 5 returned ${down}, expected 0`);
});
test('reset goes back to start', () => {
const c = makeCounter(3);
c.increment();
c.increment();
const back = c.reset();
assert.equal(back, 3, `reset() returned ${back}, expected 3`);
assert.equal(c.value(), 3, `after reset() value() is ${c.value()}, expected 3`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 3
makeFormatter(options = {}) already reads prefix, suffix and decimals. Make the arrow it returns format a number: the prefix, then the number with exactly decimals decimal places, then the suffix. So makeFormatter({suffix: " EUR"})(4.5) gives "4.50 EUR" and makeFormatter({prefix: "$"})(19.9) gives "$19.90". The file also exports euro, made with the factory.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Run the tests: the formatter returns the number as it is, with no prefix, suffix or fixed decimals.
amount.toFixed(decimals) gives the number as a string with exactly that many decimals.
Put the three parts into the template literal in order: `${prefix}${amount.toFixed(decimals)}${suffix}`.
One way to solve it. Yours can look different and still pass the checks.
// Return a function that formats a number with prefix, decimals and suffix.
export function makeFormatter(options = {}) {
const prefix = options.prefix ?? "";
const suffix = options.suffix ?? "";
const decimals = options.decimals ?? 2;
return (amount) => `${prefix}${amount.toFixed(decimals)}${suffix}`;
}
export const euro = makeFormatter({suffix: " EUR"});
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Return a function that formats a number with prefix, decimals and suffix.
export function makeFormatter(options = {}) {
const prefix = options.prefix ?? "";
const suffix = options.suffix ?? "";
const decimals = options.decimals ?? 2;
return (amount) => `${amount}`;
}
export const euro = makeFormatter({suffix: " EUR"});
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {makeFormatter, euro} from './main.js';
test('euro(4.5) gives 4.50 EUR', () => {
assert.equal(euro(4.5), '4.50 EUR', `euro(4.5) returned ${JSON.stringify(euro(4.5))}, expected "4.50 EUR"`);
});
test('a prefix goes in front of the number', () => {
const dollar = makeFormatter({prefix: '$'});
assert.equal(dollar(19.9), '$19.90', `the $ formatter returned ${JSON.stringify(dollar(19.9))}, expected "$19.90"`);
});
test('decimals sets the number of decimal places', () => {
const percent = makeFormatter({suffix: '%', decimals: 1});
assert.equal(percent(12.34), '12.3%', `percent(12.34) returned ${JSON.stringify(percent(12.34))}, expected "12.3%"`);
});
test('with no options: two decimals, nothing around them', () => {
const plain = makeFormatter();
assert.equal(plain(3), '3.00', `makeFormatter()(3) returned ${JSON.stringify(plain(3))}, expected "3.00"`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 3 of 3
pipe(first, second) should return a function that runs first on its argument, then second on that result. priceTag = pipe(withVat, euro) should then add 19% VAT and format: priceTag(10) gives "11.90 EUR". The starter runs the two functions in the wrong order. Fix pipe; leave withVat, euro and priceTag as they are.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Run the tests: pipe(addOne, double)(3) gives 7. Which function ran first?
The value must go into first, and first's result into second.
Swap the calls: (value) => second(first(value)).
One way to solve it. Yours can look different and still pass the checks.
// Return a function that runs first, then second on first's result.
export const pipe = (first, second) => (value) => second(first(value));
const withVat = (net) => net * 1.19;
const euro = (amount) => `${amount.toFixed(2)} EUR`;
// A price tag: add VAT, then format.
export const priceTag = pipe(withVat, euro);
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Return a function that runs first, then second on first's result.
export const pipe = (first, second) => (value) => first(second(value));
const withVat = (net) => net * 1.19;
const euro = (amount) => `${amount.toFixed(2)} EUR`;
// A price tag: add VAT, then format.
export const priceTag = pipe(withVat, euro);
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {pipe, priceTag} from './main.js';
const addOne = (n) => n + 1;
const double = (n) => n * 2;
const square = (n) => n * n;
test('pipe runs first, then second', () => {
const got = pipe(addOne, double)(3);
assert.equal(got, 8, `pipe(addOne, double)(3) returned ${got}, expected 8`);
});
test('priceTag(10) gives 11.90 EUR', () => {
assert.equal(priceTag(10), '11.90 EUR', `priceTag(10) returned ${JSON.stringify(priceTag(10))}, expected "11.90 EUR"`);
});
test('a piped function can be piped again', () => {
const got = pipe(pipe(addOne, double), square)(1);
assert.equal(got, 16, `pipe(pipe(addOne, double), square)(1) returned ${got}, expected 16`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsfunction makeCounter(start = 0) {
let count = start;
const increment = () => {
count += 1;
return count;
};
return {increment};
}
const counter = makeCounter;
console.log(counter.increment());
What Node.js prints
TypeError: counter.increment is not a functionWhy, and the fix
Without parentheses, counter is the factory itself, not a counter made by it. The factory has no increment of its own, so counter.increment is undefined, and calling it fails. Call the factory to make a counter: const counter = makeCounter();.
function makeCounter(start = 0) {
let count = start;
const increment = () => {
count += 1;
return count;
};
return {increment};
}
const counter = makeCounter();
counter.increment();
console.log(count);
What Node.js prints
ReferenceError: count is not definedWhy, and the fix
count lives in makeCounter's scope. Only the functions created inside can reach it; the top level of the file cannot. That is what keeps it private. Give the counter a function that returns it, such as value: () => count, and call counter.value().
const euro = (amount) => `${amount.toFixed(2)} EUR`;
console.log(euro("4.5"));
What Node.js prints
TypeError: amount.toFixed is not a functionWhy, and the fix
toFixed is a method of numbers. "4.5" is a string, which has no toFixed, so the call fails. Pass a number, euro(4.5), or turn text into a number first with Number("4.5").
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.