JAVASCRIPT / SCOPE, CLOSURES, AND THIS
Private state with closures
Build factory functions that keep state in a closure so callers can only read or change it through the methods you choose to return.
What you will learn
- Write a factory whose methods all share one private let binding
- Explain why obj.count is undefined while obj.read() returns the live value
- Give every factory call its own isolated state instead of one shared variable
- Return copies of internal arrays and objects so callers cannot mutate them
Understanding Private state with closures
A variable declared with let inside a function has no name outside that function. Nothing written from the outside can reach it: not obj.count, not Object.keys(obj), not a bracket lookup, not JSON.stringify, because the variable never becomes a property of anything. When the factory returns an object of functions, those functions are the only code that was written with that name in scope, so they are the only door in. The privacy comes from the scope rules themselves, not from a keyword and not from an _underscore naming convention.
Each call to the factory creates a fresh environment, and every function created during that call points at that same environment. That is why increment and read from one call always agree on the number while a second call's pair keeps its own: one binding per call, shared by its methods, invisible to everyone else. It also means these methods do not need this to find their data, so const inc = counter.increment; inc() still works after the method is pulled off the object.
The public surface is exactly what you hand back, and handing back a value is not the same as handing back access. Returning a number copies it, so a caller cannot write through it; returning the internal array hands over a live reference, and a single push from outside walks straight past the validation inside your methods. When the state is not a primitive, return a computed value or a copy such as items.slice() or { ...state }.
function createCounter(start) {
let count = start;
return {
increment() {
count += 1;
return count;
},
reset() {
count = start;
return count;
},
read() {
return count;
}
};
}
const a = createCounter(10);
const b = createCounter(10);
a.increment();
a.increment();
console.log(a.read());
console.log(b.read());
console.log(a.count);
console.log(Object.keys(a).join(','));State is private when the only code that can name the variable is the code you chose to return.
Worked examples
Enforcing rules on private state
Shows that a rule written inside the factory cannot be skipped, because there is no other way to change the value.
function createAccount(initial) {
let balance = initial;
return {
deposit(amount) {
if (amount <= 0) return 'invalid amount';
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) return 'insufficient funds';
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const acct = createAccount(100);
console.log(acct.deposit(50));
console.log(acct.withdraw(500));
acct.balance = 999;
console.log(acct.getBalance());Example explained
Line 1let balance = initial is the only slot holding the money, and nothing outside the factory can name it.
Line 2withdraw(500) returns early, so the balance can never go negative and the check cannot be bypassed.
Line 3acct.balance = 999 only adds an ordinary property to the returned object; the closure variable is a different slot.
Line 4getBalance() still reports 150, which proves the two slots are unrelated.
Leaking privacy through a returned array
Demonstrates that returning the internal array makes it public, while returning a copy keeps it private.
function createQueue() {
const items = [];
return {
add(item) {
items.push(item);
return items.length;
},
all() {
return items;
},
snapshot() {
return items.slice();
}
};
}
const q = createQueue();
q.add('a');
q.all().push('sneaky');
console.log(q.snapshot().join(','));
q.snapshot().push('ignored');
console.log(q.snapshot().join(','));Example explained
Line 1all() returns items itself, so after that call the outside world holds the private array.
Line 2q.all().push('sneaky') changes internal state without ever going through add().
Line 3snapshot() returns items.slice(), a brand new array, so pushing into it touches nothing inside.
Line 4The second log is unchanged, showing the copy absorbed the mutation.
One-off private state with an IIFE
Uses an immediately invoked function to create a single private counter when you need one instance rather than a factory.
const nextId = (function () {
let last = 0;
return function () {
last += 1;
return 'id-' + last;
};
})();
console.log(nextId());
console.log(nextId());
console.log(typeof last);Example explained
Line 1The function expression runs once immediately, so last is created exactly one time for the whole program.
Line 2The returned function is the sole holder of that binding, so no other code can reset the sequence and cause duplicate ids.
Line 3typeof last is 'undefined' because last was never created outside the IIFE, and typeof on an unknown name reports instead of throwing.
Important notes
Closure state does not appear in Object.keys, JSON.stringify, or the devtools object view, so add an explicit snapshot() method when you need to inspect or serialize it.
Every factory call allocates a fresh function object for each method; for many thousands of instances a class with # private fields gives the same hard privacy with methods shared on the prototype.
Common mistakes
Putting the state in the returned literal, as in return { count, increment }: the property is a one-time copy of the number, so obj.count stays at its starting value forever while increment() keeps climbing.
Declaring let count = 0 above the factory instead of inside it: every call closes over the same binding, so a second counter silently continues the first one's total.
Returning the private array or object from a reader: the caller now holds the real thing and can mutate it directly, bypassing every validation the methods perform.
Try it yourself
Change, predict, then run
In a browser console, write createTimer() that keeps a private seconds count and returns tick(), reset(), and read(). Create two timers, tick one three times, then check that the other still reads 0 and that setting timer.seconds = 99 does not change what read() returns.
Open the JavaScript workspaceCheck your understanding
A factory declares let n = 0 and returns { inc() { n += 1; return n; }, n }. After calling inc() twice, what does reading the object's n property give, and why?
- 0, because the property was set to a copy of n's value at the moment the object was created
- 2, because the shorthand property is an alias for the closure variable
- undefined, because closure variables cannot be copied into properties
- 2, because object literals turn shorthand properties into getters
Show answer
The shorthand n in the literal evaluates the variable once, during construction, and stores that number in a separate property slot; inc() then updates only the variable, so the property stays at 0. Answering 2 assumes the property tracks the variable, which would only happen if you wrote get n() { return n; }, since a getter runs the lookup again on each read.