Inner functions
JavaScript is fortunate to number itself among the programming languages that support inner function declarations . Many traditional programming languages, such as C, collect all functions in a single top-level scope. Languages with inner functions, on the other hand, allow us to gather small utility functions where they are needed, avoiding namespace pollution .
An inner function is simply a function that is defined inside of another function. For example:
function outerFn() { function innerFn() { } }
Here, innerFn()
is an inner function, contained within the scope of outerFn()
. This means that a call to innerFn()
is valid within outerFn()
, but not outside of it. The following code results in a JavaScript error:
function outerFn() { $.print('Outer function'); function innerFn() { $.print('Inner Function'); } } $.print('innerFn():'); innerFn();
We can successfully run the code, though, by calling innerFn()
from within outerFn()
as follows:
function outerFn() { $.print('Outer function'); function innerFn() { $.print('Inner function'); } innerFn(); } $.print('outerFn():'); outerFn();
This results in the output:
outerFn(): Outer function Inner function
This technique is especially handy for small, single-purpose functions. For example, algorithms that are recursive, but have a non-recursive API wrapper, are often best expressed with an inner function as a helper.
The great escape
The plot thickens when function references come into play. Some languages, such as Pascal, allow the use of inner functions for the purpose of code hiding only; those functions are forever entombed within their parent functions. JavaScript, on the other hand, allows us to pass functions around just as if they were any other kind of data. This means inner functions can escape their captors.
The escape route can wind in many different directions. For example, suppose the function is assigned to a global variable as follows:
var globalVar; function outerFn() { $.print('Outer function'); function innerFn() { $.print('Inner function'); } globalVar = innerFn; } $.print('outerFn():'); outerFn(); $.print('globalVar():'); globalVar();
The call to
outerFn()
after the function definition modifies the global variable globalVar
. It is now a reference to innerFn()
. This means that the later call to globalVar()
operates just as an inner call to innerFn()
would, and the print statements are reached:
outerFn(): Outer function globalVar(): Inner function
Note that a call to innerFn()
from outside of outerFn()
still results in an error! Though the function has escaped by way of the reference stored in the global variable, the function name is still trapped inside the scope of outerFn()
.
A function reference can also find its way out of a parent function through a return value as follows:
function outerFn() { $.print('Outer function'); function innerFn() { $.print('Inner function'); } return innerFn; } $.print('var fnRef = outerFn():'); var fnRef = outerFn(); $.print('fnRef():'); fnRef();
Here, there is no global variable modified inside outerFn()
. Instead, outerFn()
returns a reference to innerFn()
. The call to outerFn()
results in this reference, which is stored and called itself in turn, triggering the message again:
var fnRef = outerFn(): Outer function fnRef(): Inner function
The fact that inner functions can be invoked through a reference, even after the function has gone out of scope, means that JavaScript needs to keep referenced functions available as long as they could possibly be called. Each variable that refers to the function is tracked by the JavaScript runtime and once the last has gone away, the JavaScript garbage collector comes along and frees up that bit of memory.
Variable scoping
Inner functions can, of course, have their own variables, which are restricted in scope to the function itself:
function outerFn() { function innerFn() { var innerVar = 0; innerVar++; $.print('innerVar = ' + innerVar); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();
Each time this inner function is called, through a reference or otherwise, a new variable innerVar
is created, incremented, and displayed as follows:
innerVar = 1 innerVar = 1 innerVar = 1 innerVar = 1
Inner functions can reference global variables in the same way as any other function can:
var globalVar = 0; function outerFn() { function innerFn() { globalVar++; $.print('globalVar = ' + globalVar); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();
Now our function will consistently increment the variable with each call:
globalVar = 1 globalVar = 2 globalVar = 3 globalVar = 4
However, what if the variable is local to the parent function? As the inner function inherits its parent's scope, this variable can be referenced too:
function outerFn() { var outerVar = 0; function innerFn() { outerVar++; $.print('outerVar = ' + outerVar); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();
Now our function calls have more interesting behavior:
outerVar = 1 outerVar = 2 outerVar = 1 outerVar = 2
This time we get a mix of the two earlier effects. The calls to innerFn()
through each reference increment outerVar
independently. Note that the second call to outerFn()
is not resetting the value of outerVar
, but rather creating a new instance of outerVar
, bound to the scope of the second function call. The upshot of this is that after the above calls, another call to fnRef()
will print the value 3
, and a subsequent call to fnRef2()
will also print 3
. The two counters are completely separate.
When a reference to an inner function finds its way outside of the scope in which the function was defined, this creates a closure on that function. We call variables that are neither parameters nor local to the inner function free variables , and the environment of the outer function call closes them. Essentially, the fact that the function refers to a local variable in the outer function grants the variable a stay of execution. The memory is not released when the function completes, as it is still needed by the closure.