Appearance
Closure
Functions as Return Values
Higher-order functions can not only accept functions as parameters but can also return functions as results.
Let's implement a function to sum an array. Typically, a sum function is defined like this:
javascript
function sum(arr) {
return arr.reduce(function (x, y) {
return x + y;
});
}
sum([1, 2, 3, 4, 5]); // 15
However, if we don't want to calculate the sum immediately but instead want to compute it later based on our needs, we can return a function that performs the summation!
javascript
function lazy_sum(arr) {
let sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}
When we call lazy_sum()
, what is returned is not the sum result but the sum function:
javascript
let f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
Only when we call the function f
do we actually compute the sum:
javascript
f(); // 15
In this example, we defined the function sum
within lazy_sum
, and the inner function sum
can reference the parameters and local variables of the outer function lazy_sum
. When lazy_sum
returns the function sum
, the relevant parameters and variables are preserved in the returned function. This programming structure, known as "closure," has immense power.
Note that each time we call lazy_sum()
, a new function is returned, even if the same parameters are passed:
javascript
let f1 = lazy_sum([1, 2, 3, 4, 5]);
let f2 = lazy_sum([1, 2, 3, 4, 5]);
f1 === f2; // false
The calls to f1()
and f2()
do not affect each other.
Closure
Notice that the returned function references the local variable arr
in its definition. Therefore, when a function returns another function, the internal local variables are still referenced by the new function. While closures are easy to use, they can be complex to implement.
Another important point is that the returned function does not execute immediately; it only executes when f()
is called. Let's look at an example:
javascript
function count() {
let arr = [];
for (var i = 1; i <= 3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
let results = count();
let [f1, f2, f3] = results;
In the above example, each loop creates a new function, and those three functions are added to an array and returned.
You might think that calling f1()
, f2()
, and f3()
should return 1, 4, and 9, but the actual results are:
javascript
f1(); // 16
f2(); // 16
f3(); // 16
They all return 16! The reason is that the returned functions reference the variable i
, which was defined using var
, but it does not execute immediately. By the time the three functions are returned, the referenced variable i
has become 4, resulting in the final output being 16.
One takeaway when returning closures is to avoid referencing any loop variables or variables that may change later.
What if you need to reference a loop variable? The solution is to create another function that binds the current value of the loop variable as a parameter. Regardless of how the loop variable changes afterward, the bound value remains unchanged:
javascript
function count() {
let arr = [];
for (var i = 1; i <= 3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
let [f1, f2, f3] = count();
f1(); // 1
f2(); // 4
f3(); // 9
Note that here we used a "create and immediately execute an anonymous function" syntax:
javascript
(function (x) {
return x * x;
})(3); // 9
Theoretically, creating and immediately executing an anonymous function could be written as:
javascript
function (x) { return x * x }(3);
However, due to JavaScript syntax parsing issues, it will throw a SyntaxError. Therefore, the entire function definition needs to be enclosed in parentheses:
javascript
(function (x) { return x * x })(3);
Typically, an immediately executed anonymous function can break down the function body and is often written as:
javascript
(function (x) {
return x * x;
})(3);
Another approach is to define the loop variable i
using let
within the for
loop body, as let
scoping ensures a new i
is bound in each iteration:
javascript
function count() {
let arr = [];
for (let i = 1; i <= 3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
However, if i
is defined outside of the for
loop, it remains incorrect:
javascript
function count() {
let arr = [];
let i;
for (i = 1; i <= 3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
Therefore, the best approach is still to avoid referencing any loop variables in returned functions.
After all this, is closure just for returning a function and delaying execution?
Certainly not! Closures have very powerful functionalities. For example:
In object-oriented programming languages like Java and C++, private variables can be encapsulated within an object using the private
modifier.
In languages that lack class mechanisms and only use functions, closures can similarly encapsulate private variables. Here's how we can create a counter in JavaScript:
javascript
function create_counter(initial) {
let x = initial || 0;
return {
inc: function () {
x += 1;
return x;
}
}
}
It can be used like this:
javascript
let c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
c1.inc(); // 3
let c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12
c2.inc(); // 13
In the returned object, a closure is implemented that carries the local variable x
, which is inaccessible from external code. In other words, a closure is a function that carries state, and its state can be completely hidden from the outside.
Closures can also transform functions with multiple parameters into functions with a single parameter. For example, to compute (x^y) we can use the Math.pow(x, y)
function. However, considering the frequent need to calculate (x^2) or (x^3), we can use closures to create new functions pow2
and pow3
:
javascript
function make_pow(n) {
return function (x) {
return Math.pow(x, n);
}
}
// Create two new functions:
let pow2 = make_pow(2);
let pow3 = make_pow(3);
console.log(pow2(5)); // 25
console.log(pow3(7)); // 343