Scoping is the concept behind how values of variables, functions and other expressions are made available in a program and where they can be accessed from. A good understanding of scope can avoid bugs or unexpected results in a script.

Scope Areas

Scoping can be thought of having three areas, the global scope, a function scope or a block scope.

Global Scope

The global scope is where values that can be accessed anywhere in the script exist and tend to be defined at the top level of your program. For example, if you had a script.js file, the variables and functions defined inside that file would belong to the global scope but anything within a function or a code block (more on this later) would not, for example:

// Available to global scope
const globalScopeVar = 'Can be accessed from anywhere (global scope)';

// Available to global scope
function parentScope() {

  // Not available in the global scope.
  function childScope() {
    return 'child';
  }

  return 'parent';
}

// This variable can be used here since its in the global scope.
globalScopeVar;

// This function may be used since its in the global scope.
parentScope();

// This function does not exist in this scope and would throw an error.
childScope();

In the example above it shows how JavaScript determines what is available to run on the global scope. globalScopeVar and parentScope() are available but childScope() is not because it is nested in a function which makes it bound to another scope.

Nice! Global scoped values may also be called from almost anywhere, even within function and code blocks.

Notice how globalScopeVar can be accessed within parentScope() and childScope():

// Available to global scope
const globalScopeVar = 'Can be accessed from anywhere (global scope)';

// Available to global scope
function parentScope() {

  // Not available in the global scope.
  function childScope() {
    return globalScopeVar;
  }

  return globalScopeVar;
}

// This variable can be used here since its in the global scope.
console.log(globalScopeVar);

// This function may be used since its in the global scope.
console.log(parentScope());

Additionally, can parentScope() be accessed from childScope() like globalScopeVar is? Yes! Because parentScope() if defined at the global scope level:

// Available to global scope
const globalScopeVar = 'Can be accessed from anywhere (global scope)';

// Available to global scope
function parentScope() {
  // Not available in the global scope.
  function childScope() {
    return parentScope();
  }

  console.log(childScope());
}

// This variable can be used here since its in the global scope.
console.log(globalScopeVar);

// This function may be used since its in the global scope.
console.log(parentScope());

This is probably a not so useful example of how functions are applied in practice as it turns parentScope() into a function that calls itself which most likely will lead to a call stack error that looks similar to the output below.

However, it is valid JavaScript and parentScope() can be used by childScope()because it was defined in the global scope.

script.js:8 Uncaught RangeError: Maximum call stack size exceeded
    at childScope (script.js:8)
    at parentScope (script.js:11)
    at childScope (script.js:8)
    at parentScope (script.js:11)
    at childScope (script.js:8)
    at parentScope (script.js:11)
    at childScope (script.js:8)
    at parentScope (script.js:11)
    at childScope (script.js:8)
    at parentScope (script.js:11)

The take away is that global scope values are available almost anywhere in your program.

Global variables tend to be avoided as it could lead to bugs, can end up being changed unintentionally and cause unexpected behavior so the unspoken rule tends to be to avoid using them or using them carefully.

Function Scope

A function scope if determined by the curly brackets { } of its block. Any variables, functions or expressions defined within those blocks won’t be available outside of the block.

On the following example notice how the parentScopeVar is not available outside of the function it was defined in (parentScope()):

// This variable can be accessed from anywhere.
var globalScopeVar = 'Can be accessed from anywhere';

function parentScope() {
  // This variable can only be accessed within this function and its child function and code blocks.
  var parentScopeVar =
    'This variable can only be accessed within this function and its children';
}

parentScopeVar; // Error of undefined! Not defined in this scope

This example will throw an error. Values defined inside the scope of a function are not available outside of its block:

types.js:14 Uncaught ReferenceError: parentScopeVar is not defined
    at types.js:14

Like we have observed in a previous example, values from the global scope or higher scopes can be used inside the function, just not the other way around:

// This variable can be accessed from anywhere.
var globalScopeVar = 'Can be accessed from anywhere';

