This note serves as a reminder of the book's content, including additional research on the mentioned topics. It is not a substitute for the book. Most images are sourced from the book or referenced.
All notes in this series
Infor & Preface
- 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.
- 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.
- 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.
- In classic compiler theory, a program is processed by a compiler in three basic stages:
- Tokenizing/Lexing:
var a = 2;
will be tokenized asvar
,a
,=
,2
and;
. - 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 calledVaraibleDeclaration
, with a child node calledIdentifier
(a
),… - 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 phases → parsing/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! - Early Errors: How JS know duplicated parameters “greeting” (becauase of
"use strict"
) without printing “Howdy”? ← It reads the whole code first! - Hoisting: How JS know “greeting” is a block-scoped variable whereas there is
var greeting...
? ← It reads the 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!
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 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
- 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)
- 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 scopevar
andfunction
at runtimewith
: dynamically turns an object into a local scope
1function badIdea() {
2 eval("var oops = 'Ugh!';"); // without eval, "oops" doesn't exist
3 console.log(oops);
4}
5
6badIdea(); // Ugh!
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()
andwith
!
- 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.
- Inside fuction → scope in that function.
let
/const
→ the nearest{ .. }
block.var
→ enclosing function
Examples:
- 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.
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.
- 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.
- Each scope bubble is entirely contained within its parent scope bubble—a scope is never partially in two different outer scopes.
id
,name
andlog
are properties, not variables (not marbles) → They don’t get colored! Check .
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
- 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!Try with your friends a real conversation about your real codes like above conversations between these 3 guys!
- Scope chain = The connections between scopes that are nested within other scopes.
- Lookup moves upward/outward only.
- Engine asks Scope Manager → it proceeds upward/outward back through the chain of nested scopes until variable is found.
- 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.
- 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
studentName
s ofprintStudent()
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
insideprintStudent
? → 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 shadowvar
butvar
cannot shadowlet
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 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 foraskQuestion
as previous
var askQuestion = function ofTheTeacher(){...}
(named function expression) →askQuestion
is in outer scope butofTheTeacher
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};
- 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 asfunction
functions do.
- 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.
- 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.
Most JS apps are composed of individual JS files. How they are combined (in a single runtime context)? → 3 main ways:
- If you use ES Modules → each module uses
import
to other modules. They don’t need any shared outer scope, just via these imports.
- 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(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})();
- 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.
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.
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 it1<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).Review:
- Declare a global variable using
var
,function
,let
,const
, orclass
.
- If using
var
orfunction
, add declarations to the global scope object.
- Use
window
(Browser),self
(Web Worker), orglobal
(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.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 toundefined
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:
- the identifier is hoisted.
- and it’s auto initialized to
undefined
from the top of the scope!
- 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.
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 undefined
value 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 fromlet
)
❇️ 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++
!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 uninitialized1let 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!