The previous lesson ended with an open question: when createTask() does lastId = lastId + 1, it is touching a variable that lives outside it. Why can it see that variable? And could you have an id counter that nobody else could touch by accident, without leaving it loose in the file? Answering that means understanding two concepts that rank among the most important in JavaScript: scope (where each variable lives and from where it can be seen) and closures (a function's ability to remember the environment it was born in). With them you will be able to create private state, factories of preconfigured functions and result caches, three tools you will use in Nómada Tasks right through to the end of the course.

Contents

  1. What scope is
  2. Global scope
  3. Function scope
  4. Block scope: let and const versus var
  5. The scope chain, step by step
  6. Variable shadowing
  7. What a closure is
  8. Closures explained with a minimal example
  9. The classic loop with var versus let
  10. Pattern 1: an id generator
  11. Pattern 2: a factory of preconfigured functions
  12. Pattern 3: private state with the module pattern
  13. Pattern 4: simple memoization
  14. The cost in memory
  15. Common Mistakes and Tips
  16. Exercises
  17. Conclusion

  1. What scope is

The scope of a variable is the region of the code from which its name can be used. This is not an academic detail: it is what determines whether a line works or throws a ReferenceError.

JavaScript has three kinds of scope:

Scope Created by Lives as long as Declared by
Global The file or the <script> The program runs const/let/var outside everything
Function Each call to a function The call lasts Parameters and any internal declaration
Block Each { } (if, for, a bare block) The block lasts Only const and let (not var)

And one rule that governs everything else:

From the inside you can see outward; from the outside you cannot see inward.

  1. Global scope

Everything declared outside any function and any block is global: visible from anywhere in the file.

'use strict';

const TODAY = '2026-09-20';      // global: a project configuration constant
const WEEKLY_LIMIT = 40;         // global: rule R7

function headroomFor(openHours) {
  return WEEKLY_LIMIT - openHours;   // ✓ it sees the global
}

console.log(headroomFor(25));   // 15

Globals are useful for configuration constants and dangerous for everything else:

Risk Explanation
Name collisions Two files that both declare tasks clobber each other
Invisible coupling A function that reads a global does not say in its signature what it depends on
Impossible to test You cannot run it with other values without modifying the global
Mutation from anywhere Any line in the program can change it

A practical rule: globals only with const, and only for configuration. Everything that varies goes inside functions or is protected by a closure, as you will see in section 12.

  1. Function scope

Each call to a function creates a fresh scope. The parameters and the variables declared in its body exist there and disappear when it finishes.

function summarizeAssignee(name, hours) {
  const limit = 40;                        // local
  const headroom = limit - hours;          // local
  return `${name}: ${headroom} h of headroom`;
}

console.log(summarizeAssignee('Iván', 25));   // Iván: 15 h of headroom
console.log(limit);                           // ✗ ReferenceError: limit is not defined

And something important: two calls share nothing.

function countFromZero() {
  let counter = 0;      // born anew on every call
  counter++;
  return counter;
}

console.log(countFromZero());   // 1
console.log(countFromZero());   // 1  ← it remembers nothing

That amnesia is what the closures of section 7 let you break when it suits you.

  1. Block scope: let and const versus var

A block is any pair of braces: the body of an if, of a for, of a while, or a bare pair of braces. let and const stay inside the block; var does not.

function analyzeTask(status, hours) {
  if (status === 'done') {
    const message = 'Task completed';       // only inside the if
    var record = 'done';                    // it escapes into the whole function!
  }

  console.log(record);    // 'done'         ← var ignores the block
  console.log(message);   // ✗ ReferenceError
}

The full comparison of the three ways of declaring:

var let const
Scope Function Block Block
Can be reassigned Yes Yes No
Can be redeclared in the same scope Yes (dangerous) No No
Before the declaration line undefined Error (TDZ) Error (TDZ)
Added to globalThis when global Yes No No
Recommendation Do not use When it changes By default

That "can be redeclared" of var is especially treacherous:

var assignee = 'Iván';
var assignee = 'Marta';    // ✓ no error: you lost the previous value without noticing

let reviewer = 'Lucía';
let reviewer = 'Marta';    // ✗ SyntaxError: Identifier 'reviewer' has already been declared

In this course you will not use var. It appears here only because you will see it in old code and because its behavior explains the classic exercise in section 9.

  1. The scope chain, step by step

When the engine comes across a name, it looks for it in the current scope. If it is not there, it moves up to the enclosing scope. And so on, all the way to the global one. If it is not there either: ReferenceError.

'use strict';

const TODAY = '2026-09-20';                     // ── Global scope

function generateReport(assignee) {             // ── generateReport's scope
  const header = `Report for ${assignee}`;

  function detail(hours) {                      // ── detail's scope
    const line = `${hours} h`;
    return `${header} · ${line} · as of ${TODAY}`;
    //       ↑ parent scope  ↑ local  ↑ global
  }

  return detail(25);
}

console.log(generateReport('Iván'));
// Report for Iván · 25 h · as of 2026-09-20

This is how the engine looks up each name inside detail:

flowchart TD
    A["detail's scope<br/>line, hours"] -->|does not find 'header'| B["generateReport's scope<br/>header, assignee"]
    B -->|does not find 'TODAY'| C["Global scope<br/>TODAY, generateReport"]
    C -->|finds nothing| D["ReferenceError"]

Three consequences worth nailing down:

  1. The search is one-directional: upward. From generateReport you cannot see line.
  2. It stops at the first match. If detail declared its own header, the search would stop there (section 6).
  3. The chain is decided when the code is written, not when it runs. This is called lexical scope, and it is the key to understanding closures: what matters is where the function is written, not where it is called from.

  1. Variable shadowing

When an inner scope declares a variable with the same name as an outer one, the inner one covers the outer one within its region.

const status = 'global';

function inspect() {
  const status = 'function-level';

  if (true) {
    const status = 'block-level';
    console.log(status);      // block-level
  }

  console.log(status);        // function-level
}

inspect();
console.log(status);          // global

Shadowing is legal and sometimes useful (a parameter called task inside a function that works with tasks), but it is also a source of confusion. A real case that bites:

const WEEKLY_LIMIT = 40;

function checkWorkload(hours, WEEKLY_LIMIT) {    // ✗ the parameter shadows the constant
  return hours > WEEKLY_LIMIT;
}

console.log(checkWorkload(45));   // false  ← WEEKLY_LIMIT is undefined inside

Whoever reads the body assumes WEEKLY_LIMIT is 40, but inside it is undefined because the second argument was not passed, and 45 > undefined is false. Rules to avoid it:

  • Do not reuse the names of global constants as parameters.
  • If the linter warns about no-shadow, listen to it (08-02).
  • When the shadowing is deliberate, make it obvious: task outside and originalTask inside, for instance.

  1. What a closure is

A closure is a function together with the environment of variables it was created in. Put another way:

When a function is defined inside another one, it remembers the outer function's variables, even after that outer function has finished.

The surprising part is the last one. You already know that a function's variables disappear when the call ends. But if an inner function keeps using them, they do not disappear: the engine keeps them alive for as long as necessary.

flowchart LR
    subgraph E["Captured environment (survives)"]
        V["counter = 0"]
    end
    F["inner function<br/>returned to the outside"] --> V
    C["whoever calls it<br/>from outside"] --> F

  1. Closures explained with a minimal example

Let us start with the smallest possible case:

function createGreeting(name) {
  const greeting = `Hello, ${name}`;     // lives in createGreeting's scope

  return function () {                   // inner function
    return greeting;                     // uses a variable from the parent scope
  };
}

const greetMarta = createGreeting('Marta');

// createGreeting has ALREADY finished... and yet:
console.log(greetMarta());   // Hello, Marta

Read the sequence carefully:

  1. createGreeting('Marta') is called and a scope is created with name and greeting.
  2. The inner function is created, and it references greeting.
  3. createGreeting finishes and returns that function.
  4. Normally greeting would disappear. But since the returned function needs it, the engine keeps the environment alive.
  5. When greetMarta() is called, the function finds its greeting intact.

And each call to createGreeting creates an independent environment:

const greetIvan  = createGreeting('Iván');
const greetLucia = createGreeting('Lucía');

console.log(greetIvan());    // Hello, Iván
console.log(greetLucia());   // Hello, Lucía

Two functions with identical code and different memories. That is the whole mechanism. Everything that follows is an application of this same idea.

  1. The classic loop with var versus let

This exercise turns up in every job interview and, more importantly, explains a real bug you will see in Module 6 when assigning event handlers inside a loop.

const titles = ['Redesign the room', 'Signage', 'Bookings website'];
const actions = [];

for (var i = 0; i < titles.length; i++) {
  actions.push(function () {
    console.log(`${i}: ${titles[i]}`);
  });
}

actions[0]();   // 3: undefined
actions[1]();   // 3: undefined
actions[2]();   // 3: undefined

Why? Because var i has function scope, not block scope: there is one single i for the whole loop. The three functions capture the same variable, and by the time they run (after the loop) that i is already 3.

With let, each iteration creates its own i:

for (let i = 0; i < titles.length; i++) {
  actions.push(function () {
    console.log(`${i}: ${titles[i]}`);
  });
}

actions[0]();   // 0: Redesign the room
actions[1]();   // 1: Signage
actions[2]();   // 2: Bookings website
flowchart TD
    subgraph VAR["With var: one single variable"]
        A1["fn 0"] --> I["i = 3"]
        A2["fn 1"] --> I
        A3["fn 2"] --> I
    end
    subgraph LET["With let: one per iteration"]
        B1["fn 0"] --> J1["i = 0"]
        B2["fn 1"] --> J2["i = 1"]
        B3["fn 2"] --> J3["i = 2"]
    end

Before let, the solution was to wrap each iteration in an IIFE —the pattern you saw in Function Expressions and Arrow Functions— precisely to create a fresh scope on every pass:

for (var i = 0; i < titles.length; i++) {
  (function (index) {
    actions.push(function () { console.log(`${index}: ${titles[index]}`); });
  })(i);
}

Today writing let is enough. But now you know why.

  1. Pattern 1: an id generator

Back to the loose end from the previous lesson. lastId was sitting loose in the file, visible and modifiable by anyone. With a closure, it stops existing for the rest of the program.

'use strict';

/**
 * Returns a function that produces sequential identifiers (R1).
 * The counter is locked in: nobody outside can read it or change it.
 */
function createIdGenerator(start = 0) {
  let lastId = start;

  return function nextId() {
    lastId = lastId + 1;
    return lastId;
  };
}

const nextId = createIdGenerator(6);   // the backlog already has 6 tasks

console.log(nextId());   // 7
console.log(nextId());   // 8
console.log(nextId());   // 9

console.log(typeof lastId); // 'undefined'  ← protected

Compare the two versions:

Global variable lastId Closure createIdGenerator
Who can modify it Any line in the program Only the returned function
Risk of another file clobbering it High None
Several independent generators Impossible Call createIdGenerator() again
Configurable starting value By editing the file Through a parameter

That last point is very useful in testing: in Module 8 you will be able to create a generator that starts at 0 for each test, without one test contaminating the next.

const testIds = createIdGenerator();
console.log(testIds());   // 1  ← independent of the production generator
console.log(nextId());    // 10 ← the other one carries on by itself

  1. Pattern 2: a factory of preconfigured functions

A factory is a function that returns another function already configured with some fixed parameters. It saves you repeating those parameters on every call.

const assignees = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const titles    = [
  'Redesign the multipurpose room',
  'Signage for the screen-printing workshop',
  'Update the bookings website',
  'Screen-printing ink inventory',
  'Bookbinding guide for residents',
  'Carpentry workshop quote'
];
const hours    = [12, 6, 14, 3, 8, 5];
const statuses = ['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending'];

/**
 * Returns a function that says whether the task at index i belongs to `name`.
 * `name` stays captured in the closure.
 */
function createAssigneeFilter(name) {
  return function (index) {
    return assignees[index] === name;
  };
}

const assignedToIvan  = createAssigneeFilter('Iván');
const assignedToMarta = createAssigneeFilter('Marta');

console.log(assignedToIvan(0));    // true
console.log(assignedToIvan(1));    // false
console.log(assignedToMarta(1));   // true

And this is how you use it to walk the backlog with interchangeable criteria:

function listMatching(filter) {
  const found = [];
  for (let i = 0; i < titles.length; i++) {
    if (filter(i)) found.push(titles[i]);
  }
  return found;
}

console.log(listMatching(assignedToIvan));
// [ 'Redesign the multipurpose room',
//   'Bookbinding guide for residents',
//   'Carpentry workshop quote' ]

// And factories can be combined
function createStatusFilter(status) {
  return (index) => statuses[index] === status;
}

function createCombinedFilter(filterA, filterB) {
  return (index) => filterA(index) && filterB(index);
}

const ivansPending = createCombinedFilter(assignedToIvan, createStatusFilter('pending'));
console.log(listMatching(ivansPending));
// [ 'Carpentry workshop quote' ]

Notice what just happened: listMatching knows nothing about assignees or statuses; it only knows how to call a function it is handed. That is a higher-order function, and it is the whole subject of Higher-Order Functions.

  1. Pattern 3: private state with the module pattern

The module pattern combines a closure with a returned object: the variables stay private and only a controlled set of functions is exposed.

'use strict';

function createTaskStore(startId = 0) {
  // ── Private state: unreachable from outside ──────────────────
  const tasks = [];
  let lastId = startId;

  // ── Private helper function ──────────────────────────────────
  function findIndexById(id) {
    for (let i = 0; i < tasks.length; i++) {
      if (tasks[i].id === id) return i;
    }
    return -1;
  }

  // ── Public interface ─────────────────────────────────────────
  return {
    add: function (title, assignee, estimatedHours) {
      lastId = lastId + 1;
      tasks.push({
        id: lastId,
        title: title,
        assignee: assignee ?? null,
        status: 'pending',
        estimatedHours: estimatedHours
      });
      return lastId;
    },

    changeStatus: function (id, newStatus) {
      const i = findIndexById(id);
      if (i === -1) throw new Error(`Task ${id} does not exist.`);
      tasks[i].status = newStatus;
      return true;
    },

    count: function () {
      return tasks.length;
    },

    openHours: function () {
      let total = 0;
      for (const t of tasks) {
        if (t.status !== 'done') total += t.estimatedHours;
      }
      return total;
    }
  };
}

const store = createTaskStore(6);

console.log(store.add('Refurbish the lathe', 'Lucía', 10));   // 7
console.log(store.add('Buy inks', null, 2));                  // 8
console.log(store.count());                                   // 2
console.log(store.openHours());                               // 12

store.changeStatus(7, 'done');
console.log(store.openHours());                               // 2

console.log(store.tasks);           // undefined  ← protected
console.log(typeof findIndexById);  // 'undefined' ← private

The guarantees this structure offers are real, not stylistic:

  • Nobody can do store.tasks.push(garbage), because tasks is not exposed.
  • lastId cannot be tampered with, so R1 (unique, sequential identifiers) is guaranteed by construction.
  • findIndexById is an internal detail: it can be rewritten without breaking anyone.

In Module 5 you will see two evolutions of this idea: classes with private fields and ES modules, which achieve the same thing at file level. The module pattern with closures is the ancestor of both and is still perfectly valid.

  1. Pattern 4: simple memoization

Memoizing means storing a function's result so you do not have to compute it again for the same arguments. The result store lives in a closure.

/**
 * Counts one assignee's tasks, caching each result.
 * This is only correct because counting is a PURE function (see 03-03).
 */
function createMemoizedCounter(assignees) {
  const cache = {};      // private: the function's memory
  let computations = 0;
  let hits = 0;

  const count = function (name) {
    if (name in cache) {
      hits++;
      return cache[name];
    }

    computations++;
    let total = 0;
    for (const a of assignees) {
      if (a === name) total++;
    }
    cache[name] = total;
    return total;
  };

  count.stats = () => `${computations} computations, ${hits} cache hits`;
  return count;
}

const countTasksFor = createMemoizedCounter(assignees);

console.log(countTasksFor('Iván'));    // 3  (computed)
console.log(countTasksFor('Iván'));    // 3  (from cache)
console.log(countTasksFor('Marta'));   // 2  (computed)
console.log(countTasksFor('Iván'));    // 3  (from cache)
console.log(countTasksFor.stats());    // 2 computations, 2 cache hits

Two important warnings about memoization:

  1. It is only valid with pure functions. If the result depends on something that changes (the clock, an external variable, the contents of the backlog), the cache will hand back stale data. That is why 03-03 insisted so much on purity.
  2. The cache grows. Every new argument adds an entry that is never released while the function lives.

In Recursion you will apply exactly this technique to Fibonacci, where the performance difference is spectacular.

  1. The cost in memory

A closure keeps the whole environment of the outer function alive for as long as the inner function exists. That is powerful and it has a price:

function createHeavyFormatter() {
  const fullBacklog = loadThousandsOfTasks();      // imagine 50 MB of data
  const separator = ' · ';

  return function (title, hours) {
    return title + separator + hours;              // it only uses `separator`
  };
}

const format = createHeavyFormatter();
// `fullBacklog` may stay in memory for as long as `format` exists

Modern engines optimize this case rather well and usually release whatever the inner function demonstrably does not use, but it is not wise to depend on that. Good habits:

Practice Reason
Capture only what you need (extract the value before closing over it) Reduces what stays retained
Set large references to null when they are no longer needed Lets them be released
Cap the size of memoization caches Prevents endless growth
Do not create closures inside very long loops unless you need to Each one is a live environment

This connects directly with the memory leaks of Memory Management, where you will see how to detect them with DevTools. For now the idea is enough: a closure is not free, but in 99% of cases its cost is irrelevant compared with what it gives you.

Common Mistakes and Tips

1. Believing a closure copies the value. It does not copy it: it keeps a live reference to the variable.

function createLabeler() {
  let prefix = 'TN';
  const label = (id) => `${prefix}-${id}`;
  prefix = 'NOMADA';           // changed AFTER the function is created
  return label;
}

console.log(createLabeler()(7));   // NOMADA-7  ← it sees the current value, not the old one

2. Sharing state without realizing it. Two functions created in the same call share an environment:

function createDoubleCounter() {
  let n = 0;
  return { increment: () => ++n, read: () => n };
}

const c = createDoubleCounter();
c.increment(); c.increment();
console.log(c.read());   // 2  ← they share the same `n`, which is what we want here

It is desirable when you are after it and a bug when you are not.

3. Using var in a loop that creates functions. The case from section 9. With let it goes away.

4. Shadowing a global constant with a parameter. It gives false or undefined results with no error at all.

5. Memoizing an impure function. It will hand back stale data. Before memoizing, check that the function is pure.

6. Thinking that scope depends on the caller. It does not: it depends on where the function is written (lexical scope). This is the point people struggle with most and the one that makes closures predictable.

const label = 'global';

function show() {
  console.log(label);         // always 'global'
}

function call() {
  const label = 'local';      // does NOT affect show()
  show();
}

call();   // global

7. Tip: if a function needs to remember something between calls, think of a closure before a global variable. It is the difference between controlled state and loose state.

8. Tip: name your factories with the create… prefix. createIdGenerator, createAssigneeFilter, createTaskStore. The name warns that what comes back is not a piece of data but a tool.

Exercises

Exercise 1 — Predict the output

Without running the code, write down what each console.log prints and explain why.

const label = 'A';

function outer() {
  const label = 'B';

  function inner() {
    console.log(label);          // (1)
  }

  if (true) {
    const label = 'C';
    console.log(label);          // (2)
    inner();                     // (3)
  }

  inner();                       // (4)
}

outer();
console.log(label);              // (5)

function brokenCounter() {
  var n = 0;
  const fns = [];
  for (var i = 0; i < 3; i++) {
    fns.push(() => i + n);
  }
  return fns;
}
const fns = brokenCounter();
console.log(fns[0](), fns[1](), fns[2]());   // (6)

Exercise 2 — createStatusCounter

Write a factory createStatusCounter(statuses) that takes the backlog's array of statuses and returns an object with three functions: record(status) (adds one to that status's count), countOf(status) (returns how many times it has been recorded) and summary() (returns a string with the three statuses and their counts). The counts must be private. Initialize them by walking the array you receive.

