Function Scope and Hoisting

Function Scope

In JavaScript, defining a function creates a scope. The scope is lexical, or static, which means what you can access is decided by where the code is written, not by what happens at run time.

JavaScript has two kinds of scope:

  1. Global Scope: Variables, functions, and objects declared in the global scope can be used anywhere in the code.
  2. Local Scope: This covers function scopes and block scopes (block scope came with let and const in ES6).

A variable defined inside a function cannot be read from outside that function. That keeps the variable private to the function, and it means the name will not collide with a name in the global scope.

function myFunction() {
  var num = 4;
}
console.log(num);  // ReferenceError: num is not defined

Hoisting

Hoisting means that JavaScript processes function declarations during the compilation phase, before the code starts running. So we can call a function before the point where it appears in the script.

const result = add(2, 1);
console.log(result);  // 3

function add(a, b) {
  return a + b;
}

Function Declarations vs. Function Expressions

Function declarations are hoisted, so we can call them before they are defined. Function expressions do not work that way. When the function is assigned to a const or a let, the variable cannot be accessed before its declaration, so calling it too early gives a ReferenceError.

const result = add(2, 1);
console.log(result); // ReferenceError: Cannot access 'add' before initialization

const add = function (a, b) {
  return a + b;
};

When Hoisting is Helpful

Hoisting makes some patterns possible, such as mutual recursion. Here is an example:

function isEven(n) {
  return n == 0 ? true : isOdd(n - 1);
}

function isOdd(n) {
  return n == 0 ? false : isEven(n - 1);
}

const n = 3;
console.log(`isEven(${n}) -> ${isEven(n)}`); // isEven(3) -> false
console.log(`isOdd(${n}) -> ${isOdd(n)}`); // isOdd(3) -> true

These two functions call each other. Because of hoisting, we can declare them in either order and the code still works.

When Hoisting Leads to Unexpected Results

Hoisting of variables inside a function can give a result you did not expect. Here is an example:

var num = 1;
print();

function print() {
  console.log(num); // undefined
  var num = 2;
}

The local variable num inside print is hoisted to the top of the function’s scope. It shadows the outer num, and it has no value yet at the point of the console.log, so the output is undefined.