Scope and Hoisting in JavaScript
A Deep dive in to Scope and Hoisting in JavaScript
What is Scope ?
Scope is set of rules for looking up variables by their identifier name. Scope answers the question, where and how a variable is to be looked up within a program Courtesy YDKJS
1. Global Scope
If you have a variable defined & assigned outside of any block/functions, it can be accessed globally.
e.g.
const a = 5 // Here both definition and assignment is taking place in a single statement
console.log(a) // 5 ; a is available globally as it is being outside of any function/block
function foo () {
console.log(a); // a being a global variable is available inside a function scope as well
}
What if we have another variable 'b' inside a function scope foo, can we access 'b' outside scope of foo ?
const a = 5 // Here both definition and assignment is taking place in a single statement
console.log(a) // 5 ; a is available globally
function foo () {
const b = 10;
console.log(a); // a being a global variable is available inside a function scope as well
console.log(b); // b is having function scope of function foo and accessible inside function foo only
}
console.log(b); // Reference error , b is not defined ; because 'b' is not available outside scope of function 'foo'
Block level Scope in JavaScript :
In JavaScript, with introduction of 'let' & 'const' in ES6 every block can create a scope bubble with let/const declaration, it means the variables defined with let/const inside a block shall be accessible inside the block only.
The difference between let and const is that the variables defined with const cannot be re-assigned values once defined.
Nested Scope : Almost every program is written in nested blocks, e.g. Global>Function/Class>Loop blocks are typical for a JavaScript program.
The process of Scope seeking (Resolving value of a variable by variable name) starts with the inner most block and continues till global scope in order to resolve the value of an identifier.
If the variable is not resolved at the current(Inner most) block then immediate outer block is referred, if the variable is not resolved then outer block next to it is referred until the global scope is reached.
If the variable is not resolved at global level as well then error is thrown.
const a = 5 // global variable available at global level
function foo () {
const b = 10; // 'b' is available inside 'foo' only
function bar () {
const c = 20; // 'c' is available inside 'bar' only
if(true){
let d = 30;
console.log(a) // 5 ;global scope
console.log(b) // 10 ;from the scope of 'foo'
console.log(c) // 20 ; from scope of 'bar'
console.log(d) // 30 ;self scope i.e. scope if block within 'bar'
}
console.log(a) // 5 ;global scope
console.log(b) // 10 ;from the scope of 'foo'
console.log(c) // 20 ; from scope of 'bar'
console.log(d) // ReferenceError: 'd' cannot be accessed outside it's block scope of if block
}
bar();
console.log(a) // 5 ;global scope
console.log(b) // 10 ;from the scope of 'foo'
console.log(c) // Reference Error ; cannot access the variable 'c' outside it's scope of function 'bar'
console.log(d) // ReferenceError: 'd' cannot be accessed outside it's block scope
}
foo();
bar(); // Reference Error ; cannot access the function 'bar' outside it's scope of function 'foo'
console.log(a) // 5 ;global scope
console.log(b) // Reference Error ; cannot access the variable 'b' outside it's scope of function 'foo'
console.log(c) // Reference Error ; cannot access the variable 'c' outside it's scope of function 'bar'
console.log(d) // ReferenceError: 'd' cannot be accessed outside it's block scope
Shadowing of scope : If you have a variable inside a function, having name same as a global variable,
a) If you access the identifier from the function itself, the variable will resolve to local value only a) If you access the identifier at global level, the variable will resolve to the global value
e.g.
const a = 10; // outer 'a'
function foo () {
const a = 20; // local 'a'
console.log(a); // 20 ; It will resolve at local level and print 20
}
foo();
console.log(a); // 10 ; It will resolve at global level and print 10
Here variable 'a' is shadowing the global variable 'a' inside the scope of function 'foo'
undefined V/s not defined
Let's try to understand this example
function foo () {
console.log(a); // ReferenceError : 'a' is not defined
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b;
}
Did you see the difference ?
In above example the variable 'a' is not defined anywhere thus giving ReferenceError : 'a' is not defined.
But in case of b , the variable is defined but was accessed before declaration hence the error "Uncaught ReferenceError: Cannot access 'b' before initialization"
Let us take a look at another code snippet
function foo () {
a = 10;
var a;
console.log(a);
}
What could be the possible output of above snippet ?
The answer is 10
function foo () {
console.log(a);
var a = 10;
}
What could be the possible output of above snippet ?
The answer is undefined
How come two almost identical scenarios (accessing a variable before it's declaration) is leading to different outputs ?
It is because of hoisting
Hoisting
Hoisting is the phenomenon where all the declarations (function or variables declared using 'var') are processed first (i.e. moved at the top within the block)
Hoisting happens because of compilation of code. The JavaScript compiles the code before interpreting it. So in the process of compilation all the declaration are moved to the top and process first.
This problem of hoisting is managed by introduction of let & const in ES6
How ?
let : if you declare a variable using let you can not access it before initialization. It will throw an error as "Cannot access variable before initialization"
const : const will not let you declare without assigning a value. If you try to declare a variable using const and decide not to initialize it . It will throw an error "Uncaught SyntaxError: Missing initializer in const declaration" Even if you initialize the variable and try to access before it's initialization, you may face an error as "Cannot access variable before initialization"
function foo() {
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
const a = 10;
}
function bar() {
console.log(b); // Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 10;
}
function zoo() {
console.log(b); // Uncaught SyntaxError: Missing initializer in const declaration
const b;
}
TDZ Now we know that, accessing let or const variables before they’re declared will throw a ReferenceError. This period between entering scope and being declared where they cannot be accessed is called the Temporal Dead Zone
Now take a look at another example related to Hoisting
console.log(foo()); // Hello from function foo
function foo () {
return "Hello from function foo"
}
From above snippet it is clear that the function declaration are hoisted In JavaScript.
However if we try same thing with function expression, i.e. arrow functions of ES6 then we will see that the function expression is not hoisted, only the declaration of variable 'foo' is hoisted that too if we using 'var' for declaration
console.log(foo()); // Uncaught TypeError: foo is not a function
var foo = () => "Hello from Arrow Function foo"
If you try the same thing with let/const, it won't even let the declaration of 'foo' to be hoisted on top
console.log(foo()); // Uncaught ReferenceError: Cannot access 'foo' before initialization
const foo = () => "Hello from Arrow Function foo"
This is part of my series on JavaScript, JavaScript Made Easy , you can view other interesting articles there.
Subscribe to get e-mail and updates for my learning experience with JavaScript and React JS