Exercise 3 — Memoizing calculateEffort

Write memoize(fn), a generic factory that takes a function of one string or number parameter and returns a memoized version. Apply it to a calculateEffort(priority) function that is deliberately expensive (put a loop of a million iterations inside) and use Date.now() to measure the difference between the first and the second call.

Solutions

Exercise 1

No. Output Explanation
(2) C The const in the if block shadows the function-level one
(3) B Lexical scope: inner is written inside outer, so it sees outer's label. That it is called from the block is irrelevant
(4) B Same reason
(1) It is the line that produces (3) and (4); it does not run on its own
(5) A The global was never touched
(6) 3 3 3 var i is unique for the whole function; by the time the arrows run, i is already 3 and n is 0

The actual print order: C, B, B, A, 3 3 3.

To fix (6) all it takes is changing var i to let i, and then it would give 0 1 2.

Exercise 2

function createStatusCounter(initialStatuses = []) {
  // Private state
  const counts = { pending: 0, 'in-progress': 0, done: 0 };

  function increment(status) {
    if (!(status in counts)) {
      throw new Error(`Unknown status: ${status}`);
    }
    counts[status] = counts[status] + 1;
  }

  // Initialization from the array received
  for (const s of initialStatuses) {
    increment(s);
  }

  return {
    record: (status) => { increment(status); },
    countOf: (status) => counts[status] ?? 0,
    summary: () =>
      `pending: ${counts.pending} · in progress: ${counts['in-progress']} · done: ${counts.done}`
  };
}

