We use cookies to enhance your experience on the site
CodeWorlds

Variable Scope (Global, Function, Block)

Welcome back to Jurassic Park! Today we tackle one of the fundamental concepts of JavaScript - variable scope. Understanding scope is essential if you want to write efficient, safe and predictable code, especially inside a complex system such as our park full of dinosaurs.

What is variable scope?

Scope decides where in the code a given variable is available - in other words, from which places you are allowed to refer to it. Just as Jurassic Park has areas with different clearance levels (the visitor zone, the laboratory reserved for scientists, the enclosures that require special permissions), JavaScript has different levels of access to variables.

There are three main types of scope in JavaScript:

  1. Global scope
  2. Function scope
  3. Block scope

Global scope

Variables declared outside of any function or block of code have global scope. That means they are available in the entire program, inside every function and inside every block of code.

1// Global variables - available everywhere, like public information about the park
2const parkName = "Jurassic Park";
3let visitorCount = 0;
4var isOpenToPublic = true;
5
6function checkParkStatus() {
7  console.log(`Park name: ${parkName}`);
8  console.log(`Visitor count: ${visitorCount}`);
9  console.log(`Open to the public: ${isOpenToPublic}`);
10}
11
12function updateVisitorCount(newCount) {
13  // We can modify a global variable from any function
14  visitorCount = newCount;
15  console.log(`Visitor count updated: ${visitorCount}`);
16}
17
18// Calling the functions that use the global variables
19checkParkStatus();
20updateVisitorCount(250);
21checkParkStatus();

Notice that

updateVisitorCount
never received
visitorCount
as a parameter - it simply reached for it, because a global variable is visible from inside every function. That convenience is exactly why you have to be careful with global variables:

  • they can be modified by accident from anywhere in the code
  • they can lead to name collisions
  • they make it harder to track the state of the application
  • they increase the coupling between different parts of the code

Global variables in our park are like documents left out for every single employee - sometimes they are necessary, but too many of them lead to chaos and misunderstandings.

Function scope

Variables declared inside a function are available only inside that function. They are local variables belonging to that one function, and the moment the function finishes, the outside world has no way of reaching them.

1function monitorDinosaur(dinoId) {
2  // These variables have function scope - they exist only inside this function
3  const startTime = Date.now();
4  let status = "Monitoring...";
5  var sensorData = [];
6
7  // We can freely use the variables inside the function
8  status = "Collecting data...";
9  sensorData.push({ time: startTime, temperature: 37.5 });
10
11  console.log(`Status: ${status}`);
12  console.log(`Sensor data: ${JSON.stringify(sensorData)}`);
13
14  // We return the result
15  return {
16    id: dinoId,
17    monitoringStarted: startTime,
18    lastStatus: status
19  };
20}
21
22// Calling the function
23const monitoringResults = monitorDinosaur("T-Rex-01");
24
25// The lines below would throw an error, because we are trying to reach
26// function-scoped variables from outside the function
27// console.log(startTime);    // ReferenceError
28// console.log(status);       // ReferenceError
29// console.log(sensorData);   // ReferenceError
30
31// But we do have access to the value the function returned
32console.log(monitoringResults);

The only information that leaves the function is what it returns - that is why

monitoringResults
works while
status
does not. Function scope behaves like the restricted zones of the park: only authorised personnel (the code inside the function) has access to the information and the equipment in that zone, and everyone else has to settle for the official report.

The difference between var, let and const in function scope

All three keywords (

var
,
let
,
const
) create variables that live inside a function, but there is an important difference between them:

  • var
    has function scope, but not block scope
  • let
    and
    const
    have both function scope and block scope

Let us see it on an example:

1function securityCheck() {
2  // All of these variables are available inside the whole function
3  var securityCode = "A1B2C3";
4  let accessLevel = 5;
5  const securityChief = "Robert Muldoon";
6
7  if (accessLevel >= 5) {
8    var emergencyProtocol = "Code Red"; // function scope!
9    let backupSystem = "Operational";   // block scope
10    const safeRoom = "Bunker B";        // block scope
11
12    console.log(securityCode);    // A1B2C3 - available
13    console.log(accessLevel);     // 5 - available
14    console.log(securityChief);   // Robert Muldoon - available
15  }
16
17  console.log(emergencyProtocol); // Code Red - available even though it was declared inside the if block
18  // console.log(backupSystem);   // ReferenceError - this variable has block scope
19  // console.log(safeRoom);       // ReferenceError - this variable has block scope
20}
21
22securityCheck();

This is the moment where

var
shows its dangerous side:
emergencyProtocol
escaped from the
if
block and is readable in the rest of the function, while
backupSystem
and
safeRoom
stayed exactly where they were declared. A variable that leaks out of the block it belongs to is a variable somebody will overwrite later without noticing.

