Three ways to write one
- A declaration is hoisted completely, which lets you put helpers below the code that uses them.
- An arrow with no braces returns its expression. With braces you must write
return. - To return an object literal from a concise arrow, parenthesise it:
() => ({ ok: true }).
// declaration — hoisted, so it can be called above where it is written
function area(w, h) {
return w * h;
}
// expression — not hoisted; exists from this line on
const area = function (w, h) {
return w * h;
};
// arrow — not hoisted, no own `this`, and concise
const area = (w, h) => w * h;
Parameters
function tag(name, attrs = {}, ...children) {
// attrs defaults only when undefined is passed — null does NOT trigger it
}
tag("div"); // attrs is {}
tag("div", null); // attrs is null
// a default can use an earlier parameter
function slice(list, start = 0, end = list.length) { }
// more than three parameters: take an object instead
function request(url, { method = "GET", retries = 3, signal } = {}) { }
request("/api", { retries: 1 });
Why an options object
Nobody can remember the order of five arguments, and request(url, null, undefined, true) is unreadable. Named options can also be extended without breaking every call.
Returning
function noReturn() { }
noReturn(); // undefined
// automatic semicolon insertion puts a ; after return
function broken() {
return
{ ok: true }; // returns undefined!
}
function fixed() {
return {
ok: true,
};
}
Arrow functions and this
const timer = {
seconds: 0,
start() {
// an arrow keeps the surrounding `this`, which is the timer
setInterval(() => { this.seconds += 1; }, 1000);
// a function expression would get its own `this` — undefined here
setInterval(function () { this.seconds += 1; }, 1000); // TypeError
},
};
So do not use an arrow as a method
const obj = { name: "a", get: () => this.name } does not work: the arrow took this from the module, not from obj.
Pure functions
Most code has to do something impure eventually — write to the DOM, save a file, send a request. The useful discipline is to keep the calculation pure and put the side effect in one obvious place.
// pure: easy to test, safe to move, safe to call twice
const withVat = (minor, rate) => Math.round(minor * (1 + rate / 100));
// impure: reads outside state and changes its argument
let rate = 18;
function addVat(order) {
order.total = Math.round(order.total * (1 + rate / 100));
}
One job
Each of those can be tested on its own, and the one that changes next month can change without touching the others.
// before
function save(form) {
const data = Object.fromEntries(new FormData(form));
if (!data.email.includes("@")) { showError("Bad email"); return; }
data.total = data.qty * 100;
fetch("/api", { method: "POST", body: JSON.stringify(data) });
}
// after
const read = (form) => Object.fromEntries(new FormData(form));
const validate = (data) => (data.email.includes("@") ? null : "Bad email");
const priced = (data) => ({ ...data, total: data.qty * 100 });
const send = (data) => fetch("/api", { method: "POST", body: JSON.stringify(data) });
Functions as values
const by = (key) => (a, b) => a[key] - b[key];
doors.sort(by("days"));
const handlers = {
save: () => { },
cancel: () => { },
};
handlers[action]?.();
// a function that returns a function is how you make a configurable one
const prefix = (p) => (s) => `${p}${s}`;
const mm = prefix("↔ ");
Naming
- A verb:
formatPrice,loadDoors,isValid,hasError. - A predicate returns a boolean and reads as a question:
isReady(x). - A getter returns a value and does nothing else:
priceOf(x). - If the name needs "And" in it —
validateAndSave— it is two functions.