JavaScript Scope
Where Variables Live
Explanation
What is Scope?
Scope determines where variables are accessible in your code. Understanding scope prevents bugs, helps with memory management, and is essential for closures.
Key Concepts
- Global Scope: Accessible everywhere
- Function Scope: Only inside the function
- Block Scope: Only inside
{}(let/const) - Lexical Scope: Inner functions access outer variables
- Closure: Functions that remember their scope
var vs let vs const Scope
// var - function scoped
function example() {
if (true) {
var x = 1;
}
console.log(x); // 1 (accessible!)
}
// let/const - block scoped
function example2() {
if (true) {
let y = 1;
const z = 2;
}
console.log(y); // ReferenceError
}
Demonstration
Example 1: Scope Types
// Global scope
const globalVar = "I'm global";
function outerFunction() {
// Function scope
const outerVar = "I'm in outer";
if (true) {
// Block scope
const blockVar = "I'm in block";
let blockLet = "Also in block";
var functionVar = "I'm function-scoped despite being in block";
console.log(globalVar); // ✓ Accessible
console.log(outerVar); // ✓ Accessible
console.log(blockVar); // ✓ Accessible
}
console.log(globalVar); // ✓ Accessible
console.log(outerVar); // ✓ Accessible
console.log(functionVar); // ✓ Accessible (var ignores block)
// console.log(blockVar); // ✗ ReferenceError
function innerFunction() {
// Nested function scope
const innerVar = "I'm in inner";
console.log(globalVar); // ✓ Accessible
console.log(outerVar); // ✓ Accessible (lexical scope)
console.log(innerVar); // ✓ Accessible
}
innerFunction();
// console.log(innerVar); // ✗ ReferenceError
}
outerFunction();
Example 2: Closures
// Basic closure
function createCounter() {
let count = 0; // Private variable
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// console.log(count); // ReferenceError (private!)
// Closure with parameters
function createMultiplier(factor) {
return function(number) {
return number * factor; // factor is "remembered"
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Closure in loops (classic gotcha)
// Problem with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 3, 3, 3
}
// Solution 1: Use let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
// Solution 2: IIFE (old pattern)
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => console.log(j), 100);
})(i); // 0, 1, 2
}
Example 3: Hoisting
// var hoisting
console.log(hoistedVar); // undefined (not ReferenceError!)
var hoistedVar = "hello";
console.log(hoistedVar); // "hello"
// What JavaScript sees:
// var hoistedVar;
// console.log(hoistedVar); // undefined
// hoistedVar = "hello";
// let/const - Temporal Dead Zone (TDZ)
// console.log(hoistedLet); // ReferenceError
let hoistedLet = "hello";
// Function hoisting
sayHello(); // Works! "Hello"
function sayHello() {
console.log("Hello");
}
// Function expressions are NOT hoisted
// sayGoodbye(); // TypeError: sayGoodbye is not a function
const sayGoodbye = function() {
console.log("Goodbye");
};
// Class declarations are NOT hoisted
// const obj = new MyClass(); // ReferenceError
class MyClass {}
// Best practice: Declare before use
// Don't rely on hoisting for readability
Example 4: this and Scope
// Global context
console.log(this); // window (browser) or global (Node)
// Object method
const user = {
name: "Arthur",
greet() {
console.log(this.name); // "Arthur"
},
greetArrow: () => {
console.log(this.name); // undefined (arrow inherits outer this)
}
};
user.greet(); // "Arthur"
user.greetArrow(); // undefined
// Lost this context
const greet = user.greet;
greet(); // undefined (this is now global)
// Solutions
// 1. bind
const boundGreet = user.greet.bind(user);
boundGreet(); // "Arthur"
// 2. Arrow function wrapper
const wrappedGreet = () => user.greet();
wrappedGreet(); // "Arthur"
// 3. call/apply
greet.call(user); // "Arthur"
greet.apply(user); // "Arthur"
// Nested functions
const obj = {
name: "Arthur",
processItems() {
const items = [1, 2, 3];
// Problem: this is undefined in callback
// items.forEach(function(item) {
// console.log(this.name); // undefined
// });
// Solution 1: Arrow function
items.forEach((item) => {
console.log(this.name); // "Arthur"
});
// Solution 2: Save reference
const self = this;
items.forEach(function(item) {
console.log(self.name); // "Arthur"
});
// Solution 3: bind
items.forEach(function(item) {
console.log(this.name);
}.bind(this));
}
};
Example 5: Module Scope
// ES Modules - file scope
// utils.js
const privateHelper = () => "private"; // Not exported
export const publicHelper = () => "public";
// main.js
import { publicHelper } from './utils.js';
// privateHelper is not accessible
// Module pattern (before ES modules)
const Module = (function() {
// Private
const privateVar = "secret";
const privateMethod = () => console.log(privateVar);
// Public API
return {
publicMethod() {
privateMethod();
},
publicVar: "visible"
};
})();
Module.publicMethod(); // Works
// Module.privateVar; // undefined
// Module.privateMethod; // undefined
// Revealing module pattern
const Calculator = (function() {
let result = 0;
const add = (n) => result += n;
const subtract = (n) => result -= n;
const getResult = () => result;
const reset = () => result = 0;
return { add, subtract, getResult, reset };
})();
Key Takeaways:
- Use
letandconstfor predictable block scope - Closures "remember" their lexical environment
- Avoid
varto prevent hoisting confusion - Arrow functions inherit
thisfrom enclosing scope - Module scope provides true privacy
Imitation
Challenge 1: Create a Private Counter
Task: Create a counter with private state using closures.
Solution
function createSecureCounter(initial = 0) {
let count = initial;
const history = [];
return {
increment(amount = 1) {
count += amount;
history.push({ action: 'increment', amount, result: count });
return count;
},
decrement(amount = 1) {
count -= amount;
history.push({ action: 'decrement', amount, result: count });
return count;
},
getCount() {
return count;
},
getHistory() {
return [...history]; // Return copy
},
reset() {
count = initial;
history.push({ action: 'reset', result: count });
return count;
}
};
}
const counter = createSecureCounter(10);
counter.increment(5);
counter.decrement(3);
console.log(counter.getCount()); // 12
console.log(counter.getHistory()); // Shows all operations
Challenge 2: Implement Memoization
Task: Create a memoize function that caches results.
Solution
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit!');
return cache.get(key);
}
console.log('Computing...');
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Usage
const expensiveOperation = (n) => {
// Simulate expensive computation
let result = 0;
for (let i = 0; i < n * 1000000; i++) {
result += i;
}
return result;
};
const memoized = memoize(expensiveOperation);
console.log(memoized(10)); // Computing... (slow)
console.log(memoized(10)); // Cache hit! (instant)
console.log(memoized(20)); // Computing... (slow)
console.log(memoized(20)); // Cache hit! (instant)
Practice
Exercise 1: Event Handler Factory
Difficulty: Intermediate
Create a function that generates event handlers with preset configurations:
const clickHandler = createHandler('click', { log: true });
button.addEventListener('click', clickHandler);
Exercise 2: Module with State
Difficulty: Advanced
Build a state management module with:
- Private state
- Subscribe/unsubscribe to changes
- Undo/redo functionality
Summary
What you learned:
- Different scope types (global, function, block)
- How closures capture variables
- Hoisting behavior with var/let/const
thisbinding and context- Module patterns for encapsulation
Next Steps:
- Read: JavaScript Hoisting
- Practice: Build a module system
- Deep dive: Event loop and execution context
