What the heck are Higher Order Functions, Callbacks and Closure in JavaScript?
Higher Order Functions, Callbacks and Closure are the topics that overwhelm and confuses every budding JavaScript Developer.
But at the same time these three are most esoteric of JavaScript concepts. In this article I have tried to explain Higher Order Functions, Callbacks and Closure in the simplest language.
Let's begin!
First things first, when JavaScript code runs, it:
- Goes through the code line-by-line and executes each line - known as thread of execution
- Saves ‘data’ like strings and arrays so we can use that data later - in its memory We can even save code (‘functions’)
Functions
Code we save (‘define’) functions & can use (call/invoke/execute/run) later with the function’s name & ( )
Execution Context
Created to run the code of a function - has 2 parts (we’ve already seen them!)
- Thread of execution
- Memory
const num = 3;
function multiplyBy2 (inputNumber){
const result = inputNumber*2;
return result;
}
const output = multiplyBy2(num);
const newOutput = multiplyBy2(10);
Call Stack
- JavaScript keeps track of what function is currently running (where’s the thread of execution)
- Run a function - add to call stack
- Finish running the function - JS removes it from call stack
- Whatever is top of the call stack - that’s the function we’re currently running
Why do we even have functions?
Let’s see why… Create a function 10 squared
- Takes no input
- Returns 10*10 What is the syntax (the exact code we type)?
function tenSquared() {
return 10*10;
}
tenSquared() // 100
What about a 9 squared function?
function nineSquared() {
return 9*9;
}
nineSquared() // 81
What principle are we breaking?
DRY (Don’t Repeat Yourself)
We can generalize the function to make it reusable
function squareNum(num){
return num*num;
}
squareNum(10); // 100
squareNum(9); // 81
squareNum(8); // 64
Generalized Functions
‘Parameters’ (placeholders) mean we don’t need to decide what data to run our functionality on until we run the function
- Then provide an actual value (‘argument’) when we run the function
Higher order functions follow this same principle.
- We may not want to decide exactly what some of our functionality is until we run our function
Now suppose we have a function copyArrayAndMultiplyBy2
function copyArrayAndMultiplyBy2(array) {
const output = [];
for (let i = 0; i < array.length; i++) {
output.push(array[i] * 2);
}
return output;
}
const myArray = [1,2,3];
const result = copyArrayAndMultiplyBy2(myArray);
But what if want to copy array and divide by 2?
we would be needed to write the above code all over again with just 1 character different.
What principle are we breaking again?
DRY (Don’t Repeat Yourself)
How is it possible to create higher order functions in JS
Functions in javascript = first class objects They can co-exist with and can be treated like any other javascript object
- Assigned to variables and properties of other objects
- Passed as arguments into functions
- Returned as values from functions
So based on this our modified code will become -
function copyArrayAndManipulate(array, instructions) {
const output = [];
for (let i = 0; i < array.length; i++) {
output.push(instructions(array[i]));
}
return output;
}
function multiplyBy2(input) {return input * 2;}
const result = copyArrayAndManipulate([1, 2, 3], multiplyBy2);
The outer function that takes in a function is our higher-order function
Arrow functions
A shorthand way to save functions
function multiplyby2(input) {return input*2;}
const multiplyby2 = (input) => {return input*2;}
const multiplyby2 = (input) => input*2
const multiplyBy2 = input => input*2
All the above syntax are equivalent.
Updating our callback function as an arrow function
function copyArrayAndManipulate(array, instructions) {
const output = [];
for (let i = 0; i < array.length; i++) {
output.push(instructions(array[i]));
}
return output;
}
//const multiplyBy2 = input => input*2
//const result = copyArrayAndManipulate([1, 2, 3], multiplyBy2);
const result = copyArrayAndManipulate([1, 2, 3], input => input*2);
Closure
Enables powerful pro-level functions like ‘once’ and ‘memoize’.
Functions with previous run memory.
But we read above, Functions get a new memory every run/invocation
Functions with memories
- When our functions get called, we create a live store of data (local memory/variable environment/state) for that function’s execution context.
- When the function finishes executing, its local memory is deleted (except the returned value)
- But what if our functions could hold on to live data between executions?
- This would let our function definitions have an associated cache/persistent memory
- But it all starts with us returning a function from another function
Functions can be returned from other functions in JavaScript
function createFunction() {
function multiplyBy2 (num){
return num*2;
}
return multiplyBy2;
}
const generatedFunc = createFunction();
const result = generatedFunc(3);
By seeing this function it might look like generatedFunc is just another lable to the createFunction.
But from 7th line onwards it has no realtionship with createFunction.
generatedFunc in the simplest terms, is result of running createFunction one time.
Calling a function in the same function call as it was defined
function outer (){
let counter = 0;
function incrementCounter (){
counter ++;
}
incrementCounter();
}
outer();
when the incrementCounter doesn't find the counter variable in its execution context, it looks in the memory of outer and increments counter from there.
But how this happens?
- does this happens because we saved incrementCounter function definition in outer function?
- or it happens because we created execution context of incrementCounter inside the execution context of outer?
Calling a function outside of the function call in which it was defined
function outer (){
let counter = 0;
function incrementCounter (){ counter ++; }
return incrementCounter;
}
const myNewFunction = outer();
myNewFunction();
myNewFunction();
- we search for counter variable to increment in local memory of incrementCounter, didn't find it there.
- incrementCounter is running inside the global execution context, so we search for counter variable there, didn't find it again.
Turns out, while we returned incrementCounter function, we returned something more than just the code inside the function. It took with the all the surrounding data from where it was born.
When a function is defined, it gets a bond to the surrounding Local Memory (“Variable Environment”) in which it has been defined
this surrounding Local Memory is aka
- Closed over ‘Variable Environment’ (C.O.V.E.)
- Persistent Lexical Scope Referenced Data (P.L.S.R.D.)
The closure’ of live data is attached incrementCounter (then to myNewFunction) through a hidden property known as [[scope]] which persists when the inner function is returned out.
Thank you for reading so far. Let me know in the comments which JavaScript concept I should write about next.