function parentScope() {
  // This variable can only be accessed within this function and its child function and code blocks.
  var parentScopeVar =
    'This variable can only be accessed within this function and its children';

  return globalScopeVar;
}

parentScope() // Returns 'Can be accessed from anywhere';

Notice how the globalScopeVar is available to be used inside the function block. This can be thought like the globalScopeVar can cross the “gates” (as in curly brackets) and get in parentScope(), becoming available. On the other hand, parentScopeVar can never leave the “gates” of parentScope(), hence why it won’t be able to be accessed anywhere else.

But what if parentScope() had another function nested inside its block? Would parentScopeVar still be available in the that function? Would the function be available in the global scope like parentScope() is?

You might have an idea of what the answer to these questions are, but if not it’s totally okay. Let’s consider the following example:

function parentScope() {
  // This variable can only be accessed within this function and its child function and code blocks.
  var parentScopeVar =
    'This variable can only be accessed within this function and its children';

  // This function is only available to the parentScope. 
  function childScope() {
    parentScopeVar;
  }

  childScope(); // Success! childScope is available within this block.
}

parentScope(); // Success! parentScope is available in the global scope.
childScope(); // Error! childScope is only available at the parentScope.

In the example above we can see how parentScopeVar is available to childScope() but childScope() is only available within the parentScope() block and it cannot be called in the global scope.

The key points are that variables and functions declared within a function are not available outside of its code block. However, they are available to be used inside the block and other nested function blocks if needed just like all the variables are available to every other code block when defined under the global scope.

Block Scope

The block scope is similar to the function scope in that global scoped values are available to it but it has a key difference. Variables, functions and other expressions defined within these blocks will be available to the scope they’re currently part of and not limited by the curly brackets.

Block scope is talked about when using if, switch, for and other types of code blocks for control flow or iteration. Take a look of an example of a couple of code blocks being used in the global scope and how values defined within the code block can be accessed outside their curly brackets ({ }):

// This variable can be accessed from anywhere.
var globalScopeVar = 'Can be accessed from anywhere';

// Code blocks won't affect the scope of a variable.
if (true) {
  var secondGlobalScopeVar = 'Can be accessed from anywhere';

  globalScopeVar; // Success! It's available in the global scope and can be accessed in the block.
}

// Variables in a loop will still be available and in scope after the loop is done.
for (var index = 0; index < [1,2,3,4,5].length; index++) {
  console.log('Global scoped loop:', index);

  globalScopeVar; // Success! It's available in the global scope and can be accessed in the block.
}

secondGlobalScopeVar; // Success! The if statement block will run and it's available in the global scope.
index; // Success! It's available in the global scope.

In the example above, index and secondGlobalVar can be accessed from outside their blocks. Variables declared with var are not bound to the limits of the blocks.

However, there is a way to scope index and secondGlobalScopeVar to their blocks and keep them from being available in outer scopes by using let and const. Here’s the same example using those keywords but more on this topic later:

// This variable can be accessed from anywhere.
let globalScopeVar = 'Can be accessed from anywhere';

// Code blocks won't affect the scope of a variable.
if (true) {
  let secondGlobalScopeVar = 'Can be accessed inside this block';

  globalScopeVar; // Success! It's available in the global scope and can be accessed in the block.
}

// Variables in a loop defined with let won't belong to the scope after the loop is done.
for (let index = 0; index < [1,2,3,4,5].length; index++) {
  console.log('Global scoped loop:', index);

  globalScopeVar; // Success! It's available in the global scope and can be accessed in the block.
}

secondGlobalScopeVar; // Error! This variable is not defined in this scope.
index; // Error! This variable is not defined in this scope.

Defining variables with let and const are a way of scoping them to their code blocks.

Example of scope using var

Now that there’s been an introduction to scope, let’s have a look at a larger example using var. Try to read it line by line and see how the rules we’ve described so far apply to this piece of code:

/*
 * How Javascript scope works using var
 */

// This variable can be accessed from anywhere.
var globalScopeVar = 'Can be accessed from anywhere';

function parentScope() {
  // This variable can only be accessed within this function and its child function and code blocks.
  var parentScopeVar =
    'This variable can only be accessed within this function and its children';

  // Global scope variables are available in this function scope.
  console.group('parentScope');
  console.log('parentScope can access globalScopeVar: ', globalScopeVar);
  console.log('parentScope can access parentScopeVar: ', parentScopeVar);
  console.log('parentScope can access secondParentScope (function): ', secondParentScope);
  console.groupEnd('parentScope');

  /* parentScope CANNOT access:
    childScopeVar // undefined in this scope
    secondParentScopeVar // undefined in this scope
  */

  // This function is only available to the parentScope. 
  function childScope() {
    // This variable can only be accessed within this function and its child function and code blocks.
    // Cannot be accessed by parentScope or the globalScope.
    var childScopeVar = 'Only available withing this function scope and its children';
    
    console.group('childScope');
    // Global scope variables are available in this function scope.
    console.log('childScope can access globalScopeVar: ', globalScopeVar);
    // Can access the variable defined by its parent.
    console.log('childScope can access parentScopeVar: ', parentScopeVar);
    console.log('childScope can access childScopeVar: ', childScopeVar);
    console.groupEnd('childScope');

    /* childScope CANNOT access:
      secondParentScopeVar // undefined in this scope
    */
  }

  // childScope() is only available to the parentScope
  childScope();
}

function secondParentScope() {
  var secondParentScopeVar =
    'This variable can only be accessed within this function and its children';
  
  console.group('secondParentScope');
  console.log('secondParentScope can access globalScopeVar: ', globalScopeVar);
  console.log('secondParentScope can access secondParentScopeVar: ', secondParentScopeVar);
  console.groupEnd('secondParentScope');

  /* The global scope CANNOT access within this block:
    parentScopeVar; // undefined in this scope
    childScopeVar // undefined in this scope
    childScope() // undefined in this scope
  */
}

// Code blocks won't affect the scope of a variable.
if (true) {
  var secondGlobalScopeVar = 'Can be accessed from anywhere';

  console.log('Global scope can access globalScopeVar (in if code block):', globalScopeVar);
  
  /* The global scope CANNOT access:
    parentScopeVar; // undefined in this scope
    childScopeVar // undefined in this scope
    childScope() // undefined in this scope
    secondParentScopeVar // undefined in this scope
  */
}

// Variables in a loop will still belong to the scope after the loop is done.
for (var index = 0; index < [1,2,3,4,5].length; index++) {
  console.count('Global scoped loop');
}

// globalScopeVar can be accessed in the global scope with no issues.
console.log('Global scope can access globalScopeVar:', globalScopeVar);
// secondGlobalScopeVar can be accessed in the global scope even though it was defined within a code block.
// If the statement didn't evaluate to true then this variable would be undefined.
console.log('Global scope can access secondGlobalScopeVar:', secondGlobalScopeVar);
// index can be accessed in the global scope even though 
// the loop is done andit was defined within a code block.
console.log('Global scope can access index:', index);

// Running parentScope.
parentScope();
// Running secondParentScope.
secondParentScope();

/* The global scope CANNOT access:
  parentScopeVar; // undefined in this scope
  childScopeVar // undefined in this scope
  childScope() // undefined in this scope
  secondParentScopeVar // undefined in this scope
*/

This example is also available as a Gist in case you would like to read it in your code editor or run it yourself.

How let and const affect scope

On an earlier example we’ve seen how let and const can scope a variable to its code block (e.g if and for) making it unavailable anywhere else.

The let and const declarations are block scoped. This brings the benefits of not being able to access a value that is part of a different scope which might prevent it from changing unexpectedly.

