Come with me now on a journey through code and data...

Scoping Things Out

I'm working my way through the points in this list of 33 fundamentals every JavaScript developer should know in order to make sure I understand the language thoroughly. This notebook is for working through code relating to point 6.

Scope and Context

  • When we talk about scope we are talking about the variables a piece of code has access to at runtime.
  • When we talk about context we are talking about the value of this and all of it's properties.

var is bananas

Variables defined in the default or root scope are accessible globally, and variables defined within functions are only accessible within that function.

In [1]:
var a = 'apples'

function fruitVendor() {
    var b = 'bananas'
    console.log('We are inside the scope of the fruitVendor function:')
    console.log('> a is for ' + a)
    console.log('> b is for ' + b)
    console.log('\n')
}

fruitVendor()

console.log('We are at the default or root scope level:')
console.log('> a is for ' + a)
console.log('> b is for ' + b)
We are inside the scope of the fruitVendor function:
> a is for apples
> b is for bananas


We are at the default or root scope level:
> a is for apples
evalmachine.<anonymous>:15
console.log('> b is for ' + b)
                            ^

ReferenceError: b is not defined
    at evalmachine.<anonymous>:15:29
    at Script.runInThisContext (vm.js:96:20)
    at Object.runInThisContext (vm.js:303:38)
    at run ([eval]:1054:15)
    at onRunRequest ([eval]:888:18)
    at onMessage ([eval]:848:13)
    at process.emit (events.js:182:13)
    at emit (internal/child_process.js:812:12)
    at process._tickCallback (internal/process/next_tick.js:63:19)

We get scope conflict when there is a variable of the same name in the parent and child scope.

In [2]:
var a = 'apples'

function fruitVendor() {
    var a = 'avocados'
    console.log('We are inside the scope of the fruitVendor function:')
    console.log('> a is for ' + a)
    console.log('\n')
}

fruitVendor()

console.log('We are at the default or root scope level:')
console.log('> a is for ' + a)
We are inside the scope of the fruitVendor function:
> a is for avocados


We are at the default or root scope level:
> a is for apples

Although the code runs, the problem is that we no longer have access to the parent variable of the same name due to naming conflicts.

This is not really new information. But we are going somewhere with this which is JS specific so bear with me.

Above, we looked at function scope, but what about block scope? Is there much difference here when we try to define variables inside if, for, or while blocks of code?

In [3]:
if (true) {
    var c = 'clementine'
}

console.log('> c is for ' + c)
> c is for clementine

var values can be accessed outside of the scope of the block in which they were defined. This is considered by some to be inconsistent and confusing, particularly because you might feel that scope could be imagined as being encapsulated in curly braces. Lack of block scope isn't something you'd be used to if coming from many other programming languages. So in ES6 a new pair of definitions were introduced....

let and const

In [4]:
let d = 'durians'

if (true) {
    let e = 'elderberries'
    console.log('We are inside the scope of the if block:')
    console.log('> d is for ' + d)
    console.log('> e is for ' + e)
    console.log('\n')
}

console.log('We are at the default or root scope level:')
console.log('> d is for ' + d)
console.log('> e is for ' + e)
We are inside the scope of the if block:
> d is for durians
> e is for elderberries


We are at the default or root scope level:
> d is for durians
evalmachine.<anonymous>:13
console.log('> e is for ' + e)
                            ^

ReferenceError: e is not defined
    at evalmachine.<anonymous>:13:29
    at Script.runInThisContext (vm.js:96:20)
    at Object.runInThisContext (vm.js:303:38)
    at run ([eval]:1054:15)
    at onRunRequest ([eval]:888:18)
    at onMessage ([eval]:848:13)
    at process.emit (events.js:182:13)
    at emit (internal/child_process.js:812:12)
    at process._tickCallback (internal/process/next_tick.js:63:19)

Now block scope is operating the same as function scope and we can all sleep soundly at night. Hooray.

Getting Loopy

So we know that let and var operate differently in terms of scope. What happens if we use var when adding lazy loaded functions to an array, executing them at some later time?

In [5]:
var toBeExecuted = []

for (var i=0; i<3; i++) {  
  toBeExecuted.push(() => console.log(i))
}

toBeExecuted.forEach(lazyFn => lazyFn())
3
3
3

What happened? Instead of creating a local variable i for each increment in the loop, it ended up printing the final value for that variable for all function calls.

It feels unexpected and although there are work-arounds for still using a var they can be a little verbose.

Enter the let keyword:

In [6]:
let letItBeExecuted = []

for (let i=0; i<3; i++) {  
  letItBeExecuted.push(() => console.log(i))
}

letItBeExecuted.forEach(lazyFn => lazyFn())
0
1
2

Magic ✨

A Warning

It's important to note that JS still compiles when you create a new variable, even if you don't specify var, const, or let. When you do so, it first searches the current scope for a variable of that name, then trickles up through parents layers.

⚠️ If it doesn't find it all the way up to the root, it will create the variable there in the root layer, also known as a global variable. This is known as "polluting the global scope".

To avoid this place the string "use strict" at the top of the entry point file, causing a ReferenceError message if this is attempted. This trick solves a whole host of other problems as well.

In [7]:
"use strict"
function badFunction() {
    w = 'watermelon'
}

console.log(w)
evalmachine.<anonymous>:6
console.log(w)
            ^

ReferenceError: w is not defined
    at evalmachine.<anonymous>:6:13
    at Script.runInThisContext (vm.js:96:20)
    at Object.runInThisContext (vm.js:303:38)
    at run ([eval]:1054:15)
    at onRunRequest ([eval]:888:18)
    at onMessage ([eval]:848:13)
    at process.emit (events.js:182:13)
    at emit (internal/child_process.js:812:12)
    at process._tickCallback (internal/process/next_tick.js:63:19)

Let's stop here for now. Next time we will dive deeper into what lexical scope really means. We'll get into closures and hoisting, and even explore the differece between dynamic and static scoping to really understand what we're working with.

'til next time 👋