Scopes in JavaScript
Overview We use variables to store temporary values in them and then access them when we need them. But not all variables in our code are equally accessible. Whether a variable is accessible and how to access it are determined by its scope. A scope is a part of a program where we can access a variable, function or object. This part can be a function, a block, or the whole program - that is, we are always in at least one scope. Scopes can be thought of as boxes in which we put variables. Variables that lie in one box can communicate with each other. Variables can also access variables from the box their box is nested in. Scopes help to hide variables from unwanted access, manage side effects and break code into meaningful blocks. But before we look at how to use them, let's see what JS scopes are. Global Scope Global scope is the outermost box of all. When we ‘just declare a variable’, outside of functions, outside of modules, that variable goes into the global scope. const a = 8 The variable in the example is now in global scope. This means that it will be accessible from anywhere: const a = 8 console.log(a) // 8 function firstWrap() { const b = a // Variable "a" is available in this function. } const firstObjectWrap = { key: a, // Variable "a" is also available here. } function secondWrap() { const secondObjectWrap = { key: a, // Variable "a" is still available. } } Variables in the global scope are called global variables and are available to everyone. The best known example of a global variable is console. console.log(console) // Console {debug: function, error: function, log: function, info: function, warn: function, …} JS in browsers is so organized that global variables fall into the window object. If very roughly, we can say that window in the case of a browser is the global scope. console.log(console) // Console {debug: function, error: function, log: function, info: function, warn: function, …} console.log(window.console) // Console {debug: function, error: function, log: function, info: function, warn: function, …} // Same thing, because it's the same object. A global window object is an object that gives access to the browser's Web API. window recursively “contains itself” because all global objects are in window: console.log(window) // Window {0: Window, ...} We can also define global variables ourselves. For example, if we create a variable in the browser console and then try to access it via window: This will work only with var, but not with let or const. Why, we will find out later. Modular Scope When using ES modules, a variable declared outside of functions will be available, but only in the same module where it was created. // module1.js const a = 8 function wrap() { const b = a // The variable "a" is available in the function. } let c = 0 if (a

Overview
We use variables to store temporary values in them and then access them when we need them.
But not all variables in our code are equally accessible. Whether a variable is accessible and how to access it are determined by its scope.
A scope is a part of a program where we can access a variable, function or object. This part can be a function, a block, or the whole program - that is, we are always in at least one scope.
Scopes can be thought of as boxes in which we put variables. Variables that lie in one box can communicate with each other.
Variables can also access variables from the box their box is nested in.
Scopes help to hide variables from unwanted access, manage side effects and break code into meaningful blocks.
But before we look at how to use them, let's see what JS scopes are.
Global Scope
Global scope is the outermost box of all. When we ‘just declare a variable’, outside of functions, outside of modules, that variable goes into the global scope.
const a = 8
The variable in the example is now in global scope. This means that it will be accessible from anywhere:
const a = 8
console.log(a) // 8
function firstWrap() {
const b = a // Variable "a" is available in this function.
}
const firstObjectWrap = {
key: a, // Variable "a" is also available here.
}
function secondWrap() {
const secondObjectWrap = {
key: a, // Variable "a" is still available.
}
}
Variables in the global scope are called global variables and are available to everyone.
The best known example of a global variable is console
.
console.log(console)
// Console {debug: function, error: function, log: function, info: function, warn: function, …}
JS in browsers is so organized that global variables fall into the window
object. If very roughly, we can say that window
in the case of a browser is the global scope.
console.log(console)
// Console {debug: function, error: function, log: function, info: function, warn: function, …}
console.log(window.console)
// Console {debug: function, error: function, log: function, info: function, warn: function, …}
// Same thing, because it's the same object.
A global window
object is an object that gives access to the browser's Web API. window
recursively “contains itself” because all global objects are in window
:
console.log(window)
// Window {0: Window, ...}
We can also define global variables ourselves. For example, if we create a variable in the browser console and then try to access it via window
:
This will work only with var
, but not with let
or const
. Why, we will find out later.
Modular Scope
When using ES modules, a variable declared outside of functions will be available, but only in the same module where it was created.
// module1.js
const a = 8
function wrap() {
const b = a // The variable "a" is available in the function.
}
let c = 0
if (a < 100) {
c = a // The variable "a" is available in the block.
}
// module2.js
console.log(a)
// ReferenceError: a is not defined
To provide access to certain module data, it must be exported.
Division into modules simplifies the task of code structuring. This is especially important for large projects.
Block Scope
A block scope is bounded by a program block marked with {
and }
. The simplest example of such a scope is an expression inside brackets:
const a = 8
console.log(a) // 8
if (true) {
const b = 17
console.log(a) 8
console.log(b) // 17
}
console.log(b)
// ReferenceError: Can't find variable: b
The variable b
is hidden inside the scope of the block inside the parentheses and is only accessible inside that block, not outside.
Parentheses can, however, not only separate the body of a condition. They can also frame other parts of the code. For example, this is very useful in complex switch
constructions. For example:
switch (animalType) {
case 'dog': {
const legs = 4
const species = 'Shepherd'
break
}
case 'fish': {
const legs = 0
const swims = true
break
}
}
In the example above, we need to execute several lines in the case
. It is convenient to wrap all operations in a block using curly braces - then all variables and operations will be limited to this block, i.e. block scope.
Functional Scope
Functional scope is the scope within the function body. We can say that it is bounded by {
and }
of the function.
const a = 8
function scoped() {
const b = 17
}
console.log(a) // 8
console.log(b) // Reference error
The variable b
is only accessible inside the scoped function.
Functional scope is a very powerful tool for code separation. First of all, using it, we don't have to worry about “crossing variable names”.
You cannot declare let
or const
twice in one scope:
const a = 8
const a = 8
// SyntaxError: Cannot declare a const variable twice: 'a'
But functions create their own scopes that don't overlap, so there is no error in this case:
function scope1() {
const a = 8
}
function scope2() {
const a = 8
}
Since the scopes of functions do not overlap and are not connected, the first function cannot access the ‘insides’ of a neighbouring or nested function, the insides of a neighbouring function are hidden in its scope and inaccessible outside it:
function scope1() {
const a = 8
}
function scope2() {
console.log(a) // Reference error
}
The same is true for child scopes:
function outer() {
function inner() {
const a = 8
}
console.log(a) // Reference error
}
Functions only have access to variables in its own scope (everything inside its body) and in its parent scope:
function outer() {
const a = 8
function inner() {
console.log(a) // 8
}
}
There is no error here, because the function has its own scope available, as well as the scope of the outer
function.
This behaviour, when variables of parent scopes become available in child scopes, is called scope inheritance.
Note that the inner
function does not have any local variables - it works only with the local variable of the outer
function.
This special access to local variables of the parent function is often called lexical scope.
Hiding ‘internals’ allows you to create code blocks independent of each other. This is useful, for example, when we want to run a module in the browser with the assurance that it will not affect other code in any way.
Isolation of Modules with IIFE
Immediately Invoked Function Expression, IIFE is a function that is executed immediately after it has been defined.
IIFE is written like this:
(function foo() {
// Function body
})()
A named function is used here so that there is a readable stacktrace. But it is also allowed to use an anonymous one.
Alternatively, you can add ;
at the beginning of the function:
;(function foo() {
// Function body
})()
The point is that the existing automatic semicolon insertion (ASI) mechanism works only in certain cases, while a string starting with (
is not included in the list of these cases. That's why experienced developers often add ;
in those cases when their code can be copied and added to the existing code.
Let's break it down piece by piece.
- The function itself is inside:
function foo() { ... }
This is a normal function that is described and behaves according to all the rules of functions in JS.
- Brackets around the function:
(function foo() { ... })
Parentheses turn a function into an expression that can be called. That is, if before this step we have declared the function, then at this step we have prepared it for instant call.
- Call brackets:
(function foo() { ... })()
The last pair of brackets calls an expression, that is, it calls the function we created in step 1 and prepared in step 2.
Since the function inside the brackets is a regular function, it just as surely creates a scope within itself that only it can access. That is, everything inside the function remains inside.
With IIFE, we can use the same variable names without fear of accidentally overwriting variable values from other people's modules if we don't control the codebase completely ourselves.
;(function module1() {
const a = 8
console.log(a)
})()
;(function module2() {
const a = 'string'
alert(a)
})()
No name conflicts.
Functions within Functions and Closures
As we have seen above, the child function has access to the scope of the parent function:
function outer() {
let a = 8
function inner() {
console.log(a)
}
inner()
}
outer() // 8
The inner
function still has no local variables, it just uses the local variables of the parent function outer
. And still the code outside the outer
has no access to its internals.
But what if we return from the outer
function to the inner
function?
function outer() {
let a = 8
function inner() {
console.log(a)
}
return inner
}
Now we can not just call the outer
function, but we can also assign the result of the call to some variable:
const accessToInner = outer()
accessToInner() // 8
Now the variable accessToInner
contains the inner
function, which still has access to the local variable a
of the outer
function!
So we were able to "bypass" the scope? Not exactly.
We did access the variable a
through the inner
function, but only in the form and with the constraints described when we created the inner
function.
We still don't have direct access to the variable a
. For example, we can't change it - we can only output it to the console.
Roughly speaking, we've created a function that lets us read variables, but not change them. This is useful if we want to give limited access to the module internals.
Suppose we want to make a counter that can only be incremented and decremented by one:
function counter() {
// The initial value of the counter will be 0.
// We use let because we'll be changing the value, const won't work.
let state = 0
// The increase function will increment the counter by one:
function increase() {
state++
}
// The decrease function will decrease the counter by one:
function decrease() {
state--
}
// The valueOf function will output the value:
function valueOf() {
console.log(state)
}
// And we'll only give the outside access to these functions.
// Let's return an object whose field values will be the increase and decrease functions.
/*
There is still no direct access to the state variable, but external
code can change its state indirectly - through the increase and
decrease functions.
*/
return {
increase,
decrease,
valueOf,
}
}
const ticktock = counter()
ticktock.increase()
ticktock.valueOf() // 1
ticktock.increase()
ticktock.valueOf() // 2
ticktock.decrease()
ticktock.valueOf() // 1
Such controlled access concealment using scope is called closure.
Closures are convenient because each new call creates a separate area where values are completely independent of each other:
const tick1 = counter()
const tick2 = counter()
tick1.valueOf() // 0
tick2.valueOf() // 0
tick1.increase()
tick1.valueOf() // 1
tick2.valueOf() // 0
tick1.increase()
tick1.valueOf() // 2
tick2.valueOf() // 0
tick2.increase()
tick1.valueOf() // 2
tick2.valueOf() // 1
tick2.decrease()
tick1.valueOf() // 2
tick2.valueOf() // 0
The states of both counters are independent of each other, although they are created by the same function.
Hoisting Variables
Above, when we experimented with window
and global variables, we talked about that write:
var variable = 'String'
console.log(window.variable) // String
...will only work with var
, but not with let
or const
. The point here is "hoisting" of variables.
First, let's look at this kind of code:
function scope() {
a = 8
var b = 17
}
scope()
console.log(a) // 8
console.log(b) // Reference error
What happened?
To understand why accessing the variable a
did not cause errors, let's understand how var
and variable declaration work.
Since the variable a
was not declared, JavaScript decided on its own where to declare the variable and "lifted" the declaration to the top. The resulting code is like this:
var a
function scope() {
a = 8
var b = 17
}
scope()
console.log(a) // 8
Moreover, variables are "lifted" inside blocks and functions as well:
console.log(hello) // undefined
var hello = 'String'
console.log(hello) // String
Because what the code actually turns into is this:
var variable
console.log(variable) // undefined
variable = 'String'
console.log(variable) // String
In the browser, global variables are in the window
area, so the window.variable
trick worked - JS "raised" the variable to the global area, and we then set its value.
Problem
This is actually a big problem, because it's easy to forget about "lifting" and accidentally overwrite the value of a variable at an unnecessary moment.
a = 8
function shouldNotAffectOuterScopeButDoes() {
a = 9
console.log(a) // 9
}
shouldNotAffectOuterScopeButDoes()
console.log(a) // 9 (although it should have been 8)
How to Fight
The first weapon against this problem was strict mode.
It forbids declaring variables without var
, which prevents accidentally overwriting an undeclared variable or a variable from an external area.
But the mechanism of "lifting" even inside a block is rather unpredictable. It would be ideal if variables were declared and initialised where specified in the code.
And that's exactly what let
and const
do. They never leave the scope where they were defined and are always initialised where specified.
It'll work with var
:
console.log(variable) // undefined
var variable = 'String'
With let
and const
, no:
console.log(variable) // Reference error
let variable = 'String'
// ...
console.log(variable) // Reference error
const variable = 'String'
Support ❤️
It took a lot of time and effort to create this material. If you found this article useful or interesting, please support my work with a small donation. It will help me to continue sharing my knowledge and ideas.
Make a contribution or Subscription to the author's content: Buy me a Coffee, Patreon, PayPal.