Scope is a fundamental feature of the JavaScript language.

It defines the rules and range of accessing variables. Practically speaking if we want to know if variables or functions in a certain place of code are valid we need to know the scope we are working in.

JavaScript uses lexical (static) scoping (as opposed to the rarely seen in programming world – dynamic scoping). That means that scope is determined by the way code is defined and not by how it is executed.

Types of scope in JavaScript

We can single out two scope types:

  • Global scope
  • Local scope

Global scope is the outermost scope of the whole program. It’s variables are reachable from anywhere.

Local scope is a scope reachable only from a certain part of the code. For example every function has it’s own local scope and variables declared in it are reachable only from inside of it.

Example

function example() {
  var someVar = 'someVal'; // variable declared inside the function's local scope
}

console.log(someVar); // ReferenceError (variable is not reachable in the global scope)

That doesn’t mean that we cannot look for variables outside of the current local scope we are in. Let’s try to invert our example.

Inverted example

var outerVar = 'outerVal'; // variable declared in the global scope

function example() {
  console.log(outerVar);
}

example(); // logs outerVal

It worked because even though we declared outerVar outside of the functions local scope where we logged it, it was in the reachable outer scope. How to know if we can reach variable or not? Variables are looked up following nesting rules.

Nesting rules

First we inspect the scope we are in, if we can’t find the variable there we go one level (scope) up, then one level up and so on till we reach our variable or the most outer global scope.

Three level example

var globalVar = 'global'; // global scope

function levelOne() {
  var levelOneVar = 'levelOne'; // level one local scope
  
  // levelOne and the outer globalVar are accessible
  console.log(globalVar, levelOneVar); 

  function levelTwo() {
    var levelTwoVar = 'levelTwo'; // level two local scope
    
    // levelTwo and the outer globalVar and levelOneVar are accessible
    console.log(globalVar, levelOneVar, levelTwoVar);
  }	
  
  levelTwo();
}

// only globalVar is accessible
console.log(globalVar);

levelOne();

We can see three scopes here: global scope, levelOne local scope, levelTwo local scope. We can only log the variables declared inside the current scope or it’s outer scopes. So variable look up can move outwardly but cannot move inwardly.

What if we don’t find the variable?

  • If we don’t find variable in any reachable scope we get a ReferenceError
  • if we try to assign a value to variable not declared with identifier (i.e. without a var keyword) it depends if the strict mode is on.
    • no strict mode – variable is created in the global scope
    • strict mode – we get a ReferenceError

What if there are variables in different levels with the same identifier name?

  • the inner variable “shadows” the outer one and it is the one being looked up.

Example

var someVar = 'outer';

function innerScoped() {
  var someVar = 'inner';

  console.log(someVar);
}

innerScoped(); // logs inner

How to create local scope

As we saw one way is to define a function. Another common way is to create block scope which can be achieved by using let and const keywords for declaring variable identifiers and wrapping the code with curly braces {}.

Example

{
  let someVar = 'someVal'; // declared inside the block scope
}

console.log(someVar); // ReferenceError (variable is not reachable in the global scope)

Variables declared with var keyword doesn’t have block scope, if we would use var to declare someVar in the example, we could be able to reach it outside. There are some other less common ways of creating block scope (i.e. using try catch blocks for pre ES6 code)

let and const

Different keywords used to declare variable identifiers follow different rules concerning scope. The let and const keywords (introduced in ES6) are now the most common way to declare variable identifiers. So how are they different from the common var keyword? Besides that they can be used with block scope, they are not hoisted (hoisting is explained a bit further). Also values of variables declared with const cannot be reassigned.

Example

const someVar = 'someVal';
someVar = 'newVal'; // TypeError (const value is not changeable)

Note. If const variable is holding object reference it doesn’t mean you cannot change objects parameters, you just cannot assign another object.

Hoisting

JavaScript scope is being defined before execution (even though JavaScript is not manualy compiled way before execution, it is actually compiled under the hood and that is when lex scope is created). Why do we need to know that? First thing, it is important to know the tools we are using. Another thing, it explains things like hoisting.

Variables defined with var keyword are moved to the top of their scope (global or local) during compilation (with undefined value) and value is assigned only when the expression is reached during execution. Hoisting is this behaviour of moving variable declarations to the top. This could lead to unexpected results.

Example

function demonstrateHoisting() {
  'use strict';
  someVar = 'someVal';

  console.log(someVar); 
  
  var someVar;
};

demonstrateHoisting(); // logs someVal

Earlier I wrote what happens if we try to assign a value to variable not declared with identifier. If we are using strict mode we get a ReferenceError. But here we get someVal. We are not creating a global variable, this is hoisting in action. Even though someVar appears to be not declared with an identifier, it actually was, because the declaration (var someVar;) was moved to the top of the scope (above the value assignment). So we had it all along. JavaScript is not always top-to-bottom simple.

Function expressions attached to a variable follow variable hoisting rules but function declarations are hoisted differently.

Function declarations are hoisted above the variables and they have their values initialized straight away. That is why you can call a function before it is declared this way.

Example

callMe(); // logs Hello

function callMe() {
  console.log('Hello');
}

Scope is a huge topic and it’s rules are applied in so many aspects of JavaScript it’s hard to find a place stop talking about it. You can learn more and read about closures, JavaScript compilation, or dive deeper in to any of the mentioned concepts or tools.

Best Wishes, Vladas Končius


Leave a Reply

Your email address will not be published.