Reading: You Don't Know JS Yet 2 - Scope & Closures (Chap 1 — Chap 5)

Anh-Thi Dinh
👉 List of all notes for this book. IMPORTANT UPDATE Nov 18, 2024: I've stopped taking detailed notes from the book and now only highlight and annotate directly in the PDF files/book. With so many books to read, I don't have time to type everything. In the future, if I make notes while reading a book, they'll contain only the most notable points (for me).

Chap 1 — What’s the Scope?

  • How does JS know which variables are accessible by any given statement, and how does it handle two variables of the same name? ← Answer: Scope
  • First step: How the JS engine processes our program before it runs.

About This Book

  • Focus: the scope system and its function closures + the power of the module design pattern.
  • Closure: JS functions can be assigned and passed like numbers or strings. They can maintain the original scope of their variables no matter where the functions are executed.
  • Module: a code organization pattern characterized by public methods that have access to hidden variables/functions in the scope of the module.

Compiled vs. Interpreted

Compiled vs. Interpreted Code
  • Code compilation: a set of steps that process your code and turn it into a list of instructions the computer can understand. The whole source code is transform at once.
  • Interpretation: It transform your code into machine-understandable instructions but with a different processing model. The source code is transformed line by line.
  • Modern JS engines actually employ numerous variations of both compilation and interpretation in the handling of JS programs.
  • JS is most accurately portrayed as a compiled language.

Compiling Code

  • In classic compiler theory, a program is processed by a compiler in three basic stages:
      1. Tokenizing/Lexing: var a = 2; will be tokenized as var, a, =, 2 and ;.
      1. Parsing: convert array of tokens into Abstract Syntax Tree (AST) (a tree of nested elements). var a = 2 may start with a top-level node called VaraibleDeclaration, with a child node called Identifier (a),…
      1. Code Generation: convert AST to executable code.
  • The JS engine is vastly more complex than just these three stages. There are steps to optimize the performance of the execution.
  • Required: Two phasesparsing/compilation first, then execution. To prove this, there are 3 characteristics:
    • Syntax Errors from the Start: How JS knows ."Hi" without printing “greeting”? ← It reads whole code first!
      • 1var greeting = "Hello";
        2console.log(greeting);
        3
        4greeting = ."Hi"; // SyntaxError: unexpected token .
        This example isn’t well-formed. “Hello” won’t printed as expected!
    • Early Errors: How JS know duplicated parameters “greeting” (becauase of "use strict") without printing “Howdy”? ← It reads the whole code first!
      • 1console.log("Howdy");
        2
        3saySomething("Hello","Hi");
        4// Uncaught SyntaxError: Duplicate parameter name not 
        5// allowed in this context
        6
        7function saySomething(greeting, greeting) {
        8	"use strict";
        9	console.log(greeting);
        10}
        This example is well-formed. “Howdy” isn’t printed!
    • Hoisting: How JS know “greeting” is a block-scoped variable whereas there is var greeting...? ← It reads the whole code first!
      • Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution. Ref.
        1function saySomething() {
        2	var greeting = "Hello";
        3	{
        4		greeting = "Howdy"; // error comes from here
        5		let greeting = "Hi"; // defines block-scoped variable "greeting"
        6		console.log(greeting);
        7	}	
        8}
        9saySomething();
        10// ReferenceError: Cannot access 'greeting' before initialization

Compiler Speak

  • How the JS engine identifies variables and determines the scopes of a program as it is compiled?
