Skip to content

Scope - Exercises

What is the difference between var, let, and const in terms of scope? A) No difference in scope B) var is function-scoped, let and const are block-scoped C) All are block-scoped D) All are function-scoped

Answer: B Explanation: var is function-scoped (or globally scoped), while let and const are block-scoped.

What will this code output?

function test() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // ?
console.log(b); // ?
}

A) 1, 2 B) 1, undefined C) 1, ReferenceError D) undefined, ReferenceError

Answer: C Explanation: var a is function-scoped so it’s accessible, but let b is block-scoped and throws a ReferenceError when accessed outside its block.

What is a closure in JavaScript? A) A function that’s closed to modifications B) A function that has access to variables in its outer scope C) A function that can’t be called D) A private function

Answer: B Explanation: A closure is a function that has access to variables from its outer (enclosing) scope even after the outer function has finished executing.

What will this code output?

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}

A) 0, 1, 2 B) 3, 3, 3 C) 0, 0, 0 D) Error

Answer: B Explanation: Due to var being function-scoped and the asynchronous nature of setTimeout, all callbacks reference the same i which has value 3 after the loop completes.

How can you fix the previous code to print 0, 1, 2? A) Use let instead of var B) Use an IIFE (Immediately Invoked Function Expression) C) Use const instead of var D) Both A and B

Answer: D Explanation: Both using let (block scope) and IIFE (creating new scope) would capture the correct value of i.

What is the global scope in a browser environment? A) document B) window C) global D) this

Answer: B Explanation: In browser environments, the global scope is the window object.

Practice understanding the difference between function scope and block scope:

function demonstrateScope() {
// TODO: Demonstrate var (function scope)
if (true) {
var functionScoped = "I'm function scoped";
}
// TODO: Try to console.log functionScoped here - it should work
// TODO: Demonstrate let (block scope)
if (true) {
let blockScoped = "I'm block scoped";
// TODO: console.log blockScoped here - it should work
}
// TODO: Try to console.log blockScoped here - it should cause an error
// (Comment out the line to avoid the error)
// TODO: Add a for loop example with var
// TODO: Show that the var loop variable is accessible after the loop
// TODO: Add a for loop example with let
// TODO: Show that the let loop variable is NOT accessible after the loop
}
// TODO: Call your function to test it
// demonstrateScope();

Expected Output:

  • var variables should be accessible outside their block
  • let variables should only be accessible within their block
  • Loop variables behave differently with var vs let

Solution:

function demonstrateScope() {
console.log("=== Function vs Block Scope ===");
// Var example - function scoped
if (true) {
var functionScoped = "I'm function scoped";
}
console.log("var outside block:", functionScoped); // Works
// Let example - block scoped
if (true) {
let blockScoped = "I'm block scoped";
console.log("let inside block:", blockScoped); // Works
}
// console.log(blockScoped); // ReferenceError
// Loop example
for (var i = 0; i < 3; i++) {
// var i is function scoped
}
console.log("var after loop:", i); // 3
for (let j = 0; j < 3; j++) {
// let j is block scoped
}
// console.log(j); // ReferenceError
// Const example
if (true) {
const blockConstant = "I'm a block-scoped constant";
console.log("const inside block:", blockConstant);
}
// console.log(blockConstant); // ReferenceError
}

Create a function that demonstrates closure by returning an inner function:

// TODO: Create a function called createCounter that uses closure
function createCounter() {
// TODO: Create a private counter variable starting at 0
// TODO: Return a function that increments and returns the counter
}
// TODO: Create a function that asks the user how many times to count
function interactiveCounterTest() {
// TODO: Prompt the user: "How many times would you like to count?"
const times = parseInt(prompt("How many times would you like to count?"));
// TODO: Create a counter using your createCounter function
const counter = createCounter();
// TODO: Use a loop to call the counter the specified number of times
// TODO: Console.log each count with a message like "Count: 1", "Count: 2", etc.
// TODO: Create a second counter to show they are independent
const counter2 = createCounter();
console.log("Second counter starts fresh:", counter2()); // Should be 1
}
// TODO: Call your interactive function to test it
// interactiveCounterTest();

Expected Behavior:

  • User inputs a number (e.g., 5)
  • Counter counts from 1 to that number
  • Second counter shows it starts independently at 1

Solution:

function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}

Use closures to create a simple banking module with user interaction:

