What this class was, and what it was not
Two thirds of the session was a recap. Hoisting and the temporal dead zone had been covered the previous class, and the class walked them again before adding scope and closures on top, because closures do not make sense until you already believe that a variable can exist without being usable.
Some framing before the code, because it changes how you read the rest: closures are rarely used in Playwright automation. They are an interview topic. That was said plainly in class and it is repeated here rather than quietly dropped, because studying this for the wrong reason wastes the weekend. Learn it to answer the question, not because your framework will be full of them.
Hoisting, in one table
Hoisting is JavaScript moving declarations to the top of their scope before any line runs. Every declaration is hoisted. What differs is whether it arrives initialised.
| Feature | var |
let |
const |
function declaration | class declaration |
|---|---|---|---|---|---|
| Hoisted? | Yes | Yes | Yes | Yes, fully | Yes |
| Initialised during hoist? | Yes, undefined |
No, TDZ | No, TDZ | Yes, full body | No, TDZ |
| Access before declaration | undefined |
ReferenceError | ReferenceError | Works normally | ReferenceError |
| Scope | Function | Block | Block | Function | Block |
| Re-declarable | Yes | No | No | Yes | No |
| Re-assignable | Yes | Yes | No | Yes | Yes |
| In a TDZ? | No | Yes | Yes | No | Yes |
The temporal dead zone is the stretch between the top of the block and the line where a let or const is actually declared. The binding exists in that stretch. Touching it throws.
var and function declarations arrive usable.The behaviour, in code that was run rather than asserted:
console.log(a); // undefined <- var is hoisted AND initialised
var a = 10;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
Read the two error messages carefully, because they are different and the difference is the exam question. An undeclared variable gives b is not defined. A let in its dead zone gives Cannot access 'b' before initialization. The second message is proof the binding exists: JavaScript can only complain about accessing it before initialisation if it already knows it is there.
Functions: three forms, three behaviours
// 1. Declaration: fully hoisted, callable from above
greet(); // works
function greet() { return 'hi'; }
// 2. Expression assigned to var: the variable hoists as undefined
speak(); // TypeError: speak is not a function
var speak = function () { return 'hi'; };
// 3. Arrow assigned to const: hoisted into the TDZ
shout(); // ReferenceError: Cannot access 'shout' before initialization
const shout = () => 'hi';
The class kept calling JavaScript a funny language over this, and the joke has a point: three ways of writing the same function fail in three different ways.
The distinction between cases 2 and 3 is worth holding on to. A TypeError means the name resolved and the value was not callable, undefined in this case. A ReferenceError means the name could not be reached at all. So the error type alone tells you whether var or const was used.
typeof is the one surprise in the set. typeof someUndeclared returns the string 'undefined' and never throws, which is why it was long used as an existence check. typeof on a let inside its TDZ does throw. The safe-looking guard is not safe for block-scoped declarations.
Functions inside blocks
// avoid this: behaviour has historically varied between engines
if (true) {
function helper() { return 'inside if'; }
}
// do this instead
let helper;
if (true) {
helper = function () { return 'inside if'; };
}
The rule given was blunt: never declare a function inside an if block. Assign a function expression to a variable declared outside.
The rules, and the linter that will enforce them
These were given as the framework's coding standard, to be enforced by ESLint later in the course rather than by memory:
constby default. Switch toletonly when you need to reassign. Nevervar.- Declare at the top of the scope. TDZ protects you; readability still matters.
- Function declarations for hoisted utilities, arrow functions with
constfor everything else. - Never declare a function inside a block.
letinforloops,constinfor...ofandfor...in.
{
"rules": {
"no-var": "error",
"prefer-const": "error",
"no-use-before-define": "error",
"block-scoped-var": "error"
}
}
Scope: what a function can see
Before closures, the plumbing. A variable declared outside every function is global. A variable declared inside a function is local to it.
const environment = 'staging'; // global
function runTest() {
const timeout = 30000; // local to runTest
console.log(environment); // 'staging' <- inner can read outer
}
runTest();
console.log(timeout); // ReferenceError: timeout is not defined
Nesting extends the rule rather than changing it:
function outer() {
const x = 10;
function inner() {
const y = 20;
console.log(x); // 10 <- inner sees outer
}
inner();
// console.log(y); // ReferenceError: outer cannot see inner
}
The analogy that landed in the room came from a learner, not the slide: the principal can walk into any classroom, a student cannot walk into any class. Inner functions see outward. Outer functions cannot see in.
Closures
A closure is when an inner function remembers and can access variables from its outer function, even after the outer function has finished executing.
That last clause is the whole idea. Everything before it is just scope, which you already had.
The example from the notes, framed around browsers:
function startBrowser() {
let name = 'edge';
function installBrowser() {
console.log(name);
}
return installBrowser;
}
const runTc = startBrowser();
runTc(); // edge
startBrowser finished on the line that assigned runTc. Its local name should be gone. It is not, because installBrowser still refers to it, so the engine keeps it alive.
The motivation offered was hiding: a caller can run the installer without being able to see or change which browser it installs. Collapse the outer function in the editor and the inner one disappears from view entirely. That is encapsulation without a class.
A counter, and the shape you will be asked to write
The interview version returns several functions instead of one, all sharing the same private variable:
function makeCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
get: () => count,
};
}
const counter = makeCounter(0);
counter.increment();
counter.increment();
counter.increment();
console.log(counter.get()); // 3
counter.decrement();
console.log(counter.get()); // 2
start = 0 is a default parameter, so makeCounter() and makeCounter(0) behave identically.
The part worth slowing down on is that all three arrow functions close over the same count. There is one variable, not three copies. increment moves it, get reads the moved value. Call makeCounter a second time and you get a second, completely independent count.
Retry attempts
The first closure in the session that looks like something you might actually write:
function maxRetry(max) {
let attempts = 0;
function tryAgain(testName) {
attempts++;
if (attempts > max) {
return `${testName}: max retries exceeded`;
}
return `${testName}: attempt ${attempts}`;
}
return tryAgain;
}
const runRetry = maxRetry(3);
console.log(runRetry('login')); // login: attempt 1
console.log(runRetry('login')); // login: attempt 2
console.log(runRetry('login')); // login: attempt 3
console.log(runRetry('login')); // login: max retries exceeded
attempts survives between calls without being global and without being a property anyone can reach. A counter that nothing else can accidentally reset is the point.
A question from the room asked whether runRetry.tryAgain() would also work. It does not, and the reason is a useful check on whether the shape has landed: maxRetry returns the function itself, so runRetry is tryAgain under a different name. There is no object to reach through.
A rate limiter
The smallest of the three:
function makeLimiter(limit) {
let calls = 0;
return function check() {
return ++calls <= limit;
};
}
const limiter = makeLimiter(3);
console.log(limiter()); // true
console.log(limiter()); // true
console.log(limiter()); // true
console.log(limiter()); // false
++calls <= limit increments first, then compares. Calls one, two and three give 1 <= 3, 2 <= 3, 3 <= 3, all true. The fourth gives 4 <= 3, false. The <= rather than < is what makes the third call the last allowed one.
Where closures actually turn up in a Playwright project: a fixture that captures configuration, a helper factory that bakes in a base URL, a retry or polling wrapper. That is a short list, and every item is something you write once. This is genuinely an interview topic more than a daily one, which is exactly why it deserves a weekend rather than a career.
Strings, the first ten minutes
The class ended with the opening of strings, enough to make the next session continuous. All three quoting styles produce a string, and template literals add interpolation and real multi-line text:
let url = "https://app.vwo.com";
let status = 'pass';
let message = `Test completed in ${320}ms`;
Single and double quotes are interchangeable in JavaScript. Pick based on what is inside the string so you can avoid escaping. There is no separate character type: a single character is a string of length one.
Index access, at(), charAt(), charCodeAt(), includes, startsWith, indexOf and lastIndexOf were introduced and then handed to the next class, which covered them properly. Two facts to carry forward: .length is a count, so it starts at one, while an index is an offset, so it starts at zero, and a search that finds nothing returns -1, never undefined.
The full treatment, with every method and its verified output, is in the next class notes.
Tasks and announcements
- The weekend target is 120. That is the 100 JavaScript lab exercises plus at least 20 of the coding questions. Two hours across Saturday and Sunday was the estimate given.
- Push the exercises to GitHub. Exercises that only exist on your laptop do not count.
- The coding question bank grows from 89 to 150, curated from questions companies actually asked, not invented for practice.
- Re-type the three closure examples yourself. Reading them is not the same as writing them, and this is the topic where that gap is widest.
- Ask in the doubt thread. A thread is open for this session, and it was pointed out more than once that it is underused.
- Coming next: the rest of strings, then callbacks and promises.
- Coming this week: Playwright architecture.
- Later: ESLint rules in the framework to ban
varand enforceconstwith arrow functions.
Interview forms of today's material: what is the difference between b is not defined and Cannot access 'b' before initialization? Why does a function expression assigned to var fail with a TypeError rather than a ReferenceError? Write makeCounter from memory and explain why two calls to it do not share a count. Why is typeof not a safe existence check for a let?