👉 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).
- 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!
Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution. Ref.
- How the JS engine identifies variables and determines the scopes of a program as it is compiled?
- 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
- Source:
- 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
- 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)
- 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.
- For this line of code,
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!- Global... What!?
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.
- 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)
window.studentName
is a mirror of the global studentName
(change one, the other changes). - Copying Is Not Accessing
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
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!
- ES6 added it.
- The assignment to
askQuestion
creates an inferred name of "askQuestion", but that's not the same thing as being non-anonymous
=>
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
- 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”
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:
Recall an example from Chap 3.
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→ Never use these global variables!
❇️ What's in a (Window) Name?
- 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
.- 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.
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.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).
→ 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
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.
What happens when a variable is repeatedly declared in the same scope?
→ It’s “Frank”
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
.We cannot re-declare values if using
let
or const
! → SyntaxError
← the same error will occur if we use interchangeable between let
and var
.→ 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
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!Per scope instance is also true for
for..in
, for..of
.👇 How about using
const
with loops?👇
var studentName
auto initializes at the top of the scope where let studentName
does not!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!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!