JAVASCRIPT / OBJECTS AND PROPERTY MECHANICS
Factory functions that build objects
Build objects with a plain function that returns them, keeping per-object state in closure variables that no property lookup or delete can reach.
What you will learn
- Write a factory: a function that returns a fresh object literal, called without new
- Keep state in closure variables so it never shows up in Object.keys or JSON output
- Give every object its own arrays instead of one copy hoisted above the factory
- Detach methods safely because they close over variables instead of relying on this
Understanding Factory functions that build objects
A factory function is nothing more exotic than a function that ends in `return { ... }`. You call it like any other function, and each call evaluates that object literal again, so every call hands back a distinct object with its own properties. The distinctness is the point: the literal is a recipe that runs once per call, not a value shared between callers.
What makes a factory more than a shortcut for typing object literals is the scope it opens. Parameters and `let`/`const` declarations inside the factory belong to that one call, and the methods in the returned object hold live references to them, so the state stays reachable from the methods and unreachable from everywhere else. There is no property to read, list, overwrite, or delete: `ada.balance` is simply `undefined` while `ada.deposit(50)` keeps updating the number. A `_balance` property is private by convention; a closure variable is private by the language's scoping rules.
The trade is sharing for isolation. Every call builds new function objects for every method, so `a.deposit !== b.deposit`, nothing sits on a shared prototype, and `instanceof` has nothing to test against, since a factory is called without `new` and its object literal inherits from `Object.prototype`. In exchange the methods never mention `this`, so they keep working when pulled off the object and passed as a callback, and the factory can validate arguments, throw before any object exists, or decide at runtime which methods to include.
function createAccount(owner, balance) {
const history = [];
return {
owner,
deposit(amount) {
balance += amount;
history.push(amount);
return balance;
},
statement() {
return `${owner} has ${balance} after ${history.length} deposit(s)`;
}
};
}
const ada = createAccount('Ada', 100);
const bo = createAccount('Bo', 0);
ada.deposit(50);
ada.deposit(25);
bo.deposit(10);
console.log(ada.statement());
console.log(bo.statement());
console.log(Object.keys(ada).join(', '));
console.log(ada.balance);
const deposit = ada.deposit;
console.log(deposit(5));
console.log(ada.statement());A factory function is an ordinary function whose return value is a new object, and whose local variables become that object's private state.
Worked examples
One array for everyone
Shows why the arrays and objects a factory hands out must be created inside the function body.
const shared = { tags: [] };
function taggedNote(text) {
return { text, tags: shared.tags };
}
function freshNote(text) {
return { text, tags: [] };
}
const t1 = taggedNote('buy milk');
const t2 = taggedNote('call Bo');
t1.tags.push('home');
console.log('t2 tag count: ' + t2.tags.length);
const f1 = freshNote('buy milk');
const f2 = freshNote('call Bo');
f1.tags.push('home');
console.log('f2 tag count: ' + f2.tags.length);Example explained
Line 1`shared.tags` is read on every call but always resolves to the same array object, so both notes store one reference.
Line 2`t1.tags.push('home')` mutates that single array, which is why `t2` reports a tag nobody gave it.
Line 3The `[]` in `freshNote` is an array literal evaluated once per call, so `f2` gets a distinct array and stays empty.
Deciding the shape at call time
The same factory returns objects with different capabilities, and keeps a helper completely out of the public surface.
function createLogger(level) {
const lines = [];
const write = (tag, msg) => lines.push(`${tag} ${msg}`);
const api = {
error(msg) { write('ERROR', msg); },
dump() { return lines.join(' | '); }
};
if (level === 'debug') {
api.debug = (msg) => write('DEBUG', msg);
}
return api;
}
const quiet = createLogger('error');
const verbose = createLogger('debug');
verbose.debug('cache miss');
verbose.error('timeout');
quiet.error('timeout');
console.log(verbose.dump());
console.log(quiet.dump());
console.log(typeof quiet.debug);Example explained
Line 1`lines` and `write` are created once per call, so `quiet` and `verbose` accumulate into separate arrays.
Line 2`api.debug` is attached only when `level === 'debug'`, so one factory produces two different object shapes.
Line 3`typeof quiet.debug` is `undefined` because that property was never created; calling it would throw a TypeError.
Line 4`write` is a local variable, never a property of `api`, so no caller can log a line without the tag prefix.
Important notes
Each call allocates a fresh function object per method, so `a.deposit !== b.deposit`; that is irrelevant for hundreds of objects and only becomes a memory question when you create very large numbers of them.
`JSON.stringify(ada)` sees only `owner`, and `{ ...ada }` copies the method references, so the copy reads and writes the original object's hidden state instead of getting its own.
Common mistakes
Writing `const make = (x) => { value: x };` — the braces are parsed as a function body (`value:` becomes a label), so the factory returns `undefined` and the first `.method()` call fails with "Cannot read properties of undefined". Wrap the literal: `=> ({ value: x })`.
Hoisting a `const tags = []` or a defaults object above the factory to avoid re-creating it, which makes every returned object point at the same array, so one `push` appears on all of them.
Treating a factory like a constructor: `createUser('Ada') instanceof createUser` is `false` and methods added to `createUser.prototype` are never found, because the returned literal inherits from `Object.prototype`.
Try it yourself
Change, predict, then run
Write `createStopwatch()` that returns `{ tick, elapsed, reset }` with the tick count living only in a closure variable, then create two stopwatches, tick one three times and the other once, and print both `elapsed()` values. Confirm with `Object.keys` that the returned object exposes only the three methods.
Open the JavaScript workspaceCheck your understanding
A factory returns an object whose `deposit` method updates a closure variable; a class stores the same number as `this.balance`. Running `const d = obj.deposit; d(5)` works for the factory object but throws for the class instance. Why?
- The factory's `deposit` reads a variable from the scope that created it, so it needs no receiver, while the class method resolves `this` from the call site
- Object literals automatically bind their methods to the object with `.bind` when the literal is evaluated
- Copying a method out of an object literal keeps it linked to that object, whereas class methods live on the prototype and lose the link
- Class bodies run in strict mode and factories do not, so the factory version quietly creates a global `balance`
Show answer
A closure lookup is fixed lexically when the factory call created the function, so the detached copy still points at that call's `balance`. `this`, by contrast, is chosen per call, and a bare `d(5)` passes no receiver, so the class method sees `undefined` and throws. Option 1 is tempting because factory methods feel attached to their object, but nothing binds them: they never mention `this`, so there is nothing to bind.