Block scope

Block scope arrived in ES6 (ES2015) together with the new keywords

let
and
const
. Variables declared with
let
and
const
inside a block of code (anything wrapped in curly braces
{}
) are available only inside that block.

1function monitorDinosaurBehavior() {
2  let dangerLevel = 0;
3
4  // The if block - it creates a new scope
5  if (true) {
6    // Block scope - these variables exist only inside this block
7    let behavior = "Calm";
8    const vitals = { heartRate: 80, temperature: 38 };
9
10    // Modifying a variable from the outer scope
11    dangerLevel = behavior === "Aggressive" ? 5 : 1;
12
13    console.log(behavior);     // Calm - available
14    console.log(vitals);       // { heartRate: 80, temperature: 38 } - available
15    console.log(dangerLevel);  // 1 - available
16  }
17
18  console.log(dangerLevel);    // 1 - available, because it was declared in the wider scope
19  // console.log(behavior);    // ReferenceError - this variable has block scope
20  // console.log(vitals);      // ReferenceError - this variable has block scope
21
22  // The for block - it also creates a new scope
23  for (let i = 0; i < 5; i++) {
24    // 'i' has block scope, it is available only inside the loop
25    const sensorId = `Sensor-${i}`;
26    console.log(sensorId);
27  }
28
29  // console.log(i);        // ReferenceError - 'i' has block scope
30  // console.log(sensorId); // ReferenceError - this variable has block scope
31}
32
33monitorDinosaurBehavior();

Note what did not change: the block can read and modify

dangerLevel
from the outer scope, but nothing it declares itself travels back out. Block scope is like the individual enclosures in our park - each enclosure has its own rules and its own access list, and equipment assigned to one enclosure is not available in the others.

The difference between var, let and const

Now we can understand the differences between these three keywords much better:

  1. var
    :

    • Has function scope or global scope, but not block scope
    • Can be redeclared many times
    • Can be reassigned
    • Is hoisted with the value
      undefined
      assigned to it
  2. let
    :

    • Has block scope
    • Cannot be redeclared in the same scope
    • Can be reassigned
    • Is hoisted without a value (the temporal dead zone)
  3. const
    :

    • Has block scope
    • Cannot be redeclared in the same scope
    • Cannot be reassigned (but the properties of objects can be modified)
    • Is hoisted without a value (the temporal dead zone)
1function demonstrateVariables() {
2  // var
3  var dinosaur = "T-Rex";
4  var dinosaur = "Velociraptor"; // OK - redeclaring is allowed
5  dinosaur = "Triceratops";      // OK - reassigning is allowed
6
7  // let
8  let park = "Jurassic Park";
9  // let park = "Dino World";    // Error - redeclaring is not allowed
10  park = "Jurassic World";       // OK - reassigning is allowed
11
12  // const
13  const island = "Isla Nublar";
14  // const island = "Isla Sorna"; // Error - redeclaring is not allowed
15  // island = "Another Island";   // Error - reassigning is not allowed
16
17  // But the properties of a const object can be modified
18  const parkSystem = {
19    mainPower: true,
20    backupGenerator: false
21  };
22
23  // This is allowed
24  parkSystem.mainPower = false;
25  parkSystem.backupGenerator = true;
26
27  console.log(parkSystem); // { mainPower: false, backupGenerator: true }
28}
29
30demonstrateVariables();

This table of differences answers a very practical question: which declaration is best for a constant dinosaur species value? The answer is

const species = "Velociraptor";
- the species of an animal does not change, and
const
makes that promise visible in the code and enforced by the engine. Writing
let species = "Velociraptor";
also runs, but it announces that the value may change later, so a reader has to keep checking. Writing
var species = "Velociraptor";
is the weakest of the three, because on top of being reassignable it can be redeclared and it leaks out of blocks. They are definitely not all equally good - pick
const
first, reach for
let
only when a value genuinely has to change, and leave
var
in the past.

What you keep in a scope: values and their types

A scope is only as useful as the values you store inside it, so before we go deeper let us agree on which JavaScript type fits which kind of information. Dr. Wu organizes the lab records exactly this way: the dinosaur species name is text, so it becomes a String. The sample age in years is a count you can add and compare, so it is a Number - never a Boolean, because a Boolean can only answer yes or no. Whether the sample is complete is precisely such a yes-or-no fact, so a Boolean is the right match. And when there is no sample data at all, we deliberately store

null
, the value that means "intentionally empty".

Data does not always arrive in the type we need - a sensor sends text, a form sends text, and the counter in the control room expects a number. The conversion functions

String()
,
Number()
and
Boolean()
return a brand new value and never modify the original one:

1function prepareLabRecord() {
2  const species = "Velociraptor"; // String - the name of the dinosaur species
3  const ageInYears = 25;          // Number - the sample age in years
4  const isComplete = true;        // Boolean - is the sample complete
5  const dnaSequence = null;       // null - no sample data yet
6
7  // Number to String: the result is text
8  const populationLabel = String(65000000);
9  console.log(populationLabel);        // "65000000"
10  console.log(typeof populationLabel); // string
11
12  // String to Number: text that looks like a number becomes a number
13  const measured = Number("38.5");
14  console.log(measured + 1);           // 39.5
15
16  // Text that is not a number cannot be converted, so we get NaN
17  console.log(Number("T-Rex"));        // NaN
18
19  return { species, ageInYears, isComplete, dnaSequence };
20}
21
22console.log(prepareLabRecord());

Read that first conversion carefully, because it is easy to misjudge:

String(65000000)
returns
"65000000"
- the digits in quotation marks, a value of type string.
It does not return the number
65000000
, and it does not return
NaN
either, since
NaN
only appears when
Number()
receives text it cannot parse. It also does not return
undefined
- that is what you get from a variable that was declared but never given a value, which is a completely different situation from a conversion that succeeded.

Lexical scope and closures

JavaScript uses lexical scope, which means that a function can reach the variables declared in the scope that surrounds it. When a name is not found locally, the engine walks outwards through the enclosing scopes - this path is called the scope chain, and it goes in one direction only.

1const parkSecurity = "Maximum"; // global scope
2
3function securityZone() {
4  const zoneLevel = "High"; // the scope of securityZone
5
6  function enclosure() {
7    const zoneLevel = "Restricted"; // shadows the outer zoneLevel
8    console.log(zoneLevel);         // Restricted - the closest declaration wins
9    console.log(parkSecurity);      // Maximum - found in the global scope
10  }
11
12  enclosure();
13  console.log(zoneLevel);           // High - the inner value never leaked outwards
14}
15
16securityZone();

Two rules are worth memorising here. The closest declaration always wins, so the inner

zoneLevel
shadows the outer one instead of overwriting it. And information travels only upwards along the chain: an inner function sees the outer variables, but the outer scope never sees what the inner one declared. Now let us use that mechanism on purpose.

1function initSecuritySystem() {
2  // Variables in the scope of the outer function
3  const systemName = "JurassicSafe";
4  let isActive = false;
5  let alertLevel = 0;
6
7  // The inner function has access to the variables of the outer function
8  function activateSystem() {
9    isActive = true;
10    console.log(`System ${systemName} has been activated.`);
11    return true;
12  }
13
14  // Another inner function
15  function setAlertLevel(level) {
16    alertLevel = level;
17    console.log(`System ${systemName}: alert level set to ${alertLevel}`);
18  }
19
20  // We return the inner functions
21  return {
22    activate: activateSystem,
23    setAlert: setAlertLevel,
24    getStatus: function() {
25      return {
26        name: systemName,
27        active: isActive,
28        alertLevel: alertLevel
29      };
30    }
31  };
32}
33
34// We create an instance of the security system
35const securitySystem = initSecuritySystem();
36
37// We use the returned functions
38securitySystem.activate();           // System JurassicSafe has been activated.
39securitySystem.setAlert(3);          // System JurassicSafe: alert level set to 3
40console.log(securitySystem.getStatus()); // { name: "JurassicSafe", active: true, alertLevel: 3 }

In the example above the inner functions form a closure over the variables of the

initSecuritySystem
function. Even after
initSecuritySystem
has finished executing, the returned functions still have access to those variables - and each new call to
initSecuritySystem
produces its own independent set of them.

Closures are like the security teams in the park - even when they leave the command centre, they still carry the codes and the protocols they took with them.

A practical example: the park management system

Let us build a more complete example that shows the different scopes working together inside a park management system:

1// Global variables - available in the whole program
2const PARK_NAME = "Jurassic Park";
3let parkStatus = "closed";
4var totalVisitors = 0;
5
6// An IIFE (Immediately Invoked Function Expression) that creates a module
7const parkSystem = (function() {
8  // Private variables - function scope, unreachable from outside the module
9  let securityStatus = "active";
10  let powerStatus = "online";
11  const MAX_CAPACITY = 2000;
12
13  // A private helper function
14  function checkSystemStatus() {
15    return powerStatus === "online" && securityStatus === "active";
16  }
17
18  // The public API of the module
19  return {
20    // The method that opens the park to visitors
21    openPark: function() {
22      if (checkSystemStatus()) {
23        parkStatus = "open"; // Modifying a global variable
24        console.log(`${PARK_NAME} is now open to visitors!`);
25        return true;
26      } else {
27        console.log("Cannot open the park - the security systems are not fully operational.");
28        return false;
29      }
30    },
31
32    // The method that registers new visitors
33    registerVisitors: function(count) {
34      // Checking the capacity of the park
35      if (parkStatus !== "open") {
36        console.log("The park is closed. Visitors cannot be registered.");
37        return false;
38      }
39
40      // A block scoped variable
41      const availableCapacity = MAX_CAPACITY - totalVisitors;
42
43      if (count <= availableCapacity) {
44        totalVisitors += count; // Modifying a global variable
45        console.log(`Registered ${count} new visitors. Total: ${totalVisitors}`);
46
47        // Block scope inside a condition
48        if (totalVisitors > MAX_CAPACITY * 0.8) {
49          const warningMessage = "Warning: the park is approaching maximum capacity!";
50          console.log(warningMessage);
51        }
52
53        return true;
54      } else {
55        console.log(`Error: too many visitors. Available capacity: ${availableCapacity}`);
56        return false;
57      }
58    },
59
60    // The method that closes the park
61    closePark: function() {
62      // A local function with access to variables from the wider scope
63      function evacuateVisitors() {
64        console.log(`Evacuating ${totalVisitors} visitors from the park...`);
65        totalVisitors = 0;
66      }
67
68      parkStatus = "closed";
69      evacuateVisitors();
70      console.log(`${PARK_NAME} has been closed.`);
71
72      return true;
73    },
74
75    // The method that changes the status of the systems
76    updateSystems: function(power, security) {
77      // A demonstration of let versus var
78      if (power) {
79        let localMessage = "Updating the power system...";
80        console.log(localMessage);
81        powerStatus = power;
82      }
83
84      if (security) {
85        var statusMessage = "Updating the security system...";
86        console.log(statusMessage);
87        securityStatus = security;
88      }
89
90      // console.log(localMessage); // Error: localMessage has block scope
91      console.log(statusMessage);  // Works: statusMessage has function scope
92
93      return {
94        power: powerStatus,
95        security: securityStatus
96      };
97    }
98  };
99})();
100
101// Using our system
102console.log(`Park status before opening: ${parkStatus}`);
103parkSystem.openPark();
104console.log(`Park status after opening: ${parkStatus}`);
105
106parkSystem.registerVisitors(500);
107parkSystem.registerVisitors(1200);  // Should print the capacity warning
108parkSystem.registerVisitors(400);   // Should print the capacity error
109
110// Updating the systems
111parkSystem.updateSystems("offline", null);
112parkSystem.openPark(); // Should print an error - the park cannot be opened
113
114// Restoring the systems and closing the park
115parkSystem.updateSystems("online", "active");
116parkSystem.closePark();
117console.log(`Park status after closing: ${parkStatus}`);
118console.log(`Visitor count after the evacuation: ${totalVisitors}`);
119
120// An attempt to reach the private variables
121// console.log(securityStatus); // Error: the variable is unreachable
122// console.log(MAX_CAPACITY);   // Error: the variable is unreachable

Every scope from this lesson appears in that one system:

PARK_NAME
and
totalVisitors
are global,
MAX_CAPACITY
and
securityStatus
are private to the module,
availableCapacity
and
warningMessage
live only inside their blocks, and
statusMessage
escapes its
if
block simply because it was declared with
var
. The park exposes a control panel with four methods and hides everything else - that is the whole point of the module pattern.

Good practice rules

  1. Avoid global variables - just like in a real park, the more public areas there are, the harder they are to monitor and secure.

  2. Prefer

    const
    over
    let
    , and
    let
    over
    var
    - most variables should never change their value, and the ones that do should have the smallest possible scope.

  3. Use block scope - limit the visibility of a variable to the smallest area that still works.

  4. Apply the module pattern - encapsulate variables and functions inside modules and expose only the API that is actually needed.

  5. Be aware of closures - closures are a powerful tool, but they can lead to memory leaks when they are used carelessly.

Summary

Variable scope in JavaScript works like the system of access zones in Jurassic Park:

  • Global scope - like the public areas of the park, open to everyone
  • Function scope - like the zones restricted to specific members of staff
  • Block scope - like the individual enclosures and laboratories with their own access system

Understanding scope lets us write better organised, safer and more predictable code - in the same way that good zone management in a park full of dangerous dinosaurs keeps everybody safe.

"Variable scope in JavaScript is like security clearance levels in Jurassic Park" - says Dr. Rex. "A ranger in zone A can see everything in that zone and can also read the park-wide information. A visitor only sees the public areas. The important part is that information never flows downwards - the outer zones cannot look inside the inner ones!"

In the next lesson we will dig into hoisting, a mechanism tightly connected to scope that has a serious influence on how JavaScript code behaves.

Go to CodeWorlds