This tutorial aims to cover the topics of
- Global Scope
- Inner Scope (or sometimes called Function Scope)
- and Closures
in the JavaScript language.
This tutorial expects you to be familiar with a few things off the bat. Review these concepts if anything is unfamiliar!
// creates a new variable 'counter' and set it's initial value to 0;
let counter = 0;
// creates a new function 'addOne' which takes 0 parameters
function addOne() {
// adds one to counter. Equivalent to counter = counter + 1;
counter++;
}
// calling a function. This executes the body of the named function.
addOne();
// prints our result to the console
console.log(counter);
Review: the prerequisite concepts for this tutorial are
- creating and initializing a variable
- creating a function
- invoking a function
- logging to the console
If you feel comfortable with all of these topics we can move on to the first topic - Global Scope!
Scope in JavaScript works like it does in most languages. If something is defined at the root level, that's global scope - we can access (and modify) that variable from anywhere. If it's a function we defined, we can call it from anywhere.
let counter = 0;
function addOne() {
counter++;
}
console.log(counter); // <Fill in Expected Result>
counter++;
console.log(counter); // <Fill in Expected Result>
addOne();
console.log(counter); // <Fill in Expected Result>
OK, this seems to work as expected, however
What about inside of our
addOne()
function?
Every function creates it's own local scope. Compared to it's context (i.e. where our function is defined), we call this the inner scope. Our function can access/modify anything outside of it's scope, so the body of our function, { counter++; }
, has an effect that persists in the outside scope.
What about the other way around? Can global scope modify inner scope?
function addOne() {
// this time, we define counter within our function
let counter = 0;
counter++;
}
// what do we expect to happen here?
console.log(counter); // <Fill in Expected Result>
// should this work? why/why not?
counter++;
console.log(counter); // <Fill in Expected Result>
addOne();
console.log(counter); // <Fill in Expected Result>
What's the issue here exactly?
Because counter
is defined within our function's scope, it doesn't exist within the global scope, so referencing it there doesn't make sense.
Review: Can you give an informal definition for global scope? What about inner scope?
So it seems we should declare all of our variables at the global scope.
Why could this be a problem?
let counter = 0;
function addOne() {
counter++;
}
// I want addOne() to be the only way to access/alter the 'counter', but that's not the case.
addOne();
addOne();
addOne();
counter = -Infinity;
console.log(counter); // <Fill in Expected Result>
It seems reasonable to want counter
to only be accessed/modified through our addOne()
function, but if our variable is defined within the global scope, we can't enforce this.
This may not seem like a major concern - we can just make sure we don't access it directly. However as a codebase grows, especially with more than one programmer, it becomes hard to enforce such rules simply by convention. What we want is to have some form of encapsulation - i.e. the data our function relies on is completely contained within the logic of that function - it's the only door in or out!
// I want addOne() to be the only way to access/alter the 'counter'
function addOne() {
let counter = 0;
counter++;
return counter;
}
let result1 = addOne();
let result2 = addOne();
let result3 = addOne();
console.log(result1); // <Fill in Expected Result>
console.log(result2); // <Fill in Expected Result>
console.log(result3); // <Fill in Expected Result>
What's the issue with the above example? How can we explain the output?
Can you think of a way around this problem that doesn't fall back to the previous example?
There is an answer! Using a closure! What's a closure exactly? it's a word we use to refer to the context of a given function. Normally our function starts from scratch every time we run it. However, if we were to return a function from addOne()
that referenced counter
, counter would become part of the context of that new function, even after addOne()
finishes executing. This is easier to see in code than to explain in words:
function createAdder() {
let counter = 0;
return function () {
counter++;
return counter;
}
}
let addOne = createAdder();
let result1 = addOne();
let result2 = addOne();
let result3 = addOne();
console.log(result1); // <Fill in Expected Result>
console.log(result2); // <Fill in Expected Result>
console.log(result3); // <Fill in Expected Result>
This works! we only reinstantiate counter when createAdder()
is called, but it's value gets updated whenever the function it returns is called.
We say that this inner function is closed around the variable
counter
Definition: (According to MDN) A closure is the combination of a function and the lexical environment within which that function was declared.
Here's something to ponder - what do we think happens in the following example? Is counter
shared for all adders, or do they all contain a unique version?
function createAdder() {
let counter = 0;
return function () {
counter++;
return counter;
}
}
let addOne1 = createAdder();
let addOne2 = createAdder();
let result1 = addOne1();
let result2 = addOne1();
let result3 = addOne1();
let result4 = addOne2();
let result5 = addOne2();
let result6 = addOne2();
console.log(result1); // 1
console.log(result2); // 2
console.log(result3); // 3, as expected!
console.log(result4); // <Fill in Expected Result>
console.log(result5); // <Fill in Expected Result>
console.log(result6); // <Fill in Expected Result>
do we expect these last 3 to be 1, 2, 3 or 4, 5, 6 (or something else?!) Why?