The Testing Academy · Class Notes Wednesday, 28 October (IST)
Live class · study guide

JavaScript hoisting, the TDZ, closures and strings

Why a var can be read before its line and a let cannot: hoisting and the temporal dead zone, for variables, functions, arrow functions and classes. Then closures, where an inner function keeps its outer variables, and a first look at strings: quotes, character codes, indexes and searching. Files 121 to 131.

By Pramod Dutta, The Testing Academy. Study notes from the thirteenth class of Playwright 4x, built from the session recording, the batch repository, which received files 121 to 131 and three cheat-sheet images as the class ended (commit "feat: add hoisting/TDZ, closures and strings"), and the Eraser notes deck, whose section for this class holds the hoisting guide and its comparison table. Every file was run with Node 22, and the output shown is what it prints, errors included. Files that work out values without printing them are shown as pushed, then with console.log added.

Hides every output until you tap Reveal, so you can predict it first.
01

What this class covered

  • Hoisting with var: declared before the code runs, set to undefined
  • let before its line: ReferenceError
  • Hoisting inside a function
  • var in a block, and redeclaring var
  • The temporal dead zone
  • Functions, arrow functions and classes before their line
  • typeof and the temporal dead zone
  • Closures: an inner function keeps its outer variables
  • Strings: quotes, String(), character codes, indexes and searching
  • Tasks, and what comes next
  • Hoisting sets up every declaration before the first line runs; values arrive only when their line does.
  • A var read early gives undefined. A let, const or class read early throws ReferenceError: it is in the temporal dead zone.
  • Function declarations can be called before their line; function expressions and arrow functions cannot.
  • var can be declared again with the same name; let cannot.
  • typeof an undeclared name is "undefined", but typeof a let in its dead zone throws.
  • A closure is an inner function that keeps using its outer function's variables after the outer function has returned.
  • Strings are indexed from 0, and indexOf returns -1 when the text is not there.
02

Hoisting with var

Today's first topic is an interview favourite: hoisting, and with it the temporal dead zone.

TWO PHASES: THE ENGINE PREPARES, THEN RUNS (FILE 121) WHAT YOU WROTE console.log(greeting); var greeting = "Hello"; console.log(greeting); PHASE 1: MEMORY var greeting set to undefined every declaration is set up first Phase 2: run line by line line 1 prints undefined line 2 greeting = "Hello" line 3 prints Hello let and const are set up in phase 1 too, but with no value at all: reading them before their line throws ReferenceError. That stretch is the temporal dead zone.
Hoisting in one picture. The declaration is set up before the first line runs; the value arrives only when its line does.

Lab 121:

121_Var_Hoisting.jsJavaScript
console.log(greeting);
var greeting = "Hello";
console.log(greeting);


// Behind the scenes:
// var greeting;              <-- hoisted with undefined
// console.log(greeting);    <-- undefined
// greeting = "Hello!";      <-- assignment stays in place
// console.log(greeting);    <-- "Hello!"
Output
undefined
Hello

The first line reads greeting before the line that declares it, and gets undefined, not an error. (The comments, copied from the class notes, write the value as Hello!; the code prints Hello.)

What happened is the engine's two phases. When JavaScript runs your code, it first does memory creation: it scans the whole scope, finds every declaration and sets it up. A var is set up with the value undefined. Only then comes execution, line by line, and the assignment happens when its line runs. So in the engine's view, var greeting already exists, as undefined, when line 1 runs. From the class notes: hoisting does not physically move your code; it is a way to picture what the engine does with declarations before it runs anything.

Why does the engine do this first? The instructor's comparison: the first thing checked when you come to class is your homework, because it tells the teacher straight away who is ready. The engine's first job is the same kind of check: what is declared in this scope.

Two panels. Left, titled var, with the sublabel Raised, set to undefined: a flagpole with a plain blush flag raised to the top and flying open. Right, titled let, with the sublabel Raised, not ready yet: the same pole with its flag raised to the top but rolled up and tied with a terracotta ribbon.
The class's analogy: hoisting is like hoisting a flag. Every declaration goes to the top before the code runs, but only var arrives with a value, undefined.

