JavaScript Closures
A Closure is a combination of function and lexical environment where this function was declared. The environment may consist of both local variables declared in the body of a function and global ones declared outside of the function. To put it in an equation, Closure equals function plus lexical environment (for instance, local variables, global variables, outer functions) within which that function was declared.
If you keep your mind open and have no prejudices, at the end of this article we’ll grasp the idea and practical implementation of closures.
The below example shows that the inner anonymous function has access to the local variable double out of its scope (lexical scope). We bind this function to different variables outside of this scope and each time we call them, we can pass different values as arguments. As soon as the anonymous function has access to the inner variable double, it can now take both this variable value and passed the argument and make simple calculations (val * double).
function simpleDoubler(){
let double = 2; // local variable of lexical environment
return function (val){
return val * double;
}
}let doubleFour = simpleDoubler();
let doubleTen = simpleDoubler();
console.log(doubleFour(4)); // 8
console.log(doubleTen(10)); // 20
Each time, when we console log simpleDoubler function and pass the desired integer to be doubled, it performs these operations written in the body of the function. Another implementation to consider:
let colorChanger = (function() {
return function change(color) {
console.log({color});
}})();let red = colorChanger('red');
let green = colorChanger('green');
let orange = colorChanger('orange');
In the example above we created a function named colorChanger which returns another function that we can pass an argument into. Our inner function logs in the console an object with object property shorthand syntactic sugar (a feature available with ES6/ES2015) where, if the key and value have the same value (for instance, {color: color}), we can avoid duplication by simply passing the name of value ({color}). An important thing to notice here is that this is IIFE (Immediately Invoked Function Expression) which runs as soon as it’s declared, by passing both functions into parentheses and invoking them right after it (again, with parentheses), thus being invoked right after it was created.
let country = (() => {
return (name) => {
return (capital) => {
return (language) => {
return (population) => {
console.log(`country: ${name}, capital: ${capital}, language: ${language}, population: ${population}`);
if (population < 3000000) {
console.log('small population');
} else if (population >= 3000000 && population <= 100000000) {
console.log('mid-size population');
} else {
console.log('large population');
}
};
};
};
};
})();let malta = country('Malta')('Valletta')('Maltese')(514564);
let france = country('France')('Paris')('French')(65273511);
let usa = country('USA')('Washington')('English')(331002651);
The example above is a bit more complicated. There is a whole hierarchy of nested functions, but we’ll be ok as soon as we remember the most important thing in Closures, that the deeper the scope is the more it has access to the outer scopes. This means that the anonymous arrow function that takes the population as an argument has access to all arguments passed in the outer functions before (language, then capital, then name). After writing the country function we need to bind it to different variables like malta, france, and usa. The argument sequence that we pass into country function is also important. First, in parentheses, we pass string Malta, then string Valletta, then string Maltese, then integer 514564 (which is an actual population of Malta island at the time this article is written). They represent the name, capital, language, and population placeholders, respectively (this is why the sequence is important). The if statement in the code may seem fully optional at first glance since it provides an additional sorting by population size, but if we only try to cut and paste this condition anywhere outside its present scope (the core, the deepest scope), it will give as an error (Uncaught ReferenceError: population is not defined), complaining that the population is not visible while it is referenced outside but was created inside! This proves the concept of the Closures that only the inner scope has access to all the data (variables, arguments) of the lexical environment and not the other way round!
let country = (() => {
if (population < 3000000) {
console.log('small population');
} else if (population >= 3000000 && population <= 100000000) {
console.log('mid-size population');
} else {
console.log('large population');
}
return (name) => {
return (capital) => {
return (language) => {
return (population) => {
console.log(`country: ${name}, capital: ${capital}, language: ${language}, population: ${population}`);
};
};
};
};
})();let malta = country('Malta')('Valletta')('Maltese')(514564);
let france = country('France')('Paris')('French')(65273511);
let usa = country('USA')('Washington')('English')(331002651);
// Uncaught ReferenceError: population is not defined
Conclusion
Although many JavaScript developers use Closures instinctively or based on experience, it would be better if we understand the concept to be more consequent, reasonable, and confident. The deeper scope has access to a more superficial one, or the inner has access to the outer. From now on, as you already went through one of the hardest parts in JavaScript, nothing should hold you down in your journey to uncover all the secrets and beauty of this scripting language and get the most out of one of its quirky parts called Closures.