Contrasting var and ES6s new declarations: let and const

let and const are two new declaration types in JavaScript, which have the ability to completely replace var declaration. To use them effectively, and understand why they can be a direct replacement, it's important to understand how they work.

var is function scoped. It is hoisted and initialized at the top

var, regardless of where it is assigned, will become declared and initialized at the beginning of the function. This means that any variable you declare will be available everywhere inside the function. This process is referred to as hoisting.

Hoisted variables with var are declared and initialized at the top of the function, but have no value

All variables that are declared and initialized with var are initialized as undefined. They have no value until you reach the line of code that assigns it a value.

const undefinedConsoleLog = _ => {
  console.log(sadlyUndefined) //undefined
  var sadlyUndefined = 'declared by var, undefined where I console log'
  var isDefined = 'value assigned before console log'
  console.log(isDefined) //'value assigned before console log'
}

let and const is block scoped

This means that variable declaration [appears] to not be hoisted. A big win. But this big win requires you to know exactly what a "block" is, or you will get into trouble in places that seem counterintuitive.

const failedResponse = _ => {
  if ('this is a block'){
    let response = 'yes, this is a block'
    console.log(response) //'yes, this is a block'
  }
  console.log(response) //throws an error
}

Why? Because my if statement is considered a block. response is only available inside my if statement. Anything with nested curly braces is considered a block.

How do I fix my failedResponse function scoping issue?

Easy. Change where the variable is declared. A scope is itself and every nested block it contains.

const fixedResponse = _ => {
  let response //declared, and then initialized as undefined
  console.log(response) //undefined
  if ('this is a block') {
    response = 'yes, this is a block'
    console.log(response) //'yes, this is a block'
  }
  console.log(response) //'yes, this is a block'
}

Declaration and initialization are distinct concepts in let and const

I keep using a word you probably have not seen when people talk about hoisting: initialization. Declaration and initialization are not the same. Variables are always declared at the top. But let and const, they are not initialized (unless they are already at the top).

This is how let and const become block scoped. Allocation of memory (hoisting) always occurs. It is fundamental to the JavaScript engine and will never go away. But the initialization process is different. You cannot use variables that are not initialized. var is initialized as undefined, let and const throw an error because they are not initialized.

const hoistedAndInitialized = _ => {
  console.log(initializedVar) //undefined
  var initializedVar = 'declared and initialized as undefined on top'
}

const hoistedButNotInitialized = _ => {
  console.log(uninitializedLet) //ReferenceError: uninitializedLet is not defined
  let uninitializedLet = 'declared at the top, but not initialized. will throw error'
}

Replacing var in any situation

Function and block scoping are fundamentally different, but there's nothing stopping you from making the declared block as the entire function.

const alwaysFunctionScoped = _ => {
  if ('I use var') {
    var text = 'I am available anywhere in the function'
  }
  console.log(text) //'I am available anywhere in the function'
}

const alsoFunctionScoped = _ => {
  let text
  if ('I use let') {
    text = 'I have already been hoisted and initialized.
    function scope === block scope'
  }
  console.log(text) //'I have already been hoisted and initialized.
  // function scope === block scope'
}

But be careful with let and const...

If I declare text inside my if statement, they are now different variables. They might as well be completely different variable names.

const innerTextIsNowBlockScopedAgain = _ => {
  let text
  if ('I declare with let inside this block') {
    let text = 'exists inside this inner scope only.'
  }
  console.log(text) //undefined
}

Comments

Popular posts from this blog

Modularity vs. Abstraction. Which is More Important?

My dogmatic thoughts on TypeScript

Unit testable components pt.4: Testing the Redux data flow