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

Anh-Thi Dinh
draft
⚠️
I’m still reading…
⚠️
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

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!
      • 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.

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}

Function Name Scope

Arrow Functions

Backing Out

Loading comments...