050: Functions II: Scope, Closures, Recursion, and the Call Stack
Outcomes
By the end of this lesson, you can:
- create function expressions and arrow function expressions;
- choose readable syntax based on context rather than novelty;
- explain global/module, function, and block scope;
- identify local variables and shadowing;
- pass a function as a callback to an array method; and
- refactor prior functions without changing behavior.
Prerequisites and Retrieval
Retrieve function declarations from 049.
- What is the difference between a parameter and an argument?
- Why should a calculator return rather than only log?
- What does a guard return accomplish?
Today adds ways to create function values. It does not replace function declarations, and it avoids deep closure theory.
Terms
- function expression: Function defined inside an expression, often stored in a variable. — Source: MDN: function expression
- arrow function: Compact => syntax without own this, arguments, or prototype. — Source: MDN: Arrow functions
- anonymous function: Function without a name, used inline as a value. — Source: MDN: function expression
- first-class value: Functions are values assignable, passable, returnable like any data. — Source: MDN: First-class functions
- callback: “A function passed into another function as an argument, which is then invoked inside.” — Source: MDN: Callback function
- local variable: Binding visible only within its function/block scope. — Source: MDN: Glossary — Scope
- global scope: Bindings accessible everywhere in the script/module graph. — Source: MDN: Scope
- module scope: Top-level bindings of a module, private unless exported. — Source: MDN: JavaScript modules
- lexical scope: Scoping determined by where code is written, enabling closures. — Source: MDN: Closures
- implicit return: Arrow bodies without braces return their expression automatically. — Source: MDN: Arrow functions
- Closure (official): "A closure is the combination of a function bundled together with references to its surrounding state (the lexical environment)." — Source: MDN: Closure
- Callback (official): "A callback function is a function passed into another function as an argument, which is then invoked inside the outer function." — Source: MDN: Callback function
- Lexical scope (official): "Lexical scope is the scope defined by the position of declarations in source code." — Source: MDN: Scope
Beginner Explanation and Mental Model
In 049 a function declaration gave reusable code a name:
function double(number) {
return number * 2;
}
A function is also a value. A function expression can be assigned to a constant:
const double = function (number) {
return number * 2;
};
An arrow function is another expression form:
const double = (number) => {
return number * 2;
};
For one expression, braces and return can be omitted:
const double = (number) => number * 2;
These examples return the same results, but their language details are not identical. Function declarations are excellent for named domain operations and can be called earlier in their scope due to declaration hoisting. Functions assigned to const cannot be accessed before that declaration is initialized. Arrow functions also do not have their own this or arguments; that matters for object methods and advanced patterns later. Do not automatically convert every function to an arrow.
Use parentheses around parameters consistently in course code, even though one arrow parameter can omit them. Explicit parentheses make later edits easier.
Scope
Scope is name visibility. A function creates a local scope:
const taxRate = 0.10;
function calculateTax(price) {
const tax = price * taxRate;
return tax;
}
console.log(calculateTax(500)); // 50
// console.log(tax); // ReferenceError
The function can read its parameter, its local tax, and the outer taxRate. Outer code cannot read local tax. Prefer passing important dependencies as parameters (calculateTax(price, taxRate)) because explicit inputs make testing easier.
const and let also have block scope. When an inner declaration repeats an outer name, it shadows that name in the inner scope. Shadowing is valid but can confuse readers, so choose distinct descriptive names.
Callbacks
Because functions are values, one function can receive another. The receiving code decides when and with which arguments to invoke it. Array forEach() calls its callback once for each existing array element:
const names = ["Asha", "Ravi"];
names.forEach((name) => {
console.log(name);
});
The arrow is not called immediately by your source line. It is passed to forEach, which calls it with each element. More transforming array methods arrive in 053; today focuses on the callback concept.
Worked Example: Refactor a Calculator
Start with a declaration and then equivalent expressions:
function addDeclaration(a, b) {
return a + b;
}
const subtractExpression = function (a, b) {
return a - b;
};
const multiplyArrow = (a, b) => a * b;
const divideArrow = (a, b) => {
if (b === 0) {
return "Cannot divide by zero";
}
return a / b;
};
console.log(addDeclaration(8, 2));
console.log(subtractExpression(8, 2));
console.log(multiplyArrow(8, 2));
console.log(divideArrow(8, 2));
console.log(divideArrow(8, 0));
Expected output:
10 6 16 4 Cannot divide by zero
The one-expression multiplication uses an implicit return. The division function needs multiple statements and a guard, so braces and explicit returns are appropriate. A frequent bug is writing (a, b) => { a * b; }; braces create a block, and without return its result is undefined.
Intermediate Example: Scope and Callbacks
const courseName = "JavaScript";
const formatScore = (userName, score) => {
const passed = score >= 60;
const resultLabel = passed ? "passed" : "needs practice";
return `${userName}: ${score} (${resultLabel})`;
};
const scores = [72, 48, 91];
scores.forEach((score, index) => {
const displayNumber = index + 1;
const message = formatScore(`User ${displayNumber}`, score);
console.log(courseName, message);
});
Expected output:
JavaScript User 1: 72 (passed) JavaScript User 2: 48 (needs practice) JavaScript User 3: 91 (passed)
formatScore has local passed and resultLabel. The callback has its own parameter bindings and local displayNumber and message. Both scopes can read outer courseName. Neither outer scope can read those locals after calls finish. The callback receives element and index arguments from forEach.
A named callback can be reused:
const logScore = (score) => {
console.log("Score:", score);
};
scores.forEach(logScore);
Pass logScore, not logScore(). Parentheses would invoke it immediately and pass its undefined result.
Optional Advanced Extension: Lexical Capture
An inner function can read bindings from where it was defined:
const prefix = "Result";
const printValue = (value) => {
console.log(prefix, value);
};
[10, 20].forEach(printValue);
This lexical access is the foundation of closures, but no deeper theory is needed now. Keep dependencies explicit through parameters when they are important to the function's meaning; outer configuration can be reasonable when genuinely shared.
Interview focus: closures, this, and explicit binding
A closure keeps access to lexical bindings after the outer function has returned. That makes closures useful for private state, factories, memoization, and event handlers. The binding is not a snapshot automatically; a closure reads the binding's current value when it runs.
function makeCounter() {
let count = 0;
return () => ++count;
}
const nextCount = makeCounter();
console.log(nextCount(), nextCount()); // 1 2
An interview follow-up is the loop-closure trace. let creates a per-iteration
binding, while one var binding is shared by all callbacks:
const callbacks = [];
for (let index = 0; index < 3; index += 1) {
callbacks.push(() => index);
}
console.log(callbacks.map((callback) => callback())); // [0, 1, 2]
In UI code, a stale closure is a callback that retained an older render's value.
Use the dependency model of the framework, pass the current value when scheduling
the work, or read intentionally mutable state through a documented ref/store.
The fix is not to use var or to hide the dependency in a global.
this is determined by the call site for a regular function:
const account = {
name: "Maya",
label() { return this.name; },
};
console.log(account.label()); // Maya
const detached = account.label;
console.log(detached()); // undefined in strict/module code
An arrow has no own this; it captures this from the surrounding lexical scope.
Use a regular method when the receiver supplies the context, and use an arrow when
lexical context is what you want.
call, apply, and bind make that context choice explicit. call takes
individual arguments, apply takes an argument array, and bind returns a new
function with a permanently preselected context and optionally prefilled
arguments.
function introduce(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
console.log(introduce.call({ name: "Ravi" }, "Hi", "!"));
console.log(introduce.apply({ name: "Ravi" }, ["Hello", "."]));
const greetMaya = introduce.bind({ name: "Maya" }, "Welcome");
console.log(greetMaya("!"));
Follow-ups: What happens if a bound function is used with call? Its bound
this wins. What does an arrow do with call? It ignores the supplied this.
Why bind a callback? To preserve its receiver when passing a method to another
API. Prefer an arrow wrapper or bind deliberately, since each creates a new
function identity.
Deep Dive: Lexical Environments, Closures, Recursion, and Stack Frames
A closure is not simply "a function inside a function." It is a function plus access to the lexical environment where it was created.
function createCounter(start = 0) {
let count = start;
return function increment() {
count += 1;
return count;
};
}
const next = createCounter(10);
console.log(next()); // 11
console.log(next()); // 12
The outer call has completed, but count remains reachable through the returned function.
Useful closure: configuration
function createTaxCalculator(rate) {
return (subtotal) => subtotal * rate;
}
const calculateGST = createTaxCalculator(0.18);
console.log(calculateGST(1000));
Recursion
A recursive function needs both a recursive step and a base case.
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
For deeply nested or large input, iteration may be safer because JavaScript engines have finite call stacks.
Call stack reasoning
function first() {
second();
}
function second() {
third();
}
function third() {
console.trace("stack");
}
first();
When debugging, read stack traces from the error site outward to understand the chain of calls that led there.
Mistakes and Debugging
- Calling an expression before declaration: move the call below
const functionName = .... - Missing arrow return: with braces, write
return; without braces, provide exactly one returned expression. - Returning an object literal implicitly:
(value) => ({ value })needs parentheses so braces are parsed as an object expression. Objects are covered later. - Invoking a callback too early: pass
handleValue, nothandleValue(). - Assuming callback parameters have chosen values: the array method supplies element, index, and array in a documented order.
- Reading a local outside scope: return the value or use it inside its scope.
- Shadowing outer names: rename the inner binding to reveal meaning.
- Using an arrow as an object method/constructor: arrows lack their own
thisand cannot be constructors; use appropriate regular function/method syntax later. - Refactoring syntax and behavior together: first preserve inputs, output, and edge cases; then verify calls.
Best Practices
- Prefer declarations for prominent named domain functions; use arrow functions naturally for short callbacks and local expressions.
- Store function expressions in
constunless the function binding must change. - Use implicit returns only when the expression remains immediately readable.
- Keep local variables in the smallest useful scope.
- Pass important dependencies as parameters rather than reading mutable globals.
- Name reusable callbacks; inline tiny one-use callbacks.
- Return data from transformations and keep effects such as logging explicit.
- Do not refactor to arrow syntax solely to reduce line count.
Tiered Exercises
Core
Rewrite declaration functions square(number) and isAdult(age) as arrow functions. Call each twice and verify the same outputs.
Practice
Create an array of three prices. Write a named logPrice callback that prints each price and its one-based position using forEach. Calculate tax in a separate arrow function and include it in the output.
Professional Extension
Create makeLabel(prefix) that defines and returns an inner arrow function accepting a value and returning a formatted string. Use the returned function twice. Explain which outer value the inner function reads.
Complete Solutions
const square = (number) => number * number;
const isAdult = (age) => age >= 18;
console.log(square(4)); // 16
console.log(square(7)); // 49
console.log(isAdult(18)); // true
console.log(isAdult(15)); // false
const prices = [100, 250, 80];
const calculateTax = (price) => price * 0.10;
const logPrice = (price, index) => {
const position = index + 1;
const tax = calculateTax(price);
console.log(`Item ${position}: price ${price}, tax ${tax}`);
};
prices.forEach(logPrice);
Expected output:
Item 1: price 100, tax 10 Item 2: price 250, tax 25 Item 3: price 80, tax 8
const makeLabel = (prefix) => {
const formatValue = (value) => `${prefix}: ${value}`;
return formatValue;
};
const scoreLabel = makeLabel("Score");
console.log(scoreLabel(80));
console.log(scoreLabel(95));
The returned formatValue function reads the prefix parameter from the scope where it was created. Output is Score: 80 and Score: 95.
Recap and Exit Questions
Functions are first-class values. Expressions and arrows can be stored in constants and passed as callbacks. Scope determines which names code can read, and callbacks are invoked by the receiving operation with documented arguments.
- How does a function declaration differ from an expression assigned to
const? - When does an arrow need explicit
return? - What is a local variable?
- Why pass
callbackrather thancallback()? - What arguments does
forEachsupply to its callback?
Official References
- MDN: Functions guide
- MDN: function expressions
- MDN: arrow functions
- MDN: scope glossary
- MDN:
Array.prototype.forEach() - ECMA-262: arrow function definitions
- MDN:
Function.prototype.call() - MDN:
Function.prototype.apply() - MDN:
Function.prototype.bind()
References checked 2026-08-24.
Closures, lexical scope, and this
A closure is a function together with the lexical environment it can still access. Use one to preserve private state, then explain what is retained and when it can be released. Compare a method, a regular callback, and an arrow function.
A regular function this depends on how it is called; an arrow function captures this lexically and has no own arguments or prototype. Test call, apply, bind, detached methods, object methods, constructors, and event callbacks. Interview answers should explain call-site binding rather than claiming that arrow functions are always better.
