Day 2: Hoisting Solved

The first time you encounter hoisting in JavaScript it is usually by accidentally calling a function that you haven't declared yet:

dogs()
function dogs() {
  console.log('dogs')
}
Try in REPL

Your first reaction is wat?! Your second reaction is to remember that this is why you only use the good parts of JS. You will forget about this weird oddity of JS until you commit the error of modifying a global in a local scope:

var awesome ='Cats are awesome'

function cats() {
  awesome = 'Dogs are awesome'

  console.log(awesome)     
}

console.log(awesome) // => Cats are awesome
cats()
awesome // => Dogs are awesome
Try in REPL

You think, hmm that is a bit annoying, but who cares! I'll just be more careful about globals! No big deal. Then months later you will make a head scratching mistake:

function cats() {
  for (var i = 0; i < 5; ++i) {
    setTimeout(function () {
      console.log(i) // => 5 will be outputted five times
    }, 100)
  }
}
cats()
Try in REPL

Now the real frustration begins. What the heck is happening here?! Welcome to the world of JavaScript hoisting! There are two things happening here;

  1. The loop finishes before any setTimeouts are called.

  2. The variable

    i

    is hoisted into the outer scope, into "cats".

The setTimeout function is using the value of i after the loop completes. In most languages we are used to block scoping, but JavaScript has no block scoping. JavaScript is moving the var i out of the loop, hoisting it into the local scope.

What the for loop really looks like is this:

function cats() {
  var i =  0
  for (; i < 5; ++i) {
    setTimeout(function () {
      console.log(i) // => 5 will be outputted five times
    }, 100)
  }
}

The rules for hoisting are actually simple once you get it. An example should help make them clear.

Let's look at what happens if we define a variable after using it:

console.log(cats) // => undefined
 var cats = 'cats are awesome'
Try in REPL

The important thing to know here is that undefined in JS is not the same as doesn't exist. The console.log call is using the variable cats, which exists, it is just undefined.

The variable cats is hoisted to the top of the scope so that it looks like this:

var cats
console.log(cats)

The reason this is non-intuitive is because our brains think that if the var was hoisted then the value should be too. We should get /"cats are awesome"/ as our output but we don't. The secret here is that JS does not hoist assignments. JS treats a declaration and an assignment as two separate statements. In the following code:

console.log(cats) // => undefined
var cats = 'cats are awesome'

The declaration is hoisted to the top of the scope but not the value. The end result is something like:

var cats
console.log(cats) // => undefined
cats = 'cats are awesome'

let To the Rescue

This aspect of hoisting can be unintuitive and frustrating and the cause of a whole hoist of bugs. let was introduced in ES6 to help fix this. Before we look at let, we need to understand how to solve our loop issue without it.

We can solve the issue by adding some scoping. All we need to do is pass the variable i into a function:

function runSetTimeout(i) {
  setTimeout(function() {
    console.log(i)
  }, 100);
}

for (var i = 0; i < 5; ++i) {
  runSetTimeout(i)
}
Try in REPL

Every time we run through the loop we call a function that wraps up the setTimeout and passes in i as the value.

We can solve the same issue using let:

for (let i = 0; i < 5; ++i) {
  setTimeout(function () {
    console.log(i)
  }, 100)
}
Try in REPL

This ES6 code compiles to very similar code as our ES5 solution:

var _loop = function _loop(i) {
  setTimeout(function () {
    console.log(i);
  }, 100);
};

for (var i = 0; i < 5; ++i) {
  _loop(i);
}

ES6 is usable in ES5 environments because ES6 features have counterparts in ES5. All ES6 does is make the code easier to reason about.

const vs let

const is just like let except you use it when the value of your variable isn't going to re-assigned. It is good practice to use const by default and only switch to let when you need to. This reduces the likelihood you will introduce errors into your code. Wherever you would normally use var use const and then switch to let when const gives you problems.

It looks like this:

const cats = 'are awesome'
let dangerousCats = 'Sprinkles'
dangerousCats = ['tigers', 'jaguars', 'house cats']

ES6 Arrows

ES6 makes functions a lot nicer. You can now create functions that look like this:

const cats = () => {
  return 'are awesome'
}

When there is only one line you get implicit returns. It looks like this:

const cats = () => "are awesome"

this made sane

The biggest benefit of arrow functions is not their nice syntax but how they help us wrangle this. Arrow functions share the surrounding scope with the parent context. This means that you don't need to create a reference to this when using an arrow function inside an object.

Let's say for example we had a cat and this cat needs to talk. In ES5 we would do the following:

function Cat(name) {
  var self = this // store a reference to this
  this.name = name

  setTimeout(function talk() {
    console.log("Meow! My name is " + self.name)
  }, 1000)
}

var cat = new Cat('Ferguson')
Try in REPL

In ES6 we can do the following instead:

function Cat(name) {
  this.name = name

  setTimeout(() => {
    console.log("Meow! My name is " + this.name)
  }, 1000)
}

const cat = new Cat('Ferguson')
Try in REPL

Don't forget arrows have different scope

There is one gotcha with arrow functions, in babel they will remap this to undefined when there is no this. There are two ways to encounter it; Firstly, if you try to use the window or global object as this:

const windowIsNotThis = () => {
  console.log(this === window) // => false
}
windowIsNotThis()

That will log false because this has been remapped to undefined.

The second gotcha you can run into is when you try to use fat arrows on an object:


const cat = {
  name: 'Awesome Cat',
  talk: () => {
    return `My name is ${this.name}`
  }
}

cat.talk() // => Cannot read property 'name' of undefined
Try in REPL

This errors because fat arrows use the this from the outer scope. A talk method that worked would look like this:

const cat = {
  name: 'Awesome Cat',
  talk: function() {
    return (() => {
      return `My name is ${this.name}`
    })()
  }
}

cat.talk()
Try in REPL

But instead of this self-invoking nonsense we can simply use an object shorthand:

const cat = {
  name: 'Awesome Cat',
  talk() {
    return `My name is ${this.name}`
  }
}

cat.talk()
Try in REPL

That is all there is to ES6 arrows! Onto day three!