Chapter 03
JavaScript essentials
The parts of JavaScript that confuse people for years if skipped: declarations, equality, closures, destructuring, this and asynchronous code.
let, const and var
Also called block scope, hoisting
- What
constdeclares a binding that cannot be reassigned,letone that can, and both are scoped to the block.varis the old function-scoped form and is hoisted.- Why
- Default to
const, useletwhen you must reassign, and nevervar: block scoping removes a family of loop and closure bugs.
const user = { name: 'Ada' };
user.name = 'Grace';
let count = 0;
count += 1;
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
// prints 3 3 3; with let it prints 0 1 2Watch out
Thinking const makes an object immutable: you cannot reassign the name, but you can still change its properties.
=== and ==
Also called strict equality, type coercion
- What
===compares value and type with no conversion.==converts types first by rules few people remember, so0 == ''is true.- Why
- Always use
===and!==so comparisons mean what they say. Linters flag==for this reason.
console.log(0 == ''); // true
console.log(null == undefined); // true
console.log(0 === ''); // false
console.log(NaN === NaN); // false
console.log(Number.isNaN(NaN)); // trueWatch out
Testing for NaN with ===: NaN is not equal to itself, so use Number.isNaN.
Closures
Also called lexical scope
- What
- A closure is a function that remembers the variables of the scope it was created in, even after that outer function has returned.
- Why
- Closures power callbacks, event handlers, private state and factories. React hooks and most async code rely on them.
function makeCounter() {
let count = 0;
return () => {
count += 1;
return count;
};
}
const next = makeCounter();
next();
console.log(next()); // 2Watch out
A callback that captured an old value of a variable (a stale closure) keeps using it after the variable has changed.
Destructuring and spread
Also called rest parameters, spread operator, ...
- What
- Destructuring pulls values out of arrays and objects into variables in one statement. The
...syntax spreads items into a new array or object, or collects the rest. - Why
- It makes function options, copying with changes and working with API responses short and readable.
const user = { name: 'Ada', role: 'admin', city: 'London' };
const { name, ...rest } = user;
const [first, second = 'none'] = ['a'];
const moved = { ...user, city: 'Paris' };
function greet({ name, greeting = 'Hi' }) {
return `${greeting}, ${name}`;
}Watch out
Expecting spread to deep copy: nested objects are still shared, so use structuredClone for a deep copy.
The event loop
Also called call stack, microtasks, task queue
- What
- JavaScript runs your code on one thread. When the current code finishes, the event loop runs queued microtasks (promise callbacks) and then the next task (timers, I/O, events).
- Why
- It explains why a slow loop freezes a page or server, and why a
setTimeout(fn, 0)callback runs after promise callbacks.
console.log('1');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('2');Watch out
Running heavy CPU work on the main thread, which blocks every other request or click until it finishes.
Promises and async/await
Also called async, await, Promise.all
- What
- A promise represents a value that will arrive later, or fail.
awaitpauses anasyncfunction until the promise settles, so asynchronous code reads top to bottom. - Why
- Network calls, file reads and timers are all asynchronous in JavaScript.
Promise.allruns independent operations at the same time instead of one after another.
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function loadAll(ids) {
return Promise.all(ids.map((id) => loadUser(id)));
}
loadAll([1, 2]).then(console.log).catch(console.error);Watch out
Forgetting await, so you get a pending promise instead of the data and errors go unhandled.
this
Also called bind, arrow functions
- What
- In a normal function,
thisis decided by how the function is called, not where it is written. Arrow functions have nothisof their own and use the surrounding one. - Why
- It explains methods that break when passed as callbacks, and why arrow functions are the usual choice inside methods.
const timer = {
seconds: 0,
start() {
setInterval(() => { this.seconds += 1; }, 1000);
},
};
timer.start();
const start = timer.start;
start(); // this is no longer timerWatch out
Passing obj.method as a callback, which detaches it from obj so this is lost; use an arrow function or bind.