JavaScript Scope, Closures and Hoisting
Scope decides where in your code a variable can be seen and used.
Three Kinds of Scope
| Scope | Created by | Visible where |
|---|---|---|
| Global | A variable outside every function and block | Everywhere |
| Function | A variable inside a function | Only inside that function |
| Block | let or const inside { } (if, for, while) | Only inside those brackets |
Example
const shop = "Karachi Mart"; // global
function showBill() {
const total = 1250; // function scope
if (total > 1000) {
let discount = 100; // block scope
console.log(shop, total - discount);
}
// console.log(discount); would be an error here
}
showBill();
// console.log(total); would be an error hereKarachi Mart 1150
Code inside can look out and see outer variables. Code outside can never look in.
var Ignores Blocks
The old keyword var has function scope, not block scope. A var inside an if or for leaks out of the brackets:
Example
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1, var leaked out
console.log(typeof b); // "undefined", b does not exist hereThis is one of the main reasons modern code uses let and const.
Hoisting
Before running your code, JavaScript scans it and sets up every declaration first. This is called hoisting. It behaves differently for each kind of declaration:
| Declaration | Can you use it before the line? |
|---|---|
function name() {} | Yes, the whole function is ready |
var x | Yes, but its value is undefined |
let x / const x | No, you get a ReferenceError |
Example
<!DOCTYPE html>
<html>
<body>
<p id="demo"></p>
<script>
let out = "";
out += greet("Ayesha") + "<br>"; // works: function declarations are hoisted
out += "price is " + price + "<br>"; // undefined: var is hoisted without its value
var price = 500;
try {
out += city; // let is not ready yet
let city = "Lahore";
} catch (error) {
out += "Error: " + error.name;
}
function greet(name) {
return "Hello " + name;
}
document.getElementById("demo").innerHTML = out;
</script>
</body>
</html>Try it Yourself »Hello Ayesha price is undefined Error: ReferenceError
The time between the start of a block and the let or const line is called the temporal dead zone. The variable exists but cannot be touched yet.
Simple rule: declare your variables at the top of their block, before you use them. Then hoisting never surprises you.
Closures
A function remembers the variables that were around it when it was created, even after the outer function has finished. This is called a closure.
Example
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3makeCounter has finished running, but the inner function still has access to count. Nobody else can change count, so it is safe and private.
A Practical Closure
Closures are great for making small "factories" of functions. Here, each button has its own private tally:
Example
<!DOCTYPE html>
<html>
<body>
<h3>Class Vote</h3>
<button id="pizza">Pizza</button>
<button id="biryani">Biryani</button>
<p id="result"></p>
<script>
function makeVoter(food) {
let votes = 0;
return function () {
votes++;
document.getElementById("result").textContent = food + " has " + votes + " vote(s)";
};
}
document.getElementById("pizza").addEventListener("click", makeVoter("Pizza"));
document.getElementById("biryani").addEventListener("click", makeVoter("Biryani"));
</script>
</body>
</html>Try it Yourself »Each call to makeVoter creates a new votes variable, so Pizza and Biryani count separately.
Closures in Loops
With let, every loop round gets its own copy of the variable, so closures work as you expect. With var, all rounds share one variable:
Example
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log("let:", i), 0);
}
for (var j = 1; j <= 3; j++) {
setTimeout(() => console.log("var:", j), 0);
}let: 1 let: 2 let: 3 var: 4 var: 4 var: 4
Exercise
Exercise
What does this show? function outer() { let x = 5; return () => x * 2; } const f = outer(); console.log(f());
