- Explain lexical scope, the scope chain, block scope and hoisting
- Use closures, an IIFE and the module pattern to keep private state
- Predict what
thisis in a call and fix it withbind,call,applyor an arrow function
A website has a “Like” button that must remember how many times it was pressed. If you keep the counter in a global variable, any other code can break it by accident. So how can a function “remember” its own variable after it has finished? The answer is a closure — one of the most powerful ideas in JavaScript and one of the most common interview topics.
Lexical scope and the scope chain
Scope is the part of the code where a variable is visible. JavaScript uses lexical scope: which variables a function can see depends on where it is written, not where it is called. Every function and every { } block with let/const creates a new scope. When looking up a variable, JavaScript checks the current scope, then the outer one, all the way to the global scope — this is the scope chain. The search only goes from the inside out, never the other way.
const city = 'Baku';
function outer() {
const district = 'Nasimi';
function inner() {
const street = 'Nizami';
console.log(street, district, city);
}
inner();
console.log(typeof street);
}
outer();
if (true) {
let blockVar = 'inside';
var functionVar = 'var ignores blocks';
}
console.log(typeof blockVar, functionVar);▸ Expected output
Nizami Nasimi Baku undefined undefined var ignores blocks
inner sees all three scopes, while outer cannot see street. var “escapes” the block, while let stays inside it.Before running the code, the engine registers all declarations — this is called hoisting. A variable declared with var is “lifted” to the top of its scope with the value undefined. A function declaration is hoisted completely, so you can call it before the line where it is written. let and const are hoisted too, but until their line they stay in the temporal dead zone (TDZ): touching them early throws a ReferenceError.
console.log(early);
var early = 'var is hoisted';
try {
console.log(later);
} catch (error) {
console.log(error.name);
}
let later = 'let waits in the TDZ';
console.log(sayHi());
function sayHi() {
return 'Function declarations are hoisted';
}▸ Expected output
undefined ReferenceError Function declarations are hoisted
Closures
A function together with the variables of the scope where it was created. The inner function keeps access to those variables even after the outer function has finished.
Normally a function's local variables disappear when it ends. But if an inner function that uses them has been returned or stored somewhere, the engine keeps them alive. Each call of the outer function creates a new, separate set of variables — below, likes and views never get in each other's way.
function createCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
}
const likes = createCounter();
const views = createCounter(100);
likes.increment();
likes.increment();
views.increment();
console.log(likes.value(), views.value());
console.log(likes.count);▸ Expected output
2 101 undefined
count is not a property of the object — it lives only in the closure, so nobody can change it from outside.Another common use of closures is function factories: you create new functions that “remember” a parameter.
function makeMultiplier(factor) {
return (x) => x * factor;
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(5), triple(5));
console.log([1, 2, 3].map(triple));▸ Expected output
10 15 [ 3, 6, 9 ]
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log('var', i), 0);
}
for (let j = 1; j <= 3; j++) {
setTimeout(() => console.log('let', j), 0);
}▸ Expected output
var 4 var 4 var 4 let 1 let 2 let 3
IIFE and the module pattern
Before ES modules, developers hid variables with an IIFE — an Immediately Invoked Function Expression: a function wrapped in parentheses and called right away. Its variables do not “leak” into the global scope. Combine an IIFE with a closure and you get the module pattern: private data plus a public object (API) with only the methods you choose. Today ES modules do this job at the file level, but the pattern is still common in older code and useful inside a single file.
const bank = (function () {
let balance = 0;
const history = [];
return {
deposit(amount) {
balance += amount;
history.push(`+${amount}`);
},
withdraw(amount) {
if (amount > balance) return 'Not enough money';
balance -= amount;
history.push(`-${amount}`);
return 'OK';
},
report: () => ({ balance, history: [...history] }),
};
})();
bank.deposit(100);
console.log(bank.withdraw(30), bank.withdraw(500));
console.log(bank.report());
console.log(typeof balance);▸ Expected output
OK Not enough money
{ balance: 70, history: [ '+100', '-30' ] }
undefinedbalance and history change only through deposit, withdraw and report. report returns a copy of the array so that nobody can damage the history from outside.this, bind, call and apply
Unlike ordinary variables, this is not lexical: in a regular function its value depends on how the function is called and is decided at call time. Arrow functions have no this of their own — they take it from the surrounding code.
| Call | What this is |
|---|---|
obj.method() | the object left of the dot — obj |
fn() | undefined in strict mode (classes, modules), the global object in old sloppy code |
new Fn() | the newly created object |
fn.call(obj, a, b), fn.apply(obj, [a, b]) | obj — the function is called at once |
fn.bind(obj) | a new function whose this is always obj |
| arrow function | the this of the surrounding code |
function introduce(greeting, punctuation) {
return `${greeting}, I am ${this.name}${punctuation}`;
}
const aysel = { name: 'Aysel' };
const murad = { name: 'Murad' };
console.log(introduce.call(aysel, 'Hi', '!'));
console.log(introduce.apply(murad, ['Hello', '.']));
const ayselIntro = introduce.bind(aysel, 'Hey');
console.log(ayselIntro('!!'));
console.log(Math.max.apply(null, [3, 9, 4]));▸ Expected output
Hi, I am Aysel! Hello, I am Murad. Hey, I am Aysel!! 9
call takes the arguments one by one, apply takes them as an array. bind does not call the function — it returns a new one with this (and, if you like, the first arguments) fixed.When you pass a method as a callback, it gets “detached” from its object. Class methods always run in strict mode, so this becomes undefined and reading a property throws a TypeError. Fixes: bind, an arrow-function wrapper, or an arrow function written as a class field.
class Player {
score = 0;
addPoint() {
this.score++;
}
addPointArrow = () => {
this.score++;
};
}
const player = new Player();
const detached = player.addPoint;
try {
detached();
} catch (error) {
console.log('Lost this:', error.name);
}
[1, 2].forEach(player.addPoint.bind(player));
[1, 2, 3].forEach(player.addPointArrow);
console.log(player.score);▸ Expected output
Lost this: TypeError 5
Write createIdGenerator(prefix): it returns a function that gives the next id on every call: user-1, user-2… Keep the counter in a closure, and give every generator its own counter.
function createIdGenerator(prefix) {
// keep a counter here
return () => {
// increase the counter and return `${prefix}-${count}`
};
}
const userId = createIdGenerator('user');
const orderId = createIdGenerator('order');
console.log(userId());
console.log(userId());
console.log(orderId());
console.log(userId());▸ Expected output
user-1 user-2 order-1 user-3
finalPrice takes the discount from this.discount. 1) Use call to print the price of a 200-manat item for the shop object. 2) Use bind to create a function bound to shop, and pass it to map to convert the prices [50, 80].
const shop = { discount: 10 };
function finalPrice(price) {
return price - (price * this.discount) / 100;
}
// 1) finalPrice for 200 with call
// 2) bind to shop and use it in map for [50, 80]▸ Expected output
180 [ 45, 72 ]
Key points
- Lexical scope: a function sees the variables of the place where it is written; the lookup goes outward along the scope chain.
let/constare block-scoped and sit in the TDZ until their line;varis function-scoped and hoisted asundefined.- A closure is a function plus the variables it remembers; each call of the outer function creates a new set.
- An IIFE and the module pattern hide private data and return only a public API.
thisdepends on how a function is called:call/applyset it once,bindfixes it for good, and arrow functions take it from outside.
Check yourself
10 questions. Every correct answer earns XP.
function outer() { let n = 0; return () => ++n; }, what does b() return after const a = outer(); const b = outer(); a(); a();?