Lesson 10 / 17
Immediately Invoked Functions
The spellings that turn a function declaration into an expression, scope isolation and the revealing module pattern, one-time setup, and comparison with block scope.
Contents
In the previous lesson’s old fix for the loop trap, a function was called the instant it was defined. Its purpose was not to produce a value but to open a new scope. This pattern’s name is the immediately invoked function expression.
The pattern does two jobs: it isolates the declarations in its body from the outer scope, and it hands only selected values back out from the inside. It can also be read as the Closures lesson’s factory functions, run once for a single object.
Turning a Function Into an Expression
When the parser sees the word function at the start of a statement, it treats it as a
declaration, and declarations cannot be immediately invoked. This is why the
pattern’s wrapping parentheses exist: they move the function out of statement position
and into expression position.
try { new Function("function () { return 1; }();"); } catch (error) { console.log(`as declaration: ${error.constructor.name}`); } const first = (function () { return "wrapped call"; })(); const second = (function () { return "closed from inside"; }()); const third = (() => "arrow form")(); const fourth = void (function () { console.log("converted to expression by operator"); })(); console.log(first); console.log(second); console.log(third); console.log(fourth);
as declaration: SyntaxError converted to expression by operator wrapped call closed from inside arrow form undefined
The first attempt shows that the declaration form cannot be parsed; because
new Function parses the text, the error can be caught at runtime. The remaining
three forms are valid and behave identically: the call parentheses can sit inside or
outside the wrapper, and an arrow function does the same job.
The fourth form shows forcing expression position with an operator. Because the void
operator makes the result undefined, it used to be preferred where the return value
is not used. The output order also reveals a detail: the print inside the body runs
before the console.log(first) line, because the function is called immediately where
it is defined.
Scope Isolation and the Module Pattern
The pattern’s real use is gathering a group of related functions around shared, hidden state. What is defined inside is not visible from outside; only the names on the returned object become accessible. This construction is called the revealing module pattern.
const measurementLog = (function () { const records = []; let nextOrder = 1; function add(sensor, value) { const record = { order: nextOrder, sensor, value }; nextOrder += 1; records.push(record); return record.order; } function bySensor(sensor) { return records.filter((r) => r.sensor === sensor).length; } function summary() { return `${records.length} records, next order ${nextOrder}`; } return { add, bySensor, summary }; })(); console.log(measurementLog.add("S-01", 21.4)); console.log(measurementLog.add("S-02", 19.8)); console.log(measurementLog.add("S-01", 22.0)); console.log(measurementLog.bySensor("S-01")); console.log(measurementLog.summary()); console.log(Object.keys(measurementLog).join(",")); console.log(typeof measurementLog.records); console.log(JSON.stringify(measurementLog));
1
2
3
2
3 records, next order 4
add,bySensor,summary
undefined
{}
The records array and the order counter cannot be reached from outside; the log’s interface consists of only three functions. That the order number cannot be corrupted from outside is the preservation of the class invariant introduced in the Programming Foundations course’s Introduction to Object-Oriented Programming lesson — here, encapsulation is provided by a scope boundary instead of a class.
Where the pattern parts ways with a class is in number: this construction produces a single object. If a second log of the same kind is wanted, it should be used as an ordinary factory function, not called immediately. Single-instance structures — an application configuration, a log writer, a counter — are this pattern’s natural territory.
One-Time Setup
The second common use is preventing the intermediate steps needed to set up a value from leaking out. The value is computed once; intermediate variables stay inside the pattern.
const CALIBRATION = (function () { const rawLines = ["S-01;-0.4", "S-02;0.15", "S-03;0"]; const table = Object.create(null); for (const line of rawLines) { const [sensor, offset] = line.split(";"); table[sensor] = Number(offset); } return Object.freeze(table); })(); console.log(CALIBRATION["S-01"]); console.log(CALIBRATION["S-02"]); console.log(Object.isFrozen(CALIBRATION)); console.log(Object.getPrototypeOf(CALIBRATION)); console.log(typeof rawLines); try { CALIBRATION["S-04"] = 1; } catch (error) { console.log(`add: ${error.constructor.name}`); } console.log(Object.keys(CALIBRATION).join(","));
-0.4 0.15 true null undefined add: TypeError S-01,S-02,S-03
The raw lines and the loop variable are never visible in the outer scope at all; what
is left is only a table ready for use. Because the table is built with
Object.create(null) it has no chain, and because it is frozen with Object.freeze
it cannot be changed afterward — the two decisions from the Immutability Techniques
lesson are applied together here.
This pattern fits constants that have a setup cost. Doing the setup once instead of on every use is a small-scale example of the preprocessing idea from the Algorithms course.
Passing a Dependency In
The pattern’s second common use is the wrapper’s parameter list. If every name used from outside is passed explicitly, what the body depends on becomes readable, and name lookup does not reach into outer scopes.
const settings = { threshold: 25 }; const report = (function (source, unit) { const measurements = [21.4, 26.8, 23.1]; const exceeding = measurements.filter((d) => d > source.threshold); return `${exceeding.length} measurement exceeded the ${source.threshold}${unit} threshold`; })(settings, "C"); console.log(report); (function setupCheck() { try { null.value; } catch (error) { const firstLine = error.stack.split("\n")[1].trim().split(" ")[1]; console.log("name in stack trace:", firstLine); } })();
1 measurement exceeded the 25C threshold name in stack trace: setupCheck
The second call shows a detail: the wrapper can be given a name. The name is not accessible from outside — it appears only in the function’s own body and in the stack trace. In an unnamed wrapper, the same line is listed as anonymous; the named form of the pattern is preferred while debugging.
The stack trace’s format varies by runtime; here, only the name appearing in the listing matters.
Comparison With Block Scope and Modules
Both of the pattern’s functions are largely covered by the language’s later abilities. Block-scoped declarations provide isolation; the module system gives a namespace at the file level.
let blockResult; { const temp = [21.4, 19.8, 25.1]; blockResult = temp.length; } console.log(blockResult); console.log(typeof temp); const expressionResult = (() => { const temp = [21.4, 19.8, 25.1]; return temp.reduce((total, d) => total + d, 0) / temp.length; })(); console.log(expressionResult.toFixed(3)); var scopeTest = "outside"; (function () { var scopeTest = "inside"; console.log(scopeTest); })(); console.log(scopeTest);
3 undefined 22.100 inside outside
A block isolates const and let declarations, and if isolation is the only goal, it
is a plainer path than an immediately invoked function. The second section shows the
difference: a block is a statement, it produces no value; carrying its result out
requires assigning to a variable declared beforehand. An immediately invoked function
is an expression and can be bound directly to a constant.
The third section shows the one case the pattern is still needed for: names declared
with var do not enter block scope, they are isolated only by function scope.
The module system, meanwhile, has taken over the namespace job. Every file is its own scope; no name that is not exported is seen from another file. Reaching for the pattern to prevent file-level name collisions is no longer necessary. Module system details are the subject of the Modules, Tooling, and Ecosystem course.
The remaining use cases gather into three headings: setting up a single-instance
interface together with hidden state, isolating a constant’s setup steps, and confining
var declarations that block scope cannot reach.
Summary
- Because the word
functionat the start of a statement is parsed as a declaration, an immediately invoked function is moved into expression position with a wrapper. - The pattern opens a new scope; what is defined inside is not visible from outside, only the returned value becomes accessible.
- The revealing module pattern produces a single object with hidden state; a factory function is used when more than one instance is needed.
- In one-time setup, intermediate variables are isolated and the result is handed out as a frozen constant.
- Block scope covers the isolation job but produces no value; the module system takes over the namespace job.
Next Step
Throughout this topic, functions have constantly been used as values: passed as callbacks, returned from another function, written as a property onto an object, wrapped to change behavior. All of these uses come from a single ability — a function also being a value. The next lesson takes up this ability directly and shows, while building filter, map, and reduce operations on measurement records, where the binding rules come into play.
To keep your progress and take notes, Log in
My notes
Log in to take notes.