const statuses = ['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending'];
const counter = createStatusCounter(statuses);

console.log(counter.summary());        // pending: 3 · in progress: 2 · done: 1
console.log(counter.countOf('done'));  // 1

counter.record('done');
console.log(counter.summary());        // pending: 3 · in progress: 2 · done: 2

console.log(counter.counts);           // undefined  ← private
counter.record('archived');            // ✗ Error: Unknown status: archived

Comment: counts and increment are private; the public interface only allows valid operations. Nobody can write counter.counts.done = 99 because counts does not exist outside. And the throw keeps the state consistent at all times, applying the fail-fast principle from Error Handling.

Exercise 3

function memoize(fn) {
  const cache = {};

  return function (argument) {
    const key = String(argument);
    if (key in cache) return cache[key];

    const result = fn(argument);
    cache[key] = result;
    return result;
  };
}

// A deliberately expensive function
function calculateEffort(priority) {
  let accumulated = 0;
  for (let i = 0; i < 20000000; i++) {
    accumulated += i % 3;
  }
  if (priority === 'high') return 3 + (accumulated % 1);
  if (priority === 'medium') return 2 + (accumulated % 1);
  return 1 + (accumulated % 1);
}

const fastEffort = memoize(calculateEffort);

let t = Date.now();
console.log(fastEffort('high'), `${Date.now() - t} ms`);     // 3  ~120 ms