The use of let and const tends to be preferred to var, here’s a breakdown of the differences between them:

  • var can be updated but not redeclared
  • let can be updated but not redeclared and is block scoped
  • const cannot be updated or redeclared and is block scoped

Example of scope using let and const

This is an updated example of how this script would work using let and const. Take a minute to compare the two and try to see the difference and spot what variables are no longer available:

/*
 * How Javascript scope works using let and const
 * It is more restrictive as to where values can be accessed within functions and blocks
 */

// This variable can be accessed from anywhere.
const globalScopeVar = 'Can be accessed from anywhere (global scope)';

function parentScope() {
  // This variable can only be accessed within this function and its child function and code blocks.
  let parentScopeVar =
    'This variable can only be accessed within this function and its children';

  // Global scope variables are available in this function scope.
  console.group('parentScope');
  console.log('parentScope can access globalScopeVar: ', globalScopeVar);
  console.log('parentScope can access parentScopeVar: ', parentScopeVar);
  console.log('parentScope can access secondParentScope (function): ', secondParentScope);
  console.groupEnd('parentScope');

  /* parentScope CANNOT access:
    childScopeVar // undefined in this scope
    secondParentScopeVar // undefined in this scope
  */

  // This function is only available to the parentScope. 
  function childScope() {
    // This variable can only be accessed within this function and its child function and code blocks.
    // Cannot be accessed by parentScope or the globalScope.
    const childScopeVar = 'Only available withing this function scope and its children';
    
    console.group('childScope');
    // Global scope variables are available in this function scope.
    console.log('childScope can access globalScopeVar: ', globalScopeVar);
    
    // Can access the variable defined by its parent.
    parentScopeVar = 'parentScopeVar was modified within childScope()';
    console.log('childScope can access parentScopeVar: ', parentScopeVar);
    console.log('childScope can access childScopeVar: ', childScopeVar);
    console.groupEnd('childScope');

    /* childScope CANNOT access:
      secondParentScopeVar // undefined in this scope
    */
  }

  // childScope() is only available to the parentScope
  childScope();
}

function secondParentScope() {
  const secondParentScopeVar =
    'This variable can only be accessed within this function and its children';
  
  console.group('secondParentScope');
  console.log('secondParentScope can access globalScopeVar: ', globalScopeVar);
  console.log('secondParentScope can access secondParentScopeVar: ', secondParentScopeVar);
  console.groupEnd('secondParentScope');

  /* The global scope CANNOT access within this block:
    parentScopeVar; // undefined in this scope
    childScopeVar // undefined in this scope
    childScope() // undefined in this scope
    secondGlobalScopeVar // undefined in this scope
  */
}

// Code blocks won't affect the scope of a variable.
if (true) {
  let secondGlobalScopeVar = 'Can be accessed from this block only';

  console.log('Global scope can access globalScopeVar (in if code block):', globalScopeVar);
  console.log('Only this block can access secondGlobalScopeVar:', secondGlobalScopeVar);
  
  /* The global scope CANNOT access:
    parentScopeVar; // undefined in this scope
    childScopeVar // undefined in this scope
    childScope() // undefined in this scope
    secondParentScopeVar // undefined in this scope
  */
}

// Variables in a loop will still belong to the scope after the loop is done.
for (let index = 0; index < [1,2,3,4,5].length; index++) {
  console.count('Index may be accessed from this loop only');
}

// globalScopeVar can be accessed in the global scope with no issues.
console.log('Global scope can access globalScopeVar:', globalScopeVar);

// Running parentScope.
parentScope();
// Running secondParentScope.
secondParentScope();

/* The global scope CANNOT access:
  parentScopeVar; // undefined in this scope
  childScopeVar // undefined in this scope
  childScope() // undefined in this scope
  secondParentScopeVar // undefined in this scope
  secondGlobalScopeVar // undefined in this scope
  index // undefined in this scope
*/

This example is also available as a Gist in case you would like to read it in your code editor or run it yourself.

Resources