The name comes from hoisting a flag, as on Independence Day: the declaration is raised to the top of its scope before anything else happens.

03

let before its line

A cheat sheet titled Hoisting and the TDZ. Top row, var: console.log(greeting) above var greeting = "Hello";, with an orange tag reading undefined. Bottom row, let: the same console.log(greeting) inside a shaded box labelled TDZ, with a red tag reading ReferenceError, and a green label TDZ ends here at let greeting = "Hello";. A banner at the bottom reads: Declare before you use.
From the batch repo's README: the same early read, with var and with let.

Lab 122, the same idea with let:

122_Let_Hoisting.jsJavaScript
// console.log(greeting); // Error 
let greeting = "Hello";
console.log(greeting);
Output
Hello

The early read on line 1 is commented out. Switch it on and the file stops at once:

Text
122_Let_Hoisting.js:1
console.log(greeting); // Error 
            ^

ReferenceError: Cannot access 'greeting' before initialization

With var the early read gave undefined; with let it is an error. That is why let is preferred: a value used before it exists is a bug, and let reports it instead of hiding it.

let is hoisted too. Early in class, let was described as not hoisted at all. The class's own comparison table, later on, says it is hoisted but not initialized, and that is the accurate version: the engine knows about greeting from the start of the scope, but gives it no value until its line runs. That gap is the temporal dead zone, below.

04

Hoisting inside a function

Lab 123:

123_Fn_Hoisting.jsJavaScript
function getUserStatus(){
    // var status_code; => hoisted
    console.log(status_code); // undefined!
    var status_code = "200";
    console.log(status_code);
}

getUserStatus();
Output
undefined
200

The same thing, one level down. A var inside a function is hoisted to the top of that function, not to the top of the file, so status_code is undefined on line 3 and "200" after line 4, as the comment on line 2 shows.

05

var in a block, and redeclaring var

Lab 124:

124_Let_TDZ.jsJavaScript
var a = "Pramod";
// 10,0000 lines
if(true){
    console.log(a);
    var a = "temp";
    console.log(a);
}
Output
Pramod
temp

Picture 10,000 lines between the first var a and the if block, as the class did. Inside the block, var a = "temp" is not a new variable: var ignores blocks, so it is the same a as on line 1, and it overwrites it. Line 4 prints Pramod, line 6 prints temp, and the outer a is now temp too.

Despite its name, the file uses var, as the repo's README notes. With let inside the block, the block gets its own a, and line 4 now falls in its temporal dead zone:

JavaScript
if(true){
    console.log(a);
    let a = "temp";
    console.log(a);
}
Output
124_Let_TDZ.js:4
    console.log(a);
                ^

ReferenceError: Cannot access 'a' before initialization

Redeclaring. Lab 126:

126_Var.jsJavaScript
var testName = "Login Test";
var testName = "Signup Test";    // No error! Re-declaration allowed with var
var testName = "Search Test";    // Still no error!
console.log(testName); 
Output
Search Test

var lets you declare the same name three times with no error, and the last value wins. In a big codebase that silently overwrites someone else's variable. let refuses:

JavaScript
let testName = "Login Test";
let testName = "Signup Test";
Output
redeclare.js:2
let testName = "Signup Test";
    ^

SyntaxError: Identifier 'testName' has already been declared
06

The temporal dead zone

THE SAME BLOCK WITH VAR AND WITH LET (FILE 125) enter the block line 2: console.log(a) line 3: the declaration var a = 10; a = undefined prints undefined a = 10 let a = 10; the temporal dead zone: a exists but cannot be read a exists, no value ReferenceError TDZ ends: a = 10 var is set to undefined at the start, so it can be read early, wrongly. let has no value until its line runs.
The temporal dead zone runs from the start of the block to the let line. Reading the variable inside it is an error, not a silent undefined.

From the class notes: the temporal dead zone (TDZ) is the time when a variable exists but you are not allowed to use it yet. It runs from the start of the block to the line that declares a let or const. Lab 125:

125_let_tds.jsJavaScript
{
    console.log(a);
    let a = 10;
}
// ReferenceError: Cannot access 'a' before initialization