// TODO: Create a function called createBankAccount that takes initialBalance
function createBankAccount(initialBalance) {
// TODO: Create a private balance variable
// TODO: Return an object with methods: deposit, withdraw, getBalance
}
// TODO: Create an interactive banking simulation
function bankingSimulation() {
// TODO: Prompt user for initial balance
const initialAmount = parseFloat(prompt("Enter your initial balance: $"));
// TODO: Create a bank account with the initial balance
const account = createBankAccount(initialAmount);
// TODO: Create a loop that continues until user chooses to quit
let continueTransaction = true;
while (continueTransaction) {
// TODO: Show current balance
console.log(`Current Balance: $${account.getBalance()}`);
// TODO: Prompt user for action: "1-Deposit, 2-Withdraw, 3-Check Balance, 4-Quit"
const action = prompt(
"Choose an action:\n1 - Deposit\n2 - Withdraw\n3 - Check Balance\n4 - Quit",
);
// TODO: Use if/else or switch to handle each action
if (action === "1") {
// TODO: Prompt for deposit amount and call deposit method
const depositAmount = parseFloat(prompt("Enter deposit amount: $"));
console.log(account.deposit(depositAmount));
} else if (action === "2") {
// TODO: Prompt for withdrawal amount and call withdraw method
const withdrawAmount = parseFloat(prompt("Enter withdrawal amount: $"));
console.log(account.withdraw(withdrawAmount));
} else if (action === "3") {
// TODO: Display current balance
console.log(`Your balance is: $${account.getBalance()}`);
} else if (action === "4") {
// TODO: Set continueTransaction to false and show goodbye message
continueTransaction = false;
console.log("Thank you for banking with us!");
} else {
console.log("Invalid choice. Please try again.");
}
}
}
// TODO: Call your banking simulation to test it
// bankingSimulation();

Expected Features:

  • User sets initial balance
  • Interactive menu for deposit/withdraw/check balance
  • Private balance that can’t be accessed directly
  • Input validation and error messages

Solution:

function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit: function (amount) {
if (amount > 0) {
balance += amount;
return `Deposited $${amount}. New balance: $${balance}`;
}
return "Invalid deposit amount";
},
withdraw: function (amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return `Withdrew $${amount}. New balance: $${balance}`;
}
return "Invalid withdrawal amount or insufficient funds";
},
getBalance: function () {
return balance;
},
};
}

Fix this code so it prints 0, 1, 2 instead of 3, 3, 3.

// Problematic code
function problematicLoop() {
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log("Problem:", i);
}, 100);
}
}
// Fix using let
function fixedWithLet() {
// Your solution here
}
// Fix using IIFE
function fixedWithIIFE() {
// Your solution here
}
// Fix using bind
function fixedWithBind() {
// Your solution here
}

Solutions:

// Fix using let
function fixedWithLet() {
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log("Let solution:", i);
}, 100);
}
}
// Fix using IIFE
function fixedWithIIFE() {
for (var i = 0; i < 3; i++) {
(function (index) {
setTimeout(function () {
console.log("IIFE solution:", index);
}, 100);
})(i);
}
}
// Fix using bind
function fixedWithBind() {
for (var i = 0; i < 3; i++) {
setTimeout(
function (index) {
console.log("Bind solution:", index);
}.bind(null, i),
100,
);
}
}

Create nested functions to demonstrate the scope chain.

function outerFunction(x) {
// Create variables and inner functions to demonstrate scope chain
function middleFunction(y) {
// This should access x from outer scope
function innerFunction(z) {
// This should access x, y, and z
// Return a message using all three variables
}
return innerFunction;
}
return middleFunction;
}
// Test the scope chain
const test = outerFunction(1)(2)(3);
console.log(test); // Should use all three values

Solution:

function outerFunction(x) {
console.log("Outer function, x =", x);
function middleFunction(y) {
console.log("Middle function, x =", x, "y =", y);
function innerFunction(z) {
console.log("Inner function, x =", x, "y =", y, "z =", z);
return `Sum of x(${x}) + y(${y}) + z(${z}) = ${x + y + z}`;
}
return innerFunction;
}
return middleFunction;
}
// Test function for scope exercises
function testScopeExercises() {
console.log("=== Testing Scope Exercises ===");
// Test createCounter
const counter = createCounter();
const test1 = counter() === 1 && counter() === 2;
console.log("createCounter test:", test1 ? "PASS" : "FAIL");
// Test createBankAccount
const account = createBankAccount(100);
account.deposit(50);
const test2 = account.getBalance() === 150;
console.log("createBankAccount test:", test2 ? "PASS" : "FAIL");
// Test scope chain
const scopeTest = outerFunction(1)(2)(3);
const test3 = scopeTest.includes("6"); // Sum should be 6
console.log("scope chain test:", test3 ? "PASS" : "FAIL");
// Test independent counters
const counter1 = createCounter();
const counter2 = createCounter();
counter1();
counter1();
const test4 = counter1() === 3 && counter2() === 1;
console.log("independent closures test:", test4 ? "PASS" : "FAIL");
}
// Run tests
testScopeExercises();

