Skip to Main Content

Javascript Basics: Scope

One of the more abstract concepts a developer needs to understand quite early into their development journey is the concept of scope. In a simplified way, scope means when is a variable available to be used. If a variable is not in scope, you cannot refer to it. Let's see how this works in practice in Javascript. We'll start with the base rules and then look at some of the exceptions and how they work.

The baseline

The best way to think about scope is this:

A variable enters the scope when it's created and exits the scope when the block ends. While it is in the scope, you can refer to it.

Creating a variable

A variable is created in Javascript with one of the three keywords: var, const or let. So when you see a line let year = 2021, the variable year enters the scope. From this line onwards, you can refer to year inside your code.

If you try to access a variable before it's been created (or initialized as the lingo goes), you'll get a ReferenceError: year is not defined error.

There is a special case in Javascript for variables created with the keyword var, I'll talk about that a bit later under the subheading Hoisting.

Block

Above I also mentioned the variable exiting the scope when "the block ends". A block starts with an open curly brace { and ends with a closed curly brace }. Examples of blocks you've probably already seen are functions, if clauses and for loops but you can also create a block with just the curly braces if you ever want to create a block with local scope.

function add(x, y) { // block starts here
  let sum = x + y; 
  return sum;
} // block ends here, variables x, y and sum are no longer in scope

let year = 2021
if(year === 2021) { // block starts here
  console.log(`It's 2021`);
} // block ends here

{ // block starts here
  let x0 = 1;
  let x1 = 2;
  console.log(add(x0, x1));
} // block ends here, variables x0 and x1 are no longer in scope
A few examples of blocks starting and ending in Javascript

The crucial part of understanding the concept of block is that any variable created inside that block will cease to exist when the block ends.

Functions

function createPerson(name, birthYear, hobbies) {
  let currentAge = 2021 - birthYear;
  let person = {
    name: name,
    age: currentAge,
    birthYear: birthYear,
    hobbies: hobbies
  };
    
  return person;
}

const molly = createPerson("Molly", 1992, ["hiking", "photography"]);

console.log(molly) 
// prints out 
// { name: "Molly", age: 29, 
//   birthYear: 1992, 
//   hobbies: ["hiking", "photography"] }

console.log(person) // ReferenceError

In the above example, we have two blocks: one for function createPerson and the other one containing all the rest (including the function definition).

Any variable that's been defined either in the function definition (parameters: name, birthYear and hobbies) or inside the function body (currentAge and person) are only accessible inside that function body. On the last line, we try to print out person but we get ReferenceError because person is not defined in that scope.

This is crucial in understanding how the data flows in Javascript: to make something available inside a function, you need to pass it into it via arguments and to make something from inside a function available outside it, you need to pass it back via return statement.

There's another way of creating a function in Javascript too but the same base rules apply to it:

const createPerson = (name, birthYear, hobbies) => {
  let currentAge = 2021 - birthYear;
  let person = {
    name: name,
    age: currentAge,
    birthYear: birthYear,
    hobbies: hobbies
  };
    
  return person;
}

Exceptions to the baseline

Understanding the baseline above will get you through most of the use cases and they are applicaple to most other programming languages as well. But it's valuable to understand the exceptions too for the moments when you run into them in code or need to use them yourself.

Hoisting

The first exception to the rule is called hoisting. It's a concept in Javascript describing how the compiler allocates memory before executing the code. But for practical purposes, what you need to know is how sometimes the declaration and initialization of a variable changes and how functions are hoisted.

Variables

Variables defined by var keyword are hoisted while ones defined by either const or let are not. The latter two are more recent addition to the language and partly for this very reason.

console.log(message); // prints undefined
var message = "Message is gonna be hoisted";

In our above example, we don't get the ReferenceError we'd get previously for trying to access a variable that's not in the scope. That's because here's what this code ends up being before it's ran:

var message;
console.log(message);
message = "Message is gonna be hoisted";

Javascript hoists the declaration of the variable to the top but not its definition so its value will be undefined until line 3.

The reason hoisting of variables can be a problem is that variables defined with var can be redeclared in the same scope:

var year = 2021;
var year = 2022;

This can become a problem if you have a long, complex code where you accidentally end up reusing a variable name.

For example's sake, let's say you have code like this.

var isAllowed = checkUserPermissions(user); // let's say it returns false

// 30 lines of code

if(isAllowed) { openDoors(); }

Now, later on, someone needs to add a piece of code in between:

var isAllowed = checkUserPermissions(user);

// 30 lines of code

var userList = [ { id: 1 }, { id: 2 }, { id: 3 }];
for (var user of userList) {
  var isAllowed = user.id > 2;
  user.isAllowed = isAllowed;
}

if(isAllowed) {
  openDoors();
}

The developer writing the new code may not see that there already exists a variable called isAllowed and thinks they are initializing a new variable for their local scope. However, because of hoisting, the isAllowed inside the for loop is modifying the exact same variable as the one on line 1.

With let and const this is not possible because there's no hoisting and because they cannot be redeclared.

Functions

Another thing that gets hoisted is function definitions that use function keyword:

hello("World"); // prints Hello World

function hello(target) {
  console.log(`Hello ${target}`);
}

This works because all function definitions (that use function keyword) are hoisted so they are available anywhere. This is actually really nice because it allows you to define functions at the end of the file and write the main logic on top.

However, I mentioned earlier that you can define functions with the arrow syntax as well:

hello("World");

var hello = target => console.log(`Hello ${target}`);

Even when hello is declared with var keyword, the definition does not get hoisted, only the declaration so hello is undefined in line 1 and we get TypeError: hello is not a function if we try to run this.

Local and global scope

Another exception to the rule is global scope. In our Functions section I mentioned that the way data is passed into a function is through its arguments. This is not entirely the only way to do it, however it's highly suggested to avoid global variables because it's harder to keep track of them and see where they are changed.

let number = 42;

function addToNumber(value) {
  return value + number;
}

console.log(addToNumber(20)); //prints 62

Any variable that's defined outside the functions is global and accessible anywhere after its initialization, including other blocks. In a small example like above, it's relatively easy to make sure addToNumber returns what we want but what if someone adds another function call before it:

let number = 42;

makeMagicHappen();

function addToNumber(value) {
  return value + number;
}

console.log(addToNumber(20));

Here, you don't know what makeMagicHappen or any function it is calling is doing to number. They can change it as they wish (it's global) so your function is now entirely dependent on every other piece of code that gets run before yours.