// Enter Block
//  ↓
// a is created ✅
// if a -> var-> undefined
// if a -> let -> no value
// a has NO value yet ❌ 

// console.log(a) // Error TDZ (let, const), Var => undefined

// let a = 10;
//     ↓
// a = 10 ✅
Output
125_let_tds.js:2
    console.log(a);
                ^

ReferenceError: Cannot access 'a' before initialization

The block starts, a is created, and line 2 tries to read it before line 3 gives it a value. Inside the dead zone the only answer is ReferenceError. Once line 3 runs, the zone ends and a is 10, as the comments at the bottom of the file show.

var has no dead zone. The class described var as sitting in the TDZ with the value undefined. Strictly, var has no TDZ: it is set to undefined the moment its scope starts, which is exactly why it can be read early. The dead zone belongs to let, const and classes, as the last row of the class's table says.

07

Functions, arrow functions and classes before their line

The class asked the same question of every kind of declaration: can you use it before its line? The answers, run one at a time, using the examples from the class notes:

Declared with Used before its line Result
var x = 1; console.log(x) undefined
let x = 1; console.log(x) ReferenceError: Cannot access 'x' before initialization
const x = 1; console.log(x) ReferenceError: Cannot access 'x' before initialization
function sayHello() {...} sayHello() works: prints Hello, World!
var greet = function() {...} greet() TypeError: greet is not a function
const runTest = () => {...} runTest() ReferenceError: Cannot access 'runTest' before initialization
class User {...} new User("Dev") ReferenceError: Cannot access 'User' before initialization
  • Function declarations are hoisted whole, name and body, so you can call them before they appear. The last class used that already.
  • A function expression assigned with var is hoisted like any var: as undefined. Calling undefined is a TypeError.
  • An arrow function in a const follows the const rule, so it is in the dead zone until its line.
  • Classes are hoisted but stay in the dead zone too. Classes come with object-oriented programming, later in the course.

The class's comparison table, from the notes:

Feature var let const function class
Hoisted? Yes Yes Yes Yes, fully Yes
Initialized during hoisting? Yes, as undefined No (TDZ) No (TDZ) Yes, the whole body No (TDZ)
Used before its line undefined ReferenceError ReferenceError works ReferenceError
Scope function block block function block
Can be declared again? Yes No No Yes No
Can be reassigned? Yes Yes No Yes Yes
Must be initialized? No No Yes not applicable not applicable
Has a TDZ? No Yes Yes No Yes
08

typeof and the temporal dead zone

Lab 127:

127_TDZ_TypeOf.jsJavaScript
typeof missingName;

// var missingName= "undefined";
typeof missingName;

It prints nothing: typeof works out an answer and nothing prints it. With the first line wrapped in console.log:

Text
undefined

missingName was never declared anywhere, yet typeof answers "undefined" with no error. Read the name directly, though, and it fails:

JavaScript
console.log(typeof missingName);
console.log(missingName);
Output
undefined
typeof_missing.js:2
console.log(missingName);
            ^

ReferenceError: missingName is not defined

Inside a dead zone, even typeof throws:

JavaScript
{
    console.log(typeof score);
    let score = 100;
}
Output
typeof_tdz.js:2
    console.log(typeof score);
            ^

ReferenceError: Cannot access 'score' before initialization

So typeof is not a safe check: for a name that does not exist it quietly says "undefined", and for a let that is not ready yet it throws. The class's conclusion: an undefined name should be an error, not a quiet "undefined".

typeof creates nothing. In class, the quiet "undefined" was explained as JavaScript creating the variable by default. Strictly, it creates nothing: typeof is the one operator allowed to touch a name that was never declared, and it returns "undefined" for it. The second line above proves it, because missingName is still not defined after the typeof.

09

Closures

A CLOSURE: THE INNER FUNCTION KEEPS ITS OUTER VARIABLES (FILE 128) startBrowser() runs let name = "edge"; INNER FUNCTION installBrowser() console.log(name); then startBrowser has finished reads name from the outer function return THE CALL const runTc = startBrowser(); runTc is now installBrowser LATER runTc(); installBrowser runs, and still sees name prints edge called from outside, installBrowser() is not defined
startBrowser is over by the time runTc() runs, yet name is still there: the returned function took it along. That is a closure.

