Scope in JavaScript
Scope determines where variables can be accessed in your code. In JavaScript, there are three main types of scope: global scope, function scope, and block scope (ES6+). Understanding scope is essential for avoiding bugs such as variable shadowing, variable leaking, and unexpected behavior in nested loops.
Types of Scope
1. Global Scope
Variables defined outside any function or block — they can be accessed from anywhere.
// Global scope
const universityName = "Nusantara University";
let countStudent = 5000;
function displayInfo() {
// Can access the global scope
console.log(universityName);
console.log(countStudent);
}
displayInfo(); // ✅ Works
console.log(universityName); // ✅ Works too
2. Function Scope (var)
var is defined in the function scope — it can only be accessed inside that function.
function calculateGrade() {
var grade = 85; // function scope
var passed = true;
console.log(grade); // ✅ 85
console.log(passed); // ✅ true
}
calculateGrade();
// console.log(grade); // ❌ ReferenceError!
// console.log(passed); // ❌ ReferenceError!
3. Block Scope (let, const)
let and const are defined in the block scope — they can only be accessed inside the {} block where they are defined.
// Block scope
if (true) {
const message = "Hello"; // block scope
let count = 0; // block scope
console.log(message); // ✅ "Hello"
console.log(count); // ✅ 0
}
// console.log(message); // ❌ ReferenceError!
// console.log(count); // ❌ ReferenceError!
// A loop is also a block scope
for (let i = 0; i < 3; i++) {
const num = i * 2;
console.log(num);
}
// console.log(i); // ❌ ReferenceError!
// console.log(num); // ❌ ReferenceError!
Code Example: Scope Chain
// Scope chain — an inner scope can access outer scopes
const global = "I am global";
function outer() {
const outerVar = "I am outer";
function middle() {
const middleVar = "I am middle";
function inner() {
const innerVar = "I am inner";
// Inner can access every scope above it
console.log(innerVar); // ✅ "I am inner"
console.log(middleVar); // ✅ "I am middle"
console.log(outerVar); // ✅ "I am outer"
console.log(global); // ✅ "I am global"
}
inner();
}
middle();
}
outer();
// The scope chain only works UPWARD, not SIDEWAYS
function functionA() {
const fromA = "this is from A";
}
function functionB() {
const fromB = "this is from B";
// ❌ functionB cannot access functionA's variables
// console.log(fromA); // ReferenceError!
}
functionA();
functionB();
Variable Shadowing
This occurs when a variable in an inner scope has the same name as a variable in an outer scope.
const name = "Global John";
function showName() {
const name = "Local John"; // shadowing!
console.log(name); // "Local John" — uses the closest one
}
showName();
console.log(name); // "Global John"
// Shadowing in a loop
const grade = 100;
for (let i = 0; i < 3; i++) {
const grade = i * 10; // shadowing!
console.log(`Loop ${i}: grade = ${grade}`);
// Loop 0: grade = 0
// Loop 1: grade = 10
// Loop 2: grade = 20
}
console.log(grade); // 100 (still global)
// ⚠️ var and shadowing — different behavior
var x = 10;
function tryIt() {
var x = 20; // shadowing — still exists because var is function-scoped
if (true) {
var x = 30; // this changes x in the function scope, not a block scope!
console.log(x); // 30
}
console.log(x); // 30 (not 20!)
}
tryIt();
console.log(x); // 10 (global)
Code Explanation
| Keyword | Scope | Hoisting | Reassign | Re-declare |
|---|---|---|---|---|
var |
Function | ✅ (undefined) | ✅ | ✅ |
let |
Block | ✅ (TDZ) | ✅ | ❌ |
const |
Block | ✅ (TDZ) | ❌ | ❌ |
TDZ (Temporal Dead Zone): let and const are hoisted but cannot be accessed before their declaration — accessing them before declaration throws a ReferenceError.
// Hoisting behavior
console.log(a); // undefined (var is hoisted)
var a = 10;
// console.log(b); // ❌ ReferenceError! (let/const in the TDZ)
// let b = 20;
Real-World Examples
1. Variable Scope in Nested Loops for Grade Data Processing
// Student grade data
const dataGrade = [
{ studentId: "2023001", name: "John", grade: [85, 90, 78] },
{ studentId: "2023002", name: "Amy", grade: [92, 88, 95] },
{ studentId: "2023003", name: "Mike", grade: [70, 65, 80] },
];
// ✅ Using let (block scope) — safe in loops
for (let i = 0; i < dataGrade.length; i++) {
const student = dataGrade[i];
let gradeTotal = 0;
for (let j = 0; j < student.grade.length; j++) {
gradeTotal += student.grade[j];
}
const average = gradeTotal / student.grade.length;
console.log(`${student.name} — Average: ${average.toFixed(1)}`);
// John — Average: 84.3
// Amy — Average: 91.7
// Mike — Average: 71.7
}
// ❌ Common bug with var (function scope)
// If you use var in a loop, the variable is shared
function varBugExample() {
const functionArray = [];
for (var i = 0; i < 5; i++) {
functionArray.push(function() {
return i;
});
}
// All functions return 5 (not 0, 1, 2, 3, 4!)
console.log(functionArray[0]()); // 5
console.log(functionArray[2]()); // 5
}
varBugExample();
// ✅ Solution with let
function correctLetExample() {
const functionArray = [];
for (let i = 0; i < 5; i++) {
functionArray.push(function() {
return i;
});
}
// Each function has its own closure over i
console.log(functionArray[0]()); // 0
console.log(functionArray[2]()); // 2
}
correctLetExample();
2. Scope in Nested Functions with Lecturer Data
function createFacultyPortal(lecturerList) {
// Outer scope — lecturer data
const lecturer = lecturerList;
return {
findByMajor(majorCode) {
// Function scope — majorCode parameter
const result = lecturer.filter(d => d.majorCode === majorCode);
return {
count: result.length,
name: result.map(d => d.name),
averageThesis: function() {
// Nested scope — accessing result from the parent scope
const total = result.reduce((acc, d) => acc + d.thesisCount, 0);
return total / result.length;
}
};
}
};
}
const facultyList = [
{ name: "Dr. Aaron", majorCode: "CS", thesisCount: 12 },
{ name: "Dr. Sarah", majorCode: "CS", thesisCount: 8 },
{ name: "Dr. John", majorCode: "IS", thesisCount: 15 },
];
const portal = createFacultyPortal(facultyList);
const lecturerCS = portal.findByMajor("CS");
console.log(`Number of CS lecturers: ${lecturerCS.count}`);
console.log(`Average Thesis: ${lecturerCS.averageThesis()}`);
3. Block Scope for Tiered Input Validation
function validationForm(data) {
const error = [];
// Block scope — each validation has its own errorMessages
{
const errorMessages = [];
if (!data.name) errorMessages.push("Name is required");
if (data.name && data.name.length < 3) errorMessages.push("Name at least 3 characters");
if (errorMessages.length) error.push({ field: "name", errors: errorMessages });
}
{
const errorMessages = [];
if (!data.email) errorMessages.push("Email is required");
if (data.email && !data.email.includes("@")) errorMessages.push("Invalid email format");
if (errorMessages.length) error.push({ field: "email", errors: errorMessages });
}
{
const errorMessages = [];
if (!data.studentId) errorMessages.push("StudentID is required");
if (data.studentId && !/^\d{8}$/.test(data.studentId)) errorMessages.push("StudentID must be 8 digits");
if (errorMessages.length) error.push({ field: "studentId", errors: errorMessages });
}
return {
valid: error.length === 0,
errors: error
};
}
const result = validationForm({ name: "Bo", email: "john", studentId: "123" });
console.log(result);
// { valid: false, errors: [
// { field: "name", errors: ["Name at least 3 characters"] },
// { field: "email", errors: ["Invalid email format"] },
// { field: "studentId", errors: ["StudentID must be 8 digits"] }
// ]}
Conclusion
- Global scope: variables can be accessed from anywhere — avoid overusing it.
- Function scope (
var): can only be accessed inside the function — rarely used nowadays. - Block scope (
let,const): can only be accessed inside the{}block — recommended. - Scope chain: inner functions access outer variables (upward, not sideways).
- Variable shadowing: same-named variables in different scopes — use with care.
- Using
varin loops causes bugs because of function scope. Useletfor loops. - Use block scope
{}to isolate variables in validation or tiered processes.