1var students = [
2	{ id: 14, name: "Kyle" },
3	{ id: 73, name: "Suzy" },
4	{ id: 112, name: "Frank" },
5	{ id: 6, name: "Sarah" }
6];
7
8function getStudentName(studentID) {
9	for (let student of students) {
10		if (student.id == studentID) {
11			return student.name; 
12		}
13	}
14}
15
16var nextStudent = getStudentName(73);
17console.log(nextStudent); // Suzy
  • All variables/identifiers in a program is either target of an assignment (LHS) or the source of a value (RHS).
    • Target: if there is a value is being assigned to it.
    • Source: otherwise!
  • JS engine must first label variables as “target” or “source”.
  • Target: different ways
    • 1students = [ //...
      1for (let student of students) {
      student is assigned a value of students.
      1getStudentName(73)
      The argument studentID of getStudentName() is assigned value 73.
      1function getStudentName(studentID) {
      A function declaration is a special case of a target reference.
  • Source:
    • 1for (let student of students) // "students" is a source reference
      2
      3if (student.id == studentID) // both "student" and "studentID" are sources
      4
      5return student.name // "student" is a source reference
      6
      7getStudentName(73) // "getStudentName" is a source reference (resolves to a function reference value)

Cheating: Run-Time Scope Modifications

  • Clear: scope is determined as the program is compiled, not affected by runtime conditions. ← in non-strict-mode, we can cheat this rule! ← shouldn’t use!
    • eval() modifies the scope var and function at runtime
      • 1function badIdea() {
        2	eval("var oops = 'Ugh!';"); // without eval, "oops" doesn't exist
        3	console.log(oops); 
        4}
        5
        6badIdea(); // Ugh!
    • with : dynamically turns an object into a local scope
      • 1var badIdea = { oops: "Ugh!" };
        2
        3with (badIdea) {
        4	console.log(oops); // Ugh!
        5}
        “badIdea”’s properties become variables in the new scope
  • Shoule use strict-mode and avoid eval() and with!

Lexical Scope

  • Lexical Scope: scopes determined at compile time.
  • The key idea of "lexical scope" is that it's controlled entirely by the placement of functions, blocks, and variable declarations, in relation to one another.
    • Examples:
    • Inside fuction → scope in that function.
    • let / const → the nearest { .. } block.
    • var → enclosing function
  • Target / Source should be resolved as coming from one of the scopes that are lexical available to it. Otherwise, “undeclared” error!
  • In complilation, no program has been executed yet (no reserving memory for scopes and variables). Instead, compilation creates a map of all the lexical scopes that lays out what the program will need while it executes.

Chap 2 — Illustrating Lexical Scope

The goal here is to think about how your program is handled by the JS engine in ways that more closely align with how the JS engine actually works. ← illustrate via some metaphors.

Marbles, and Buckets, and Bubbles... Oh My!

  • Understanding scope → sorting colored marbles into buckets of their matching color.
    • marbles = variables
    • buckets = scopes (function and blocks)
1// outer/global scope: RED
2
3var students = [
4	{ id: 14, name: "Kyle" },
5	{ id: 73, name: "Suzy" },
6	{ id: 112, name: "Frank" },
7	{ id: 6, name: "Sarah" }
8];
9
10function getStudentName(studentID) {
11	// function scope: BLUE
12	for (let student of students) {
13		// loop scope: GREEN
14		if (student.id == studentID) {
15			return student.name;
16		}
17	}
18}
19
20var nextStudent = getStudentName(73);
21console.log(nextStudent); // Suzy
The codes.
The illustration.
  • Each scope bubble is entirely contained within its parent scope bubble—a scope is never partially in two different outer scopes.
  • id, name and log are properties, not variables (not marbles) → They don’t get colored! Check .

A Conversation Among Friends

JS as an conversations between friends:
  • Engine: responsible for start-to-finish compilation and execution of our JavaScript program.
  • Compiler: one of Engine's friends; handles all the dirty work of parsing and code-generation (see previous section).
  • Scope Manager: another friend of Engine; collects and maintains a lookup list of all the declared variables/identifiers, and enforces a set of rules as to how these are accessible to currently executing code.
You need to think like the Engine and their friends think.
1var students = [
2	{ id: 14, name: "Kyle" },
3	{ id: 73, name: "Suzy" },
4	{ id: 112, name: "Frank" },
5	{ id: 6, name: "Sarah" }
6];
7
8function getStudentName(studentID) {
9	for (let student of students) {
10		if (student.id == studentID) {
11			return student.name; 
12		}
13	} 
14}
15
16var nextStudent = getStudentName(73);
17
18console.log(nextStudent); // Suzy
Conversation between Compiler and Scope Manager.
Conversation between Engine and Scope Manager.

Nested Scope

  • For this line of code,
    • 1for (let student of students) {
       
       
      Engine exhausts all lexically available scopes (moving outward) → cannot find → errors like ReferenceError.
  • Lookup Failures
    • “Not defined” is different from undefined. The latter is “declared” but has no value yet!
      1var studentName;
      2typeof studentName; // "undefined" (declared but has no value)
      3typeof doesntExist; // "undefined" (not declared)
      Same undefined but different meanings.
  • Global... What!?
    • 1function getStudentName() {
      2	// assignment to an undeclared variable :(	
      3	nextStudent = "Suzy";
      4}
      5getStudentName();
      6console.log(nextStudent);
      7// "Suzy" -- oops, an accidental-global variable!
      Scope Manager never heard of nextStudent (local and global) + we are in non-strict mode → ⚠️ He creates one global!

Continue the Conversation

Try with your friends a real conversation about your real codes like above conversations between these 3 guys!

Chap 3 — The Scope Chain

  • Scope chain = The connections between scopes that are nested within other scopes.
  • Lookup moves upward/outward only.

"Lookup" Is (Mostly) Conceptual

  • Engine asks Scope Manager → it proceeds upward/outward back through the chain of nested scopes until variable is found.
    • Why students is in the RED? → it’s only found in the outer scope (1). The same for studentID.
  • The marble's color is known from compilation → be stored with each variable’s entry in the AST. → Engine doesn’t need to lookup through a bunch of scopes ← a key optimization benefit of lexical scope.

Shadowing

  • If you need to maintain 2 or more variables of the same name → you must use separate (often nested) scopes.
    • 1var studentName = "Suzy";
      2
      3function printStudent(studentName) {
      4	studentName = studentName.toUpperCase();
      5	console.log(studentName);
      6}
      7
      8printStudent("Frank"); // FRANK
      9printStudent(studentName); // SUZY
      10console.log(studentName); // Suzy
  • Follow the “lookup” rules → 3 studentNames of printStudent() are belongs to BLUE(2) because it stops when we look up until the one inside () . The RED(1) studentName (1st line) is never reached!
    • This is a key aspect of lexical scope behavior, called shadowing. The studentName (parameter) shadows the 1st studentName (global)! ← variable in the nested shadows one outside
  • Global Unshadowing Trick: (this one isn’t good practice, don’t use) → you want to access the 1st studentName inside printStudent ? → use a global variable (eg. window in JS for browsers)
    • 1var studentName = "Suzy";
      2
      3function printStudent(studentName) {
      4	console.log(studentName);
      5	console.log(window.studentName); // 👈 HERE!
      6}
      7
      8printStudent("Frank");
      9// "Frank"
      10// "Suzy"
      1var one = 1;
      2let notOne = 2;
      3const notTwo = 3;
      4class notThree {}
      5
      6console.log(window.one); // 1 
      7console.log(window.notOne); // undefined 
      8console.log(window.notTwo); // undefined 
      9console.log(window.notThree); // undefined
      window.studentName is a mirror of the global studentName (change one, the other changes).
  • Copying Is Not Accessing
    • 1var special = 42;
      2
      3function lookingFor(special) {
      4	var another = { special: special };
      5
      6	function keepLooking() {
      7		var special = 3.141592;
      8		console.log(special);
      9		console.log(another.special); // Ooo, tricky!
      10		console.log(window.special);
      11	}
      12	keepLooking();
      13}
      14lookingFor(112358132134);
      15// 3.141592 
      16// 112358132134 
      17// 42
      Note that: the another.special isn’t the BLUE(2) special (lokingFor ’s parameter) → shadowing no longer applies.
  • Illegal Shadowing: let can shadow var but var cannot shadow let
    • 1function something() {
      2	var special = "JavaScript";
      3	{
      4		let special = 42; // totally fine shadowing
      5	}
      6}
      1function another() {
      2	// ..
      3	{
      4		let special = "JavaScript";
      5		{
      6			var special = "JavaScript";
      7			// ^^^ Syntax Error // ..
      8		}
      9	}
      10}
      1function another() {
      2	// ..
      3	{
      4		let special = "JavaScript";
      5		ajax("https://some.url", function callback(){ 
      6			var special = "JavaScript"; // totally fine shadowing
      7			// ..
      8		});
      9	}
      10}
      There is a function boundary in between.

Function Name Scope

  • function askQuestion() {...} → create an identifier in the enclosing scope (function ở trong cái nào là scope trong cái đó).
  • var askQuestion = function(){...} → the same is true for askQuestion as previous
  • var askQuestion = function ofTheTeacher(){...} (named function expression) → askQuestion is in outer scope but ofTheTeacher isn’t!
    • 1var askQuestion = function ofTheTeacher() {
      2	console.log(ofTheTeacher);
      3};
      4
      5askQuestion(); // function ofTheTeacher()...
      6console.log(askQuestion); // function ofTtheTeacher()...
      7console.log(ofTheTeacher); // ReferenceError: ofTheTeacher is not defined
      1var askQuestion = function ofTheTeacher() {
      2	"use strict";
      3	ofTheTeacher = 42; // TypeError <- ofTheTeacher is read-only
      4};

Arrow Functions

  • ES6 added it.
    • 1var askQuestion = () => {
      2	// ..
      3};
  • The assignment to askQuestion creates an inferred name of "askQuestion", but that's not the same thing as being non-anonymous
    • 1askQuestion.name; // askQuestion
  • => arrow functions have the same lexical scope rules as function functions do.

Backing Out

  • A new scope is formed when a function is defined, creating a scope chain that controls variable access.
  • Each new scope has its own variables, and shadowing can occur if a variable name is repeated.
  • The next chapter focuses on the global scope, a primary scope in all JS programs.

Chap 4 — Around the Global Scope

  • A program's outermost scope is all that important in modern JS.
  • Global scope is (still) helpful.
  • Understanding global scope is key to structuring programs with lexical scope.

Why Global Scope?

Most JS apps are composed of individual JS files. How they are combined (in a single runtime context)? → 3 main ways:
  1. If you use ES Modules → each module uses import to other modules. They don’t need any shared outer scope, just via these imports.
  1. If you use a bundler → all files are concatenated before delivery to JS engine. ← In build steps, contents of file are wrapped in a single enclosing scope, like
    1. 1(function wrappingOuterScope(){
      2	var moduleOne = (function one(){
      3		// ...
      4	})();
      5
      6	var moduleTwo = (function two(){
      7		// ...
      8		function callModuleOne() {
      9			moduleOne.someMethod(); 
      10		}
      11		// ...
      12	})();
      13})();
  1. Without a bundler or non-ES module, files load via <script> in the browser. Without a common scope, they must interact via the global scope.
    1. Each top-level variable in each file will end up as a global variable!
Global scope is also where:
  • JS exposes its built-ins:
    • primitives: undefined, null, Infinity , NaN
    • natives: Date(), Object(), String(), etc.
    • global functions: eval(), parseInt(), etc.
    • namespaces: Math, Atomics, JSON
    • friends of JS: Intl, WebAssembly
  • The environment hosting the JS engine exposes its own built-ins:
    • console (and its methods)
    • the DOM (window, document, etc)
    • timers (setTimeout(..), etc)
    • web platform APIs: navigator, history, geolocation, WebRTC, etc.

Where Exactly is this Global Scope?

Different JS environments handle the scopes of your programs, especially the global scope, differently.
  • ❇️ Browser "Window”
    • 1// example.js
      2var studentName = "Thi";
      3function hello() {
      4	console.log(`Hello, ${ window.studentName }!`);
      5}
      6hello(); // Hello, Thi!
      7window.hello(); // Hello, Thi!
      If above file is integrated in a website via <script> or <script src=...>studentName and hello identifiers are delcared in the global scope.
      ❇️ Globals Shadowing Globals
      A global object property can be shadowed by a global variable:
      1window.something = 42; // global obj prop
      2let something = "Thi";
      3
      4console.log(something); // Thi 
      5console.log(window.something); // 42
      Recall an example from Chap 3.
      1var one = 1;
      2let notOne = 2;
      3const notTwo = 3;
      4class notThree {}
      5
      6console.log(window.one); // 1 
      7console.log(window.notOne); // undefined 
      8console.log(window.notTwo); // undefined 
      9console.log(window.notThree); // undefined
      👌
      Always use var for globals. Reserve let and const for block scopes (See Chap 6).
      ❇️ DOM Globals
      In a browser-hosted JS environment → a DOM element with an id attribute automatically creates a global variable that references it
      1<ul id="my-todo-list">
      2	<li id="first">Write a book</li>
      3</ul>
      1first;
      2// <li id="first">..</li>
      3
      4window["my-todo-list"];
      5// <ul id="my-todo-list">..</ul>
      Never use these global variables!
      ❇️ What's in a (Window) Name?
      1var name = 42;
      2console.log(name, typeof name); // "42" string
      Special case: window.name is a pre-defined global in a browser context.
      1let name = 42;
      2console.log(name, typeof name); // 42 number
      Use let instead. ← this will shadow the global object prop “name”
      1// any other names
      2var thi = 42;
      3console.log(thi, typeof thi); // 42 number
  • Web Workers (WW)
    • Web Workers enable a JavaScript file to run on a separate thread (OS wise) from the main JS program, extending browser-JS behavior.
      WW cannot access DOM (there is no window) but is shared with some web APIs like navigator.
      It doesn’t share the global scope with the main JS program.
      The global object reference is made via self.
      1var studentName = "Kyle"; let studentID = 42;
      2
      3function hello() {
      4	console.log(`Hello, ${ self.studentName }!`);
      5}
      6
      7self.hello(); // Hello, Kyle!
      8self.studentID; // undefined
  • Developer Tools Console/REPL
    • For example, Developer Tools in a web browser has a different JS env. It prioritize the developer convenience.
    • It’s not suitable to determine/verify behaviors of an JS program context!
  • ES Modules (ESM)
    • One of the most obvious impacts of using ESM is how it changes the behavior of the observably top-level scope in a file.
      1var studentName = "Kyle";
      2
      3function hello() {
      4	console.log(`Hello, ${ studentName }!`);
      5}
      6
      7hello(); // Hello, Kyle!
      8export hello;
      If this code is loaded from an ES module → studentName and hello aren’t global vars. Insetad, they are module-wide or “module global”.
      ESM encorages a min usage of global scope!
  • Node
    • Node treats every single .js file that it loads, including the main one you start the Node process with, as a module (ESM or CommonJS). → The top level of your Node programs is never actually the global scope.
      In CommonJS (prev version of Node) → look-alike global declared variables are actually inside a function module (module scope). ← In order to declare a “real” global scope, use global (something like window of a browser JS env, it’s defined by Node).

Global This

Review:
  • Declare a global variable using var, function, let, const, or class.
  • If using var or function, add declarations to the global scope object.
  • Use window (Browser), self (Web Worker), or global (Node) to manipulate global variables.
Note: a function can be dynamically constructed from a string using Function(), similar to eval() ← run in non-strict mode, its this points to the global object.
As of ES2020 → use globalThis for all of different ways to reference to the global scope object.

Chap 5 — The (Not So) Secret Lifecycle of Variables

When Can I Use a Variable?

1greeting(); // Hello!
2
3function greeting() {
4	console.log("Hello!");
5}
This works fine even greeting() is declared after it’s called but why?
The term most commonly used for a variable being visible from the beginning of its enclosing scope, even though its declaration may appear further down in the scope, is called hoisting.
Function hoisting → When a function declaration's name identifier is registered at the top of its scope, it's additionally auto initialized to that function's reference. That's why the function can be called throughout the entire scope!
❇️ Hoisting: Declaration vs. Expression
  • Function hoisting only applies to formal function declarations (outside of blocks).
    • 1greeting(); // TypeError
      2
      3var greeting = function greeting() {
      4	console.log("Hello!");
      5};
      Error: “greeting” is not a function! ← a TypeError (we do something with a value that isn’t allowed), not a ReferenceError!
      → It found the greeting but it’s not function, so you cannot call it as a function!
  • Variables declared with var are also automatically initialized to undefined at the beginning of their scope
❇️ Variable Hoisting
1greeting = "Hello!";
2console.log(greeting); // Hello!
3
4var greeting = "Howdy!";
Why can we assign to greeting before it’s declared?
1var greeting; // hoisted declaration
2greeting = "Hello!"; // the original line 1
3console.log(greeting); // Hello!
4greeting = "Howdy!"; // `var` is gone!
The codes actually looks like this.
There are 2 parts:
  1. the identifier is hoisted.
  1. and it’s auto initialized to undefined from the top of the scope!

Hoisting: Yet Another Metaphor

  • Hoisting just likes lifting.
  • JS engine will rewrite program before execution.
  • Function declarations are hoisted first then variables are hoisted immediately after all the functions.
    • 1studentName = "Suzy";
      2greeting(); // Hello Suzy!
      3
      4function greeting() {
      5	console.log(`Hello ${ studentName }!`); 
      6}
      7var studentName;
      Original codes.
      1function greeting() {
      2	console.log(`Hello ${ studentName }!`);
      3}
      4var studentName;
      5studentName = "Suzy";
      6greeting(); // Hello Suzy!
      Hoisted looks like.

Re-declaration?

What happens when a variable is repeatedly declared in the same scope?
1var studentName = "Frank";
2console.log(studentName); // Frank
3
4var studentName;
5console.log(studentName); // ???
→ It’s “Frank”
1var studentName;
2var studentName; // clearly a pointless no-op(eration)!
3
4studentName = "Frank";
5console.log(studentName); // Frank
6console.log(studentName); // Frank
After hoisting.
var studentName is very different from var studentName = undefined! ← It’s implicitly get undefinedvalue when hoisting but it’s not like being assigned to undefined.
1var studentName = "Frank";
2console.log(studentName); // Frank
3
4var studentName;
5console.log(studentName); // Frank <- still!
6
7// let's add the initialization explicitly
8var studentName = undefined;
9console.log(studentName); // undefined <- see!?
Remark: Different from var, let considers let studentName and let studentName = undefined are the same! Check section “Uninitialized Variables”.
We cannot re-declare values if using let or const! → SyntaxError ← the same error will occur if we use interchangeable between let and var.
1let studentName = "Frank";
2console.log(studentName);
3let studentName = "Suzy";
The only way to "re-declare" a variable is to use var!
👉 My note “Declare variables & Scope
❇️ Constants?
  • Different from let, const requires a variable to be initialized! ← SyntaxError if we don’t!
  • We cannot re-assign with const (different from let)
❇️ Loops
1var keepGoing = true;
2while (keepGoing) {
3	let value = Math.random(); // no redeclare too for `var` (<- like keepGoing)
4	if (value > 0.5) {
5		keepGoing = false; 
6	} 
7}
Is value is re-declared? → No! ← Each loop iteration is its own new scope instance.
All rules of scope are applies per scope instance (each time a scope is entered during execution, everything resets.)
Remember: var, let and const are removed from the code by the time it starts to execute! They’re handled entirely by the compiler!
1for (let i = 0; i < 3; i++) {
2	let value = i * 10;
3	console.log(`${ i }: ${ value }`);
4}
Is i redeclared? ← no! It’s like value
1{
2
3// a fictional variable for illustration
4let $$i = 0;
5
6for ( /* nothing */; $$i < 3; $$i++) { 
7	// here's our actual loop `i`!
8	let i = $$i;
9	let value = i * 10;
10	console.log(`${ i }: ${ value }`);
11}
Verbose version.
Per scope instance is also true for for..in, for..of.
👇 How about using const with loops?
1var keepGoing = true;
2while (keepGoing) {
3	const value = Math.random(); // this is fine
4	if (value > 0.5) { keepGoing = false; }
5}
1for (const index in students) {
2	// this is fine
3}
1for (const student of students) {
2	// this is fine
3}
1for (const i = 0; i < 3; i++) {
2	// 🚨 oops, this is going to fail with
3	// a Type Error after the first iteration
4}
Why? → Using $$i like previous “verbose version”, we have that at the beginning const $$i = 0 which isn’t allowed to re-assigned in $$i++!

Uninitialized Variables (aka, TDZ)

1console.log(studentName); // "Suzy"
2var studentName = "Suzy";
var → variable is hoisted & auto initialized to undefined
1console.log(studentName); // ReferenceError
2let studentName = "Suzy";
let and const → variable isn’t initialized!
1studentName = "Suzy"; // ReferenceError
2console.log(studentName);
3let studentName;
What if we initialzize the value for studentName? → ReferenceError ← called uninitialized
1let studentName = "Suzy";
2console.log(studentName); // Suzy
The only way to initialize with let or const
👇 var studentName auto initializes at the top of the scope where let studentName does not!
1var studentName = "Frank";
2console.log(studentName); // Frank
3
4var studentName = undefined;
5console.log(studentName); // undefined
1let studentName;
2// or:
3// let studentName = undefined;
4
5studentName = "Suzy";
6console.log(studentName); // Suzy
let and const don’t auto-initialze at the top of the scope but they do hoist (auto register at the top of the scope). Let’s prove it!
1var studentName = "Thi";
2
3{
4	console.log(studentName); // Error: ReferenceError: not initialization
5	let studentName = "Suzy";
6	console.log(studentName); // Suzy
7}
If they’re not hoisted (in the block scope) → the first studentName should be “Thi”!
In summary, TDZ errors occur because let/const declarations hoist like var, but delay auto-initialization until the declaration point, creating a TDZ (Temporal Dead Zone).
☝️
Advice: To avoid TDZ errors, always put let and const declaration at the top of any scope!