t = Date.now();
console.log(fastEffort('high'), `${Date.now() - t} ms`);     // 3  0 ms

t = Date.now();
console.log(fastEffort('medium'), `${Date.now() - t} ms`);   // 2  ~120 ms

Comment: the second call with 'high' is practically instantaneous because it does not run the loop. String(argument) normalizes the key so that 3 and '3' share an entry (something we want in this case, but which in others could be a problem: it is the limitation of using an object as a cache with text keys). The restriction to one parameter is no accident: memoizing with several requires serializing all the arguments into a single key, something you will solve with JSON.stringify in JSON and Copying Objects.

Conclusion

You now know where each variable lives. Global scope is for configuration constants like TODAY and WEEKLY_LIMIT; function scope is born and dies with each call; and block scope, which only let and const respect, keeps variables inside their braces. The scope chain looks each name up from the inside out and stops at the first match, which explains shadowing and the classic bug of a parameter covering a global constant. And above all you know that scope is lexical: it depends on where the function is written, not on who calls it.

Out of that comes the central concept of the lesson. A closure is a function that keeps alive the environment it was born in, and with it you have built four tools that Nómada Tasks will use to the very end: createIdGenerator(), which guarantees R1 without leaving any counter loose; createAssigneeFilter(), a factory that produces preconfigured, combinable filters; createTaskStore(), with genuinely private state and a controlled public interface; and createMemoizedCounter(), which caches the results of pure functions. You also finally understand the loop with var that always returns the last index, and you know that a closure retains memory, a cost studied in depth in Memory Management.

But in this lesson you have taken something for granted that has not been explained yet: that the engine "knows" which variables exist in each scope before anything runs. That is what lets you call a function declaration before its line, what makes var be undefined instead of raising an error, and what makes let and const throw that mysterious Cannot access before initialization you have now seen three times. All of that is the preparation work the engine does for every scope, and it is the subject of Hoisting and the Execution Context, where you will also take apart the call stack you only glimpsed in 03-01.

JavaScript Course: From Beginner to Advanced

Module 1: Introduction to JavaScript

Module 2: Control Structures

Module 3: Functions

Module 4: Objects and Arrays

Module 5: Advanced Objects and Functions

Module 6: The Document Object Model (DOM)

Module 7: Browser APIs and Advanced Topics

Module 8: Testing and Debugging

Module 9: Performance and Optimization

Module 10: JavaScript Frameworks and Libraries

Module 11: Final Project

© Copyright 2026. All rights reserved