DEV Community

Cover image for Blockless Scope: JavaScript Shenanigans
Samuel Rouse
Samuel Rouse

Posted on

Blockless Scope: JavaScript Shenanigans

As a JavaScript developer, you've likely been told that let and const are block scoped. But, how does that work when there is no block?

Classic Example: var vs. let

It doesn't get much more straightforward than this.

// logs 10 ten times
for (var i = 0;i < 10;i += 1) {
  setTimeout(() => console.log(i), 10);
}

// logs 0-9
for (let i = 0;i < 10;i += 1) {
  setTimeout(() => console.log(i), 10);
}
Enter fullscreen mode Exit fullscreen mode

This classic example (which I talked about in Simplify setTimeout) shows a clear example of a variable and a block scope.

But for loops don't need a block statement, they can have a plain statement. So what happens to our block-scoped let when there's no block?

// Still logs 10 ten times
for (var i = 0;i < 10;i += 1) setTimeout(() => console.log(i), 10);

// Still logs 0-9
for (let i = 0;i < 10;i += 1) setTimeout(() => console.log(i), 10);
Enter fullscreen mode Exit fullscreen mode

So...where is the scope?!

Function Scope var

For var, nothing changes. It uses only function or global scope. Using var in the for declaration makes the variable available to the containing function or global scope, and the value will persist after the loop completes.

  var a = 1;

  for (var a = 2;a<5;a+=1) console.log(a);

  console.log(a);
Enter fullscreen mode Exit fullscreen mode

var can re-declare variables without throwing errors, so the var a = 2 in the for loop's initialization expression is the same variable as var a = 1 on the first line.

2
3
4
5
Enter fullscreen mode Exit fullscreen mode

Lexical Scope let

This is where it can look confusing, but it's just a special rule of the loop initialization expression.

Variables declared with var are not local to the loop, i.e., they are in the same scope the for loop is in. Variables declared with let are local to the statement.
MDN, for statement initialization

So while there is no block, there is a scope available exclusively to the statement, whether that's a block statement or not.

Lexical Scope

When we talk about block scope in ES2015+, we're really talking about lexical scope, or the scope as it relates to its surroundings.

The word lexical refers to the fact that lexical scoping uses the location where a variable is declared within the source code to determine where that variable is available.
– MDN, Closures, Lexical scoping

Block scope is a type of lexical scoping. for loop scoping with is another. You can think of this almost like the arguments of a function being available inside the function scope. Function arguments are technically bindings as opposed to variables, but they behave essentially the same.

// Argument a is scoped to the function
[1,2,3].forEach((a) => console.log(a))

// initialization is scoped to the loop statement.
for(let b = 1; b < 5; b += 1) console.log(b);
Enter fullscreen mode Exit fullscreen mode

And, in case you forgot, you can make arbitrary blocks any time you want that have their own scope.

All The Scopes

// Arguments are available to the function scope
function scope(a = 2) {
  console.log(a);

  // Redeclaring the variable in the function scope
  var a = 'a';

  // Function scope
  console.log(a);

  // Redeclaring in the function scope again.
  for (var a = 8; a < 10; a += 1) console.log(a);

  // Function scope
  console.log(a);

  // for statement scope
  for (let a = 1; a < 3; a += 1) console.log(a);

  // Back to the function scope
  console.log(a);

  {
    // Free-floating block with its own scope
    let a = 'random block';
    console.log(a);
  }

  // Back to the function scope
  console.log(a);

  // Access the function scope variable
  for (a = 1; a < 5;a += 1) {
    // var doesn't get block scope, so it is hoisted.
    var b = 2;
    // But let has block scope.
    let c = 3;
    console.log(a, b, c);
  }

  // Access the hoisted variable in the function scope
  console.log(b);

  try {
    // ReferenceError - c was declared in the loop block
    console.log(c);
  } catch(e) {
    // d is scoped to the catch block
    // e is a binding 
    let d = 4;
    console.log(d, e);
  }
}

scope();

// ReferenceError - Not declared in the global scope.
console.log(a);
Enter fullscreen mode Exit fullscreen mode

Conclusion

While we mostly think about block and function scope, there are more scope and special cases, some of which we use probably without even being aware they are different.

for loop initialization expressions provide a special scope to the loop statement, even if the statement isn't a block.

Function arguments are bindings to the function body or the function expression in the case of arrow functions.

Top comments (0)