let and const in javascript
Tags: javascript, react, angular, webdev
Published: 2020-05-25 00:19:04

*Note: This is part of my es6 series of posts.*

The `let` keyword declares a variable that is strictly scoped to the current block, statement, or expression where it is defined. This is in contrast to `var` declarations which are scoped to the current function. An additional difference with `var` is that `let` variables are not hoisted to the top of the scope and they can only be used at a point in the code after they have been defined.

`const` variables share all these characteristics with the additional restriction that redeclaring them will generate an error, and changing their value once declared will fail silently.

As a rule of thumb `const` provides the strictest usage contract and clearly signals a narrow intent that the variable will not be redeclared or subsequently have its value reassigned, therefore `const` should be used in preference to `let` and `var` wherever appropriate.

Examples of "blocks" that `let` and `const` variables are scoped to include `if` statements, `for` loop headers and bodies and naked `{}` blocks.

### Block Scope

Attempting to access a `let` or `const` variable outside the block it's declared in will throw an error. Note the use of a naked `{}` block in this example to arbitrarily create a new scope.

```js
var a = 1;

{
  let b = 2;
}

console.log(a); // 1
console.log(b); // ReferenceError, b is undefined
```

### Hoisting

Unlike `var` declarations which are hoisted to the top of their enclosing scope `let` and `const` declarations may only be accessed after they've been declared. `let` and `const` variables are said to be in the scope's TZD (temporal dead zone) before they've been declared, and any attempt to read or write them beforehand will generate an error.

:warning: Most transpilers currently don't handle this behavior fully to-spec, so the above example will probably only error in a native ES6 environment.

```js
{
  console.log(foo); // undefined
  console.log(bar); // ReferenceError: bar is in the 'TDZ'
  var foo = 'foo';
  let bar = 'bar';
}
```

### Loop Scope

When `let` is used in a `for` loop header a new `i` is scoped for each iteration of the loop. This makes writing async code in loops more intuitive since the closure doesn't need to be created manually. This can also help with traditionally counter-intuitive tasks such as applying click event handlers in a loop.

```js
for (var i=1; i<=5; i++) {
    setTimeout(function(){
        console.log(i);
    }, i*100);
}
// 6,6,6,6,6

for (let i=1; i<=5; i++) {
    setTimeout(function(){
        console.log(i);
    }, i*100);
}
// 1,2,3,4,5
```

### Implicit Scope Creation

Using `let` within an `if` block implicitly creates a new scope. This is a hazard of using `let`. The new scope is easily spotted in the simple example above, but when code becomes more complicated hunting for new scopes created by `let` could become a cognitive burden. A rule of thumb is to place `let` declarations at the top of their enclosing block to clearly signpost their use and also avoid being bitten by the TDZ.

```js
if ( foo ) {
    // We're in the same scope as outside the 'if'
}

if ( foo ) {
    // We're in a new scope
    let a = 1;
}
```

### Read-Only const

As mentioned, reassign a value to a constant will silently fail while redeclaring the constant will throw an error.

```js
const foo = 'foo';
foo = 'bar' // Silently fails, foo is still equal to 'foo'
const foo = 'bar' // TypeError, foo has already been defined
```

However constants are not immutable, therefore the properties of non-primitive values defined as a constant may be manipulated freely.

```js
const foo = {a: 1};
foo.a = 2;
foo.a; // 2
```

Happy coding 😃.