Challenge 1: Function Factory with Configuration

Section titled “Challenge 1: Function Factory with Configuration”

Create a function factory that produces configured functions.

function createValidator(config) {
// config = { type: 'email'|'phone'|'password', options: {...} }
// Return a validator function based on the config
const validators = {
email: (value, options) => {
// Basic email validation
return value.includes("@") && value.includes(".");
},
phone: (value, options) => {
// Phone number validation
const cleaned = value.replace(/\D/g, "");
return cleaned.length === 10;
},
password: (value, options) => {
// Password validation with configurable rules
const minLength = options.minLength || 8;
const requireSpecial = options.requireSpecial || false;
if (value.length < minLength) return false;
if (requireSpecial && !/[!@#$%^&*]/.test(value)) return false;
return true;
},
};
return function (value) {
// Your implementation here
};
}
// Test your factory
const emailValidator = createValidator({ type: "email" });
const passwordValidator = createValidator({
type: "password",
options: { minLength: 10, requireSpecial: true },
});
console.log(emailValidator("test@email.com")); // true
console.log(passwordValidator("short")); // false
console.log(passwordValidator("longenough!@")); // true

Solution:

function createValidator(config) {
const validators = {
email: (value, options) => {
return value.includes("@") && value.includes(".");
},
phone: (value, options) => {
const cleaned = value.replace(/\D/g, "");
return cleaned.length === 10;
},
password: (value, options) => {
const minLength = options.minLength || 8;
const requireSpecial = options.requireSpecial || false;
if (value.length < minLength) return false;
if (requireSpecial && !/[!@#$%^&*]/.test(value)) return false;
return true;
},
};
const validator = validators[config.type];
const options = config.options || {};
return function (value) {
if (!validator) {
throw new Error(`Unknown validator type: ${config.type}`);
}
return validator(value, options);
};
}

Create a memoization function that caches expensive function calls.

function memoize(fn) {
// Create a cache using closure
// Return a function that checks cache before calling fn
}
// Test function (expensive operation)
function fibonacci(n) {
console.log(`Calculating fibonacci(${n})`);
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(10)); // Should cache intermediate results
console.log(memoizedFib(10)); // Should use cached result

Solution:

function memoize(fn) {
const cache = {};
return function (...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log("Cache hit for", key);
return cache[key];
}
console.log("Cache miss for", key);
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}

Create a system for managing private variables in objects.

function createPrivateVars() {
// Create a system where objects can have truly private variables
// that can only be accessed through specific methods
const privateStore = new WeakMap();
return {
create: function (publicMethods) {
// Create object with private variables
},
get: function (obj, key) {
// Get private variable
},
set: function (obj, key, value) {
// Set private variable
},
};
}
// Usage example:
const privateVars = createPrivateVars();
const obj = privateVars.create({
getName: function () {
return privateVars.get(this, "name");
},
setName: function (name) {
privateVars.set(this, "name", name);
},
});
obj.setName("John");
console.log(obj.getName()); // 'John'
console.log(obj.name); // undefined (truly private)

Solution:

function createPrivateVars() {
const privateStore = new WeakMap();
return {
create: function (publicMethods) {
const obj = {};
privateStore.set(obj, {});
// Copy public methods to the object
Object.assign(obj, publicMethods);
return obj;
},
get: function (obj, key) {
const privateData = privateStore.get(obj);
return privateData ? privateData[key] : undefined;
},
set: function (obj, key, value) {
const privateData = privateStore.get(obj);
if (privateData) {
privateData[key] = value;
}
},
};
}

Implement debounce and throttle functions using closures.

function debounce(func, delay) {
// Implement debounce: delay execution until after delay ms have passed
// since the last time it was invoked
}
function throttle(func, limit) {
// Implement throttle: limit execution to once per limit ms
}
// Test functions
const debouncedLog = debounce((msg) => console.log("Debounced:", msg), 300);
const throttledLog = throttle((msg) => console.log("Throttled:", msg), 300);
// Simulate rapid calls
for (let i = 0; i < 5; i++) {
setTimeout(() => debouncedLog(`Message ${i}`), i * 50);
}

Solution:

function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}

Find and fix the scope issue:

var buttons = document.querySelectorAll("button");
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function () {
console.log("Button", i, "clicked"); // Always logs the same i
});
}

Issue: var i is function-scoped, so all event listeners reference the same variable. Fix:

const buttons = document.querySelectorAll("button");
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function () {
console.log("Button", i, "clicked");
});
}
// Alternative fix with IIFE:
for (var i = 0; i < buttons.length; i++) {
(function (index) {
buttons[index].addEventListener("click", function () {
console.log("Button", index, "clicked");
});
})(i);
}

