Javascript's Scope and Closures, What's the Difference.

·

5 min read

When I started learning about Javascript's scope and closures, it was hard to tell the difference. They would use scope to explain closures and it just seemed so confusing. In this blog, I hope to tackle this very confusing topic and make it easier to finally get it! It was one of my favorite things to learn about Javascript. So let's get started!

Let's talk about what scope is in Javascript.

let name = 'Mark'
function one(){
  let nameInFunc = 'Sally'
  console.log('nameInFunc', nameInFunc)
  console.log('name', name)
}
one()

/*
output: 
nameInFunc Sally
name Mark
*/

In the code above, we have declared a variable called name at the global level. We created a function and declared another variable inside the function called nameInFunc. When we invoked the function, it printed out the value of the name and nameInFunc. Function one has access to the name variable because it's being declared at the global level.

(input analogy)

However, if we try to access nameInFunc outside the function you will get an error. This is because the variable was declared within a range (scope), that range being the function.

let name = 'Mark'
function one(){
  let nameInFunc = 'Sally'
  console.log('nameInFunc', nameInFunc)
  console.log('name', name)
}
one()
console.log(nameInFunc)
//output: nameInFunc is not defined

Anytime you declare a variable within a set of curly brackets you're limiting its access within a scope where the variable can only be used within those curly brackets. This rule applies when you use let and const, using var will do some whacky stuff and you never want to use it.

Let's run this code in the console of the Chrome browser.

let nameInGlobal = 'Sally'
function one(){
    let nameInOne = 'Mark'
    console.log('nameInOne:', nameInOne)
    if(nameInOne === 'Mark'){
        let nameInCondition = 'Will'
        console.log('nameInGlobal:', nameInGlobal)
        console.log('nameInOne:', nameInOne)
        console.log('nameInCondition:', nameInCondition)
    }
    console.log('nameInCondition', nameInCondition)
}
one()

If you take a look at the image above you'll see the red, blue and yellow highlights. Each variable is within a certain scope and can only be accessed depending on where they are being defined. For instance, on the right side of the picture, nameInCondition's scope is under the block scope, nameInOne is under the local scope, and nameInGlobal is under the script scope. Depending on how you look at things, if you see from the debugger's point of view, starting from the top, you have the block code that can access variables in the local scope and script scope, you have the local scope that can only access variables in the script scope.

This is because we have declared the nameInCondition variable inside of a block of code to which only the code inside it has access to it. This is what scope is. The variables that are declared within a block of code cannot be used outside of it. In this case, nameInCondition cannot be used outside the if statement.

Now, this is where it gets interesting! Let's talk about the lexical environment and closures. Take a look at the example below:

function one(){
    let nameInOne = 'Mark'
    console.log('nameInOne:', nameInOne)
    return function two(){
        let nameInTwo = 'Will'
        console.log('nameInOne:', nameInOne)
        console.log('nameInTwo:', nameInTwo)
    }
}
one()
/*
Output: 
nameInOne: Mark
ƒ two(){
        let nameInTwo = 'Will'
        console.log('nameInOne:', nameInOne)
        console.log('nameInTwo:', nameInTwo)
    }
*/

When we call the function one, it prints nameInOne: Mark. The environment between function one and two is the lexical environment and the relationship between them is the closure they have. The variables that are declared in function one are enclosed and can only be used by function two and any other nested functions within it. Think about it this way, say the person you are close to knows your personality and characteristics but the rest of the world doesn't(The rest of the world being all the other functions, their variables, and global variable declarations). So they have a closeness with you that other people don't have. Your personality and characteristics represent the variables and instructions that are written in function one and the person you are closest to represents the nested function. Function one (you) has a closeness to function two (your person)that other functions don't and that is what closure is. It is the relationship between two functions where one function is enclosed in another and the nested function will have access to the parent function's declared variables. The biggest difference between scope and closure is the access they have to the parent function. Using closure, the nested function will have access to its parent function's variables even after the parent function is removed from the call stack. When referring to the scope, once the parent function is removed from the call stack those variables are no longer accessible to the block of code.

function one(){
    let nameInOne = 'Mark'
    console.log('nameInOne:', nameInOne)
    return function two(){
        let nameInTwo = 'Will'
        console.log('nameInOne:', nameInOne)
        console.log('nameInTwo:', nameInTwo)
    }
}
const callTwo = one()
/*
Output: 
nameInOne: Mark
*/
callTwo()
/*
Output:
nameInOne: Mark
nameInTwo: Will
*/

In the code above, after the function one was invoked it returns the nested function and it is removed from the call stack. When we invoke the callTwo function, it still has access to the variables declared in function one. And that is the difference between closures and the scope.

Let's wrap things up, the scope is when a variable is declared outside a block of code and you can use its value inside the code block. The more nested the code block is, any variables declared in it won't be accessible outside that code block. The lexical environment is the environment between a parent function and its child function. The relationship is a closure one, where anything declared between the parent function and its child function is enclosed. I hope that was helpful!