In this article, we will dive deep into different types of scopes and how they work in JavaScript. We will also touch on the concepts of lexical scoping and scope chain. Let's start with the concept of scope first.
Scope
A scope is an environment in which variables and functions are created. JavaScript has three types of scopes: global scope, function scope, and block scope.
Global scope
Global scope is created with the top-level variables and functions that are located outside of any function in a script.
const name = 'John';
function logger() {
console.log(name); // John
}
logger();
In the above code, the variable name
and the function logger
create a global scope. Since variables and functions created in the global scope are accessible from anywhere within the scope, we can access the name
variable inside the logger
function.
Function scope
Every function in JavaScript creates its own scope, where variables created within that scope are accessible only inside that scope. Function scope is also referred to as local scope.
function logger() {
const age = 30;
console.log(age); // 30
}
console.log(age); // ReferenceError: age is not defined
In the above code, the logger
function creates a function scope. As per the rule of function scope, the age
variable can be accessed inside the function, but trying to access it outside of the function results in a reference error.
Block scope
Starting from ES6, JavaScript introduced the block scope. A block scope is created by a block of code wrapped around curly braces. For example, an if
statement or a for
loop creates a block scope. Variables created within the block scope are accessible inside that scope and not outside of it.
const name = 'John';
if (name === 'John') {
const message = 'Good morning';
console.log(`${message} ${name}`); // Good morning John
}
console.log(message); // ReferenceError: message is not defined
Following the rule, we can access the message
variable inside the if
block. But when we try to access it outside of the block, we get a reference error.
Important to note that this rule applies to variables created with let
and const
, which is why they are referred to as block-scoped variables.
On the other hand, variables created with var
are function-scoped and accessible both inside and outside of a block scope.
const name = 'John';
if (name === 'John') {
var message = 'Good morning';
console.log(message); // Good morning
}
console.log(message); // Good morning
As you can see in the above code, we can access the message
variable both inside and outside of the if
block as it's created with var
.
In strict mode, function declarations are also block-scoped. This means that function declarations created within a block are only accessible inside that block and not outside of it.
'use strict';
const name = 'John';
if (name === 'John') {
const message = 'Welcome';
function welcome() {
console.log(`${message} ${name}`);
}
welcome(); // Welcome John
}
welcome(); // ReferenceError: welcome is not defined
In the above code, when we try to access the welcome
function outside of the if
block, we get a reference error.
In non-strict mode, the behavior is different. Function declarations created within a block scope can be accessed both inside and outside of that scope. Take a look at the example below.
const name = 'John';
if (name === 'John') {
const message = 'Welcome';
function welcome() {
console.log(`${message} ${name}`);
}
welcome(); // Welcome John
}
welcome(); // Welcome John
Good to know that function expressions and arrow functions created with var
, let
, and const
follow the same scope principles as variables created with these keywords.
Lexical scoping
Lexical scoping refers to the rules that we can access variables from a parent scope within a child scope while preventing access to variables from a child scope within a parent scope and from a sibling scope within another sibling scope. Lexical scoping uses the location of a variable declaration to determine where that variable is available.
function logger() {
const name = 'John';
if (name === 'John') {
const message = 'Good morning';
}
function welcome() {
const age = 30;
console.log(name); // John
console.log(message); // ReferenceError: message is not defined
}
welcome();
console.log(age); // ReferenceError: age is not defined
}
logger();
In the above code, the welcome
function being within the child scope of the logger
function can access the name
variable defined in the logger
function (parent scope).
However, trying to access the age
variable, which is defined in the welcome
function (child scope), from the logger
function (parent scope) results in a reference error.
Similarly, trying to access the message
variable defined in the if
statement (sibling scope) from the welcome
function (sibling scope) also results in a reference error.
This is lexical scoping in action.
Scope chain
Scope chain refers to the concept of accessing variables from one scope within another and the sequence of connections created in the process. A scope chain is created when a child scope accesses a variable from its parent scope.
The process is as follows: a child scope, not finding a variable in its own scope, does a variable lookup within its parent scopes. The lookup continues up to the global scope if the variable isn't found within the parent scopes.
Important to know that variable lookup only works from the inner scope to the outer scope and not the other way around. Following are examples of scope chain creation.
const age = 30;
function welcome() {
const name = 'John';
function introduce() {
const intro = "Hi, I'm";
console.log(`${intro} ${name}. I'am ${age} years old!`); // Hi, I'm John. I'am 30 years old!
}
introduce();
}
welcome();
Take a look at the console.log
within the introduce
function. First, it searches for the intro
variable in its scope and finds it.
It then searches for the name
variable in its scope and, not finding, it does a variable lookup in its parent scope where it finds the name
variable.
Lastly, it searches for the age
variable in its scope, then in its parent scope of the welcome
function, and finally in the global scope where it finds the age
variable.
Notice that accessing each name
and age
variable within the introduce
function links multiple scopes together. This is scope chain creation in action.
With this, it's a wrap for the scope and scope chain in JavaScript. I hope you've learned a thing or two from this article. Writing some code and seeing these in action will reinforce your learning further.
Scope is one of three topics of a JavaScript execution context. If you are interested in learning the other two topics, I recommend checking out the articles on Understanding the dynamic nature of the ‘this’ keyword in JavaScript, as well as Understanding the variable environment and hoisting in JavaScript for further insights.