Fix the closure memory leak:

function createHandler() {
const largeData = new Array(1000000).fill("data");
return {
process: function () {
// Only uses a small part of largeData
console.log(largeData[0]);
},
};
}
// This keeps largeData in memory even though we only need the first element

Fix:

function createHandler() {
const largeData = new Array(1000000).fill("data");
const firstElement = largeData[0]; // Extract only what we need
return {
process: function () {
console.log(firstElement);
},
};
}

Fix the this binding issue in closures:

const obj = {
name: "MyObject",
method: function () {
setTimeout(function () {
console.log(this.name); // undefined - wrong this context
}, 100);
},
};

Fix:

const obj = {
name: "MyObject",
method: function () {
// Solution 1: Store this reference
const self = this;
setTimeout(function () {
console.log(self.name);
}, 100);
// Solution 2: Use arrow function
setTimeout(() => {
console.log(this.name);
}, 100);
// Solution 3: Use bind
setTimeout(
function () {
console.log(this.name);
}.bind(this),
100,
);
},
};

Create a configuration manager with private settings.

function createConfigManager(defaultConfig) {
let config = { ...defaultConfig };
const listeners = [];
return {
get: function (key) {
return config[key];
},
set: function (key, value) {
const oldValue = config[key];
config[key] = value;
// Notify listeners
listeners.forEach((listener) => {
listener(key, value, oldValue);
});
},
subscribe: function (listener) {
listeners.push(listener);
// Return unsubscribe function
return function () {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
},
reset: function () {
config = { ...defaultConfig };
},
};
}
// Usage
const config = createConfigManager({ theme: "light", lang: "en" });
const unsubscribe = config.subscribe((key, newVal, oldVal) => {
console.log(`Config changed: ${key} from ${oldVal} to ${newVal}`);
});
config.set("theme", "dark"); // Triggers listener

Application 2: Event Emitter with Scope Control

Section titled “Application 2: Event Emitter with Scope Control”

Create an event emitter that uses closures for namespace isolation.

function createEventEmitter() {
const events = {};
return {
on: function (event, listener) {
if (!events[event]) {
events[event] = [];
}
events[event].push(listener);
},
off: function (event, listenerToRemove) {
if (!events[event]) return;
events[event] = events[event].filter(
(listener) => listener !== listenerToRemove,
);
},
emit: function (event, ...args) {
if (!events[event]) return;
events[event].forEach((listener) => {
listener(...args);
});
},
once: function (event, listener) {
const onceWrapper = (...args) => {
listener(...args);
this.off(event, onceWrapper);
};
this.on(event, onceWrapper);
},
};
}

Implement a rate limiter using closures.

function createRateLimiter(maxRequests, timeWindow) {
const requests = [];
return function (requestHandler) {
const now = Date.now();
// Remove old requests outside the time window
while (requests.length > 0 && requests[0] <= now - timeWindow) {
requests.shift();
}
// Check if we've exceeded the limit
if (requests.length >= maxRequests) {
throw new Error("Rate limit exceeded");
}
// Add current request timestamp
requests.push(now);
// Execute the request
return requestHandler();
};
}
// Usage
const limitedFetch = createRateLimiter(5, 1000); // 5 requests per second
try {
limitedFetch(() => console.log("API call made"));
} catch (error) {
console.log(error.message);
}
  • I understand the difference between function scope and block scope
  • I know when to use var, let, and const
  • I understand what closures are and how they work
  • I can identify and fix scope-related bugs
  • I understand the scope chain and variable lookup
  • I can use closures to create private variables
  • I can implement the module pattern using closures
  • I understand how this behaves in different scopes
  • I can debug common scope and closure issues
  • I can apply scope concepts to solve real-world problems
  1. Practice with nested function examples
  2. Implement various design patterns using closures
  3. Debug scope-related issues in existing code
  4. Create utility functions that leverage closures
  5. Build modules with private and public interfaces
  6. Practice with the temporal dead zone and hoisting
  7. Experiment with different ways to bind this

Next Topic: 08 - Arrays and Objects