Functions and Closures
Implicit vs. Explicit
In JavaScript, we can define functions that receive named parameters. For example:
function printArgs(a, b) { console.log(a, b); } printArgs(10, 20, 30, 40);
The printArgs
function receives a and b as arguments. We call these explicit parameters, but every JavaScript function also has implicit parameters, even if it was defined without any explicit parameters. An example of an implicit parameter is the arguments
parameter. You can access an arguments
parameter inside any function in JavaScript. For example, to access the third and fourth arguments passed to printArgs
, we can use arguments
elements:
function printArgs(a, b) { // Implicit Parameter: arguments console.log(a, b); console.log(arguments[2], arguments[3]); } printArgs(10, 20, 30, 40);
Since we have arguments
as an implicit parameter, that word is a reserved one. For example, we cannot use the word arguments
as an explicit parameter:
function printArgs(a, b, arguments) { } // SyntaxError: arguments is a reserved word
The code above will fail because we are attempting to define an explicit arguments
parameter. This is invalid in strict mode
.
Strict mode is a way to opt in to a restricted variant of JavaScript. It is the default mode in modern JavaScript environments. Your code should not rely on anything that is only allowed in non-strict mode. |
Exercise:
Explore the implicit arguments
parameter in the printArgs
example above. See what it holds. Pass different arguments to the printArgs
function and see how that affects the implicit arguments
parameter. What happens when you do not pass any arguments?
The arguments Parameter
The implicit arguments
parameter is an object representing the array of explicit arguments provided in the function call. The implicit arguments parameter is not exactly an array but can be mostly treated like one. We can loop over it and use its items one by one just like an array.
For example, say that we want to write a function that returns the sum of all its arguments no matter how many of those we pass.
Let’s name the function addArgs
and test it by calling it with a different number of arguments:
function addArgs() { // ... } addArgs(4, 5, 6) // should return 15 addArgs(1, 1, 1, 1, 1, 1) // should return 6
Since we are dealing with a dynamic number of arguments here, we cannot explicitly name them. We have to use the implicit arguments parameter. Here is a possible implementation:
function addArgs() { let sum = 0; for (let i = 0; i < arguments.length; i++) { sum += arguments[i]; } return sum; };
In the code above, we defined a sum
container and looped over the implicit arguments
parameter as if it was an array. For each item, we added to sum
the value of the arguments object at that index and finally returned sum.
Exercise:
Try to write a multiplyArgs
function that returns the product of all of its arguments no matter how many it receives:
Here is a starting template:
function multiplyArgs() { // Implement... }; multiplyArgs(4, 5, 6); // should return 120 multiplyArgs(2, 2, 2, 2); // should return 16
Solution: jscomplete.com/playground/HyXsdRxgz
Rest Parameters
The implicit arguments
parameter is not exactly an array. In the addArgs
example, if we attempt to use a forEach
call on arguments
instead of a regular for loop, the code will not work.
function addArgs() { let sum = 0; arguments.forEach(arg => { sum += arg; }); return sum; }; // TypeError: arguments.forEach is not a function
This is because the arguments
object is not exactly an array. We cannot directly use most of the array methods on arguments
. If we need to use array methods, we will have to first convert this arguments
parameter into an array or use the call
function prototype trick instead.
Using the call
trick is actually pretty easy. We can simply grab a reference to the array method that we want to invoke (using the Array prototype object for example). We can then call
the call
function on that reference, passing in the arguments
parameter as the first argument and any explicit parameters that we want to pass to the original method as the remaining arguments of the call
function.
// Instead of: arguments.arrMethod(arg1, arg2) // We can do: arrMethodRef.call(arguments, arg1, arg2)
For the addArgs
example, we can get a reference to forEach
using Array.prototype.forEach
and then call the call
function on that:
function addArgs() { let sum = 0; Array.prototype.forEach.call(arguments, arg => { sum += arg; }); return sum; };
Note how we passed the arguments
object as the first argument to the call
function. We passed the original forEach
function callback handler as the second argument to the call
function. This will work fine.
However, in modern JavaScript, instead of using the call
trick with the arguments
parameter, there is an easier way to treat a dynamic list of arguments as a native array. We can use Rest Parameters to create an array from the explicit arguments provided in the function call.
We just define a named argument with three dots before it.
function addArgs(...args) { }
The three-dots operator is called the rest operator. Using it before the last explicit argument name converts that named argument into an array of all argument values provided in that position.
The variable args
above will be an actual JavaScript array object. So we can use all array methods on it, like forEach
for example, to do our addArgs
example without a need to use the call
function. Or better yet, we can just use the reduce
method on this new args
array to sum the content with a one-liner:
function addArgs(...args) { return args.reduce((acc, curr) => acc + curr); };
The reduce method is explained in this article.
Exercise:
Combine a REST parameter with other explicitly named arguments and test to see what happens.
Closures
JavaScript closures are like car functions — they come with various components that can be in various positions specific to that car.
Every function in JavaScript has a closure and this is one of the coolest features of the JavaScript language. Without closures, it would be difficult to implement common structures like callbacks or event handlers.
We create a closure whenever we define a function. Then, when we execute a function, its closure enables it to access data in its scopes.
It is kind of like when a car is manufactured (defined), it comes with a few functions like start, accelerate, decelerate
. These car functions get executed by the driver every time they operate the car. Closures for these functions come defined with the car itself and they close over variables they need to operate.
Let’s narrow this analogy to the accelerate
function. The function definition happens when the car is manufactured:
function accelerate(force) { // Is the car started? // Do we have fuel? // Are we in traction control mode? // Many other checks... // If all good, burn more fuel depending on the force // variable (how hard we\'re pressing the gas pedal) }
Every time the driver presses the gas pedal, this function gets executed. Note how this function needs access to a lot of variables to operate, including its own force
variable. But more importantly, it needs variables outside of its scope that are controlled by other car functions. This is where the closure of the accelerate function (which we get with the car itself) comes in handy.
Here is what the accelerate
function’s closure promised to the accelerate
function itself:
Ok accelerate, when you get executed, you can access your force variable, the isCarStarted variable, the fuelLevel variable, and the isTractionControlOn variable. You can also control the currentFuelSupply variable that we are sending to the engine.
Note that the closure did not give the accelerate
function fixed values for these variables, but rather permission to access those values at the time the accelerate function is executed.
Closures are closely related to function scopes. When we execute a function, a private function scope is created and used for the process of executing that function. Then, these function scopes get nested when you execute functions from within functions (which is a common practice).
A closure is created when we define a function, not when we execute it. Then, every time we execute that function, its already-defined closure gives it access to all the function scopes available around it.
In a way, you can think of scopes as temporary (the global scope is the only exception to this), while you can think of closures themselves as permanent.
To truly understand closures and the role they play in JavaScript, you first need to understand five simple concepts about JavaScript functions and their scopes.
Functions are Assigned by Value Reference
When you put a function in a variable like this:
function sayHello() { console.log("hello"); }; var func = sayHello;
You are assigning the variable func
a reference to the function sayHello
, not a copy. Here, func is simply an alias to sayHello
.
Anything you do on the alias you will actually be doing on the original function. For example:
func.answer = 42; console.log(sayHello.answer); // prints 42
The property answer
was set directly on func
and read using sayHello
, which works.
You can also execute sayHello
by executing the func
alias:
func(); // prints "hello"
Scopes Have a Lifetime
When you call a function, you create a scope during the execution of that function. Then that scope goes away.
When you call the function a second time, you create a new different scope during the second execution. Then this second scope goes away as well.
function printA() { console.log(answer); var answer = 1; }; printA(); // This first call creates a new scope // which gets discarded right after printA(); // This second call creates a new different scope // which also gets discarded right after;
These two scopes that were created in the example above are different. The variable answer
here is not shared between them at all.
Every function scope has a lifetime. They get created and discarded right away. The only exception to this fact is the global scope, which does not go away as long as the application is running.
Closures Span Multiple Scopes
When you define a function, a closure gets created.
Unlike scopes, closures are created when you define a function, not when you execute it. Closures also do not go away after you execute the function.
You can access the data in a closure long after a function is defined and after it gets executed as well.
A closure encompasses everything the defined function can access. This includes the defined function’s scope, all the nested scopes between the global scope and the defined function scope, and the global scope itself.
var G = 'G'; // Define a function and create a closure function functionA() { var A = 'A' // Define a function and create a closure function functionB() { var B = 'B'; console.log(A, B, G); } functionB(); // prints A, B, G // functionB's closure does not get discarded A = 42; functionB(); // prints 42, B, G } functionA();
When we define functionB
here, its created closure will allow us to access the scope of functionB
, plus the scope of functionA
, plus the global scope.
Every time we execute functionB
, we can access variables B
, A
, and G
through its previously created closure. However, that closure does not give us a copy of these variables but rather a reference to them. So if, for example, the value of the variable A
gets changed at some point after the closure of functionB
is created, when we execute functionB
after that, we will see the new value, not the old one. The second call to functionB prints 42
, B
, G
because the value of variable A
was changed to 42
and the closure gave us a reference to A
, not a copy.
Do Not Confuse Closures With Scopes
It is common for closures to be confused with scopes, so let’s make sure not to do that.
// scope: global var a = 1; void function one() { // scope: one // closure: [one, global] var b = 2; void function two() { // scope: two // closure: [two, one, global] var c = 3; void function three() { // scope: three // closure: [three, two, one, global] var d = 4; console.log(a + b + c + d); // prints 10 }(); // three }(); // two }(); // one
In the simple example above, we have three functions that are all defined and immediately invoked, so they all create scopes and closures.
The scope of function one()
is its body. Its closure gives us access to both its scope and the global scope.
The scope of function two()
is its body. Its closure gives us access to its scope, plus the scope of function one()
, plus the global scope.
And similarly, the closure of function three()
gives us access to all scopes in the example. This is why we were able to access all variables in function three()
.
But the relation between scopes and closures is not always this. Things get complicated when the defining and invoking of functions happen in different scopes. Let me explain that with an example:
var v = 1; var f1 = function () { console.log(v); } var f2 = function() { var v = 2; f1(); // *Will this print 1 or 2?* }; f2();
What do you think the above example will print? The code is simple; f1()
prints the value of v
, which is 1
on the global scope, but we execute f1()
inside of f2()
, which has a different v
that is equal to 2
. Then we execute f2()
.
Will the previous code print 1 or 2?
If you are tempted to say 2
, you will be surprised. This code will actually print 1
. The reason is, scopes and closures are different. The console.log
line will use the closure of f1()
, which is created when we define f1()
. This means the closure of f1()
gives us access to only the scope of f1()
plus the global scope. The scope where we execute f1()
does not affect that closure. In fact, the closure of f1()
will not give us access to the scope of f2()
at all. If you remove the global v
variable and execute this code, you will get a reference error:
var f1 = function () { console.log(v); } var f2 = function() { var v = 2; f1(); // ReferenceError: v is not defined }; f2();
This is very important to understand and remember.
Closures Have Read and Write Access
Since closures give us references to variables in scopes, the access that they give us means both read and write, not just read.
Take a look at this example:
function outer() { let a = 42; function inner() { a = 43; } inner(); console.log(a); } outer();
The inner()
function here, when defined, creates a closure that gives us access to the variable a
. We can read and modify that variable and if we do modify it we will be modifying the actual a
variable in the outer()
scope.
This code will print 43
because we used the inner()
function’s closure to modify the outer()
function’s variable.
This is actually why we can change global variables everywhere. All closures give us both read and write access to all global variables.
Closures Can Share Scopes
Since closures give us access to nested scopes at the time we define functions, when we define multiple functions in the same scope that scope is shared among all created closures. Because of this, the global scope is always shared among all closures.
function parentFunction() { let a = 10; function double() { a = a + a; console.log(a); }; function square() { a = a * a; console.log(a); } return { double, square }; } let { double, square } = parent(); double(); // prints 20 square(); // prints 400 double(); // prints 800
In the example above, we have a parentFunction()
function with variable a
set to 10
. We define two functions in this parentFunction()
scope, double()
and square()
. The closures created for double() and square()
both share the scope of parentFunction()
. Since both double()
and square()
change the value of a
, when we execute the last three lines, we double a
(making a = 20), then square that doubled value (making a
= 400), and then double that squared value (making a
= 800).
Exercise:
Let’s check your understanding of closures. Before you execute the following code, try to figure out what it will print:
let a = 1; const function1 = function() { console.log(a); a = 2; } a = 3; const function2 = function() { console.log(a); } function1(); function2();
Solution: jscomplete.com/playground/BkpqwkWgz