Scope, Functions and Closures
It's not my intention to repeat the excellent explanations of others on the subjects of scope, functions and closures (some linked below). Instead this is meant to be a quick code focused reference for the occasions in which I find myself able to share what I've learned with others.
Scope
Scope is one of those subjects that we learn early on, think we understand and will therefore believe we can bypass. I believe understanding scope is important to understanding closures so I'll review in brief here.
The base scope in Javascript is the global scope which in the browser is called window
. Anything defined in the global scope is acccessible anywhere else in our programs. We know this by habit even if we haven't consciously considered it. When we define the add
function in a script tag we can call (or invoke) it anywhere else in our program. Open the console and try it out.
function add (a, b) {
return a + b;
}
add(1,2); // returns 3
Javascript has function scope. In other words when we wrote the add
function we created a new scope. add
's scope is anything we defined between ()
and {}
when we wrote the add
function; in this case the parameters a
and b
.
add
can access its own function scope and it can access it's parent's scope; in this case the global scope. Again we know this by habit.
var a = 1;
function add (b) {
return a + b;
}
add(2); // returns 3
function tenMinus(1) {
return 10 - add(a, 1);
}
tenMinus(2) // 7
The examples are very contrived but it comes as no surprise that add
can access a
or that tenMinus
can access add
. A function can access it's own scope and the scopes of its parent context or contexts. Conversely a parent scope can't access the scope of a child or function. There is no way to access the parameter b
of the add
function from the global scope.
In JavaScript we can write functions anywhere including inside of other functions.
var a = 1;
function add (b) {
function times () {
return a * b;
}
return a + times();
}
Additional nested functions create additional nested scopes and the rules continue to apply. The global scope can't access add
's scope and add
can't access times
' scope. But times
can access both add
's scope and the global scope while add
can as before access the global scope.
At this point you may be wondering why I've left let
and const
out. I've done so because they work with block scope1 which is pretty awesome but we're going to stay focused on function scope for this discussion
A last note about function scope before moving on. When we invoke add(1,2)
the run-time environment creates a new function scope for this invocation of add. 1
and 2
are inserted as the values of a
and b
. The add
function then runs to completion. After returning 3 the function scope is destroyed. This rule can be broken as we'll see later.
Functions
You may have heard that functions in JavaScript are first class objects or perhaps you've heard as Javascript has first class functions2. Either way is means the same thing. In JavaScript functions are treated like any other variable type. This is worth clarifying. In Javascript a function can be assigned to a variable, passed as an argument to a function or returned as the result of a function just like strings, arrays and etc.
This is again something we probably "know" beause we've typed it in hundreds of times but the implications are far reaching.
// 1
function add (a, b) {
return a + b;
}
// 2
const add = (a, b) => a + b;
// OR 3
const add = function (a, b) {
return a + b;
}
Above we see our initial add function (1) is declared using the function
keyword. This is called a "function declaration"3. In examples 2 and 3 we use a "function expression"4 to declare the add function. There are some subtle differences between function declarations and function expressions but in this case the key takeaway is that add
is NOT a function.
const o = { key: 1 }
Consider for a moment the creation of an object with a single key value pair. Is o
an object? At first glance you may be inclined to say yes but o
is not an object. o
is a name that references someplace memory where an object lives. Functions are no different. add
is not a function. add
is a name that references a place in memory where a function is stored.
If add
and o
are both just names referring to memory what happens when we do the following?
const p = o;
const a = add;
We probably know that p
is now a references to the same data stored in memory as o
. You may know this as "objects in javascript are passed by reference." But what's the case with a
? Is it also a reference to the same function as the add
keyword. If so can we invoke it?
a(3,4) // returns 7
a
is just another name for add
. a
can be invoked just like add
. Go ahead and try it in the console.
Just as a function can be assigned to a variable it can also be passed as an argument to a function and can again be invoked.
function math (a, b, callback) {
return callback(a, b);
}
math(2, 4, add) // returns 6
In this example we pass add
as an argument to the math
function which in turn invokes add
using the name callback
. This is again something we may know by habit. Every time we use a foreach
or map
(and many other built-in methods) we pass a function as an argument to another function.
Let's now turn to returning a function from a function.
function addOrSubtract (symbol) {
function add (a, b) {
return a + b;
}
function subtract (a, b) {
return a - b;
}
return symbol === "+" ? add : subtract;
}
const m = addOrSubtract("-");
m(3, 2) // returns 1
The addOrSubtract
function is a function that returns either the add
function or the subtract
function based on a string, "+" or "-" in this case5. When we invoke addOrSubtract
with "-" what is the value of m
? m
is now a reference to subtract
. When we invoke m
with 3 and 2 the subtract
function is called and 1 is returned.
This is pretty cool and is the first important step in learning how you can use functions to create functions in Javascript but it begs a further example.
function incrementer (startNum) {
let i = startNum;
function increment () {
return ++i;
}
function decrement () {
return --i;
}
return {
increment,
decrement
};
}
const myInc = incrementer(2);
myInc.increment(); // ?
myInc.decrement(); // ?
incrementer
is a function that instatiates a variable index
which holds a number. It returns an object containing both increment
and decrement
functions that change the value of index
and return it.
"But wait," you say. "Way back up there in Scope you told me functions run to completion and afterwards their function scope is destroyed." I did say that. I also said this rule can be broken.
Closures
Let's pause for a moment to consider the scopes both implicit and created when we wrote the incrementer
function. First there is the global scope known as window in the browser. The incrementer
function creates its own scope and within it defines the i
variable, increment
and decrement
functions. The increment
and decrement
functions in turn have their own scopes.
As we learned earlier the increment
and decrement
functions can access their own scope and all of their ancestor scopes, incrementer
and global
in this case. incrementer
in turn can access its ancestor scope, the global scope. When we invoke incrementer
the environment creates a new function scope for incremeter
, the value 2 is assigned to i
the increment
and decrement
functions are created and then returned. But this time the function scope created by invoking incrementer
is not destroyed.
If we return a reference to the function scope or "lexical environment" of a function that scope is preserved and can continure to be referenced during the lifecycle of our program. That reference is called a "closure". It is "...the combination of a function and the lexical environment within which that function was declared."6
Believe it or not this is probably something you've seen and perhaps done many times in the past without knowing there's a name for it. If you've worked in what now seems like the old-timey web and wrote a browser based program then you've most certainly worked with the revealing module pattern.
(function (win) {
// ...plugin code
return function init () {}
}, (window))
This pattern keeps the global scope clean and free of conflicts with other browser programs while exposing a public interface. This is a closure.
On the other hand you may be more familiar with more current coding examples like this definition of add.
const add = x => y => x + y;
If we read this definition of add
carefully then we'll see that add
is a function that takes an argument named x
and returns a new function. The new function takes an argument named y
which returns the sum of x
and y
.
const add2 = add(2);
add2(5) // 7
Here we invoke add
with the value 2. add
returns a new function that wants an argument y
and that function is assigned to the variable add2
. When add2
is in turn invoked with 5 the final part of the add
function is called and 2 + 5 = 7.
Chaining, sort of
The syntax of our last definition of add
necessarily creates and odd sort of syntax. If for instance we wanted to add 2 and 3 we'd either have to write a lot of code or use more parenthesis.
const add = x => y => x + y;
const add2 = add(2);
add2(3); // a lot of work for 5
// or
add(2)(3) // weird!
However if you're familiar with dot chaining this isn't so odd.
[2,1,3].map((n) => add(n, 2)).sort();
In this code block we add 2 to each value in an array and then sort it. We may take it for granted that .map
and .sort
will work but they work because each method invocation returns an Array
and Array
s have both the map
and sort
methods. Dot chaining may in turn break if we call a method that returns an unexpected type.
[2,1,3].join().sort();
// Uncaught TypeError: [2, 1, 3].join().sort is not a function
This code fails because join returns a String
and String
does not have a sort
method. Our code would not break however if we called a valid String
method.
[2,1,3].join().toLocaleUpperCase(); // please enjoy a capitalized string
We could call our chained code like this.
const arr = [2,1,3];
const summedArr = arr.map((n) => add(n, 2));
const sorted = summedArr.sort()
We don't write our code that way because it's long and the interrim steps of assigning values to variables is unnecessary. Our last add
code from above (repeated here) is no different.
const add = x => y => x + y;
const add2 = add(2);
add2(3); // a lot of work for 5
// or
add(2)(3) // weird!
We can go through the extra steps of assigning interrim states to variables but it's not necessary. If we know a function will return a function we can call it immediately. And like trying to chain sort
to a String
if we get it wrong we'll get a type error.
const addOne = x => x + 1
addOne(2)(3) // Uncaught TypeError: addOne(...) is not a function
The moral of this story is, don't fear parenthesis.
And a Whole Lot More…
Closure or the ability of a function to retain a reference to the lexical environment in which it was declared is in my opinion the key to understanding the functional nature of Javascript. By "functional nature" I don't mean its ability to work. I mean the "Functional Programming" paradigm.
That is perhaps a longer conversation for another time.