From the class notes: a closure is when an inner function remembers and can access variables from its outer function, even after the outer function has finished executing. Lab 128:

128_Closure.jsJavaScript
function startBrowser() {
    let name = "edge";

    function installBrowser() {
        console.log(name);
    }

    return installBrowser;
}

const runTc = startBrowser();
runTc();
Output
edge

startBrowser creates name and an inner function, installBrowser, and returns the inner function without calling it. By the time runTc() runs, startBrowser has finished. Yet installBrowser still prints edge: it kept access to name. Asked whether let being block-scoped stops this, the answer was no. The inner function only reads name; it does not change it, and the closure keeps it reachable.

Two panels. Left, titled Outer function, with the sublabel Owns the car: a house with an open garage, a terracotta car inside and a car key on a hook. Right, titled Inner function, with the sublabel Keeps the key: the same house with its door shut and lights off, and the key on the ground, linked by a dotted line to the closed garage.
The class's analogy: in a family, the children can use the parents' car. An inner function can use the outer function's variables, even after the outer function has finished.

The instructor's analogy: in a family, the children can use the parents' car. A normal function's local variables vanish when it ends; an inner function holds on to its outer function's variables.

What closures are for, from the class list: data privacy, function factories, callbacks, and modules. Why put a function inside a function at all? Privacy: if the inner function is never shown, nobody outside even knows it exists. Can you call installBrowser directly from outside? No:

JavaScript
const runTc = startBrowser();
runTc();
installBrowser();
Output
edge
128_Closure.js:13
installBrowser();
^

ReferenceError: installBrowser is not defined

runTc() still prints edge; the direct call fails, because the name installBrowser exists only inside startBrowser.

10

Strings

A cheat sheet titled Strings: Index, length and at(). let str = "Hello"; with the letters H, e, l, l and o in five yellow cells, indexes 0 to 4 below and -5 to -1 above. To the right: str.length gives 5, str[0] gives "H", str.at(-1) gives "o". A banner at the bottom reads: Strings can't be changed in place.
From the batch repo's README: a string is indexed like an array, from 0.

Now the course moves into strings, then objects and object-oriented programming. From the class notes: a string is a sequence of characters, and in testing strings are everywhere: URLs, selectors, assertions, response messages, log parsing. Lab 129:

129_String.jsJavaScript
let url = "https://app.vwo.com";
let status = 'pass';
let message = `Test completed in ${320}ms`;
console.log(message);


// Single quotes
let a = 'hello';

// Double quotes
let b = "world";

// Template literals (backticks) - allows expressions & multiline
let name1 = "Alice";
let msg = `Hello, ${name1}! 2 + 2 = ${2 + 2}`;
console.log(msg);



// Multiline
let report = `
  Test: Login
  Status: Pass
  Duration: 320ms
`;


console.log(String(200));
String(true); //  "true"
String(null); // "null"
String([1, 2]); // "[1,2]"
Output
Test completed in 320ms
Hello, Alice! 2 + 2 = 4
200
  • Single and double quotes make the same kind of string.
  • Backticks make a template literal: ${...} is replaced by the value of what is inside, here 320 and 2 + 2. Asked why the dollar is there: without it, the braces are plain text.
  • Backticks also allow several lines, as report shows.
  • String() turns other values into text. Lines 29 to 31 print nothing; with console.log added they give:
Text
Test completed in 320ms
Hello, Alice! 2 + 2 = 4
200
true
null
1,2

String([1, 2]) is 1,2, without the brackets the comment on line 31 shows; the repo's README makes the same correction.

Character codes. Behind the scenes every character has a number: JavaScript uses ASCII and Unicode codes. From the class notes:

JavaScript
console.log("A".charCodeAt(0)); // 65
console.log("Z".charCodeAt(0)); // 90
console.log("a".charCodeAt(0)); // 97
console.log("z".charCodeAt(0)); // 122
Output
65
90
97
122

Uppercase A to Z are 65 to 90, and lowercase a to z are 97 to 122.

Length and indexes. Lab 130:

130_String_Fn.jsJavaScript
// String Properties & Basic Access

let a = "dasdadas\nadasd";
let a2 = 'dasdad "a"sdsa\ndasd';

let str = "Hello, World!";
console.log(str.length); // start 1
console.log(str[0]); // index = 0
console.log(str[7]);
console.log(str.at(-1));
console.log(str.at(-6));

// charAt()
str.charAt(0); 
str.charCodeAt(0);  // 72
Output
13
H
W
!
W

"Hello, World!" has 13 characters, at indexes 0 to 12, so str[0] is H and str[7] is W. at() counts from the end with a negative number, as with arrays: at(-1) is ! and at(-6) is W. The two calls at the bottom work out answers without printing them; printed, charAt(0) gives H and charCodeAt(0) gives 72, the code for H:

Text
13
H
W
!
W
H
72

Searching. Lab 131:

131_String_Search.jsJavaScript
// Searching & Checking

let url = "https://staging.vwo.com/api/login?retry=true";
// includes()
url.includes("staging");  
url.includes("production");

// startsWith / endsWith
url.startsWith("https");
url.startsWith("http://"); 
url.endsWith("true"); 

// indexOf / lastIndexOf
console.log(url.indexOf("a"));
console.log(url.lastIndexOf("a"));
console.log(url.indexOf("nothere"));

// ASCII -> A -> 65
Output
10
24
-1

Only the three indexOf lines print. The class noticed in class: the includes, startsWith and endsWith lines answer true or false, but nothing prints them. Printed, they give:

Text
true
false
true
false
true
10
24
-1
THE URL IN FILE 131, ONE CHARACTER PER INDEX h t t p s : / / s t a g i n g . v w o . c o m / a p i / l o g i n ? r e t r y = t r u e 0 8 10 24 33 43 url.length 44 url[8] "s" url.indexOf("a") 10 url.lastIndexOf("a") 24 url.indexOf("nothere") -1 url[url.length - 1] "e" Indexes start at 0, so a 44-character string ends at index 43. A search that finds nothing gives -1.
The two as sit at 10 and 24, the answers indexOf and lastIndexOf give in file 131.
Expression Value
url.length 44
url[0] "h"
url[8] "s"
url.indexOf("a") 10
url.lastIndexOf("a") 24
url.slice(0, 5) "https"
url.slice(8, 23) "staging.vwo.com"
url[url.length - 1] "e"

A learner asked whether lastIndexOf("a") should be 21. It is 24: the second a is the one in api, at index 24. indexOf works with any text, not just one character: "nothere" is not in the URL, so the answer is -1, as with arrays.

11

Tasks and announcements

  • The 120 exercises: finish at least 120 and push them to your GitHub before Friday's class. Many have; some have not. From here the pace picks up, into TypeScript and Playwright, and the exercises on GitHub are what make you confident.
  • Push today's files too: the strings chapter is in the batch repo.
  • Doubts go in the doubt thread, which Meeti has posted.
  • Recordings or sign-in problems: the support team will contact you and help.
  • Next class: more string methods, then multi-dimensional arrays; after that objects and object-oriented programming, with TypeScript. Higher-order functions are dropped from this batch, because they are rarely used in Playwright automation; the earlier batches' videos still cover them.
  • Why JavaScript first: Playwright in this course uses TypeScript, a bigger brother of JavaScript. As the instructor put it, you learn ABCD before you learn to make words.

Task. Finish at least 120 exercises and push them to GitHub before Friday's class.

Interview questions. What is hoisting? What does a var give if you read it before its line, and what does a let do? Can you call a function expression before its line? What does typeof give for a name that was never declared, and for a let in its dead zone? What is a closure?

Show the answer

Answer. Hoisting sets up every declaration in a scope before the code runs: the declarations, not the values. A var read early gives undefined; a let throws ReferenceError, because it is in the temporal dead zone until its line. A function declaration can be called early, but a function expression cannot: as a var it is still undefined, and calling it is a TypeError. typeof a name that was never declared is "undefined"; typeof a let in its dead zone throws ReferenceError. A closure is an inner function that keeps access to its outer function's variables after the outer function has returned, the way runTc() still prints edge in lab 128.