Skip to content

Instantly share code, notes, and snippets.

@AloofBuddha
Last active September 18, 2018 18:55
Show Gist options
  • Save AloofBuddha/74ed9a2724afa4e79552bb4d1a808113 to your computer and use it in GitHub Desktop.
Save AloofBuddha/74ed9a2724afa4e79552bb4d1a808113 to your computer and use it in GitHub Desktop.
Closure tutorial

Global Scope, Inner Scope and Closures

This tutorial aims to cover the topics of

  • Global Scope
  • Inner Scope (or sometimes called Function Scope)
  • and Closures

in the JavaScript language.


Prerequisites

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!


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>

Inner Scope

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?

The Problem with Global 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?

Closures

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.

Closures - Extra Credit

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment