JavaScript 101: Scope

Recently I’ve been looking over some JavaScript basics. Time and again it seems to me that there are several sophisticated concepts of the language which seem to constantly elude developers’ grasp, and Scope tends to be one of the little nuggets always cropping up on that tricky list.

So what is Scope anyway?

Scope represents a collection of rules based on which variables can be looked up. Variable look-up is the backbone of any program, or at least the backbone of any interesting program.

Nested scopes

Now, when it comes to variable look-up, there are usually several scopes to consider. Just as there are usually multiple layers of nested blocks and functions, there also are multiple levels of nested scopes.

When a variable look-up happens, the JavaScript engine consults the immediate scope at first. If the variable isn’t found there, the look-up continues in the next, containing scope, and so on and so forth until found, or until the top (global) scope is reached.

Let’s have a quick look at an example:

function foo(x) {
  //the reference for y cannot be found here
  //so the engine looks for it in the global scope
  console.log(x*y);
}
//and finds it here
var y = 2;
foo(2); //4

Nowhere to be found

Now, it’s all fun and games moving from inner scope to outer scope and so on in search of this and that, but sometimes the engine reaches the global scope without finding what it was looking for. A few things can happen in this case, and the resolution mostly depends on the type of look-up the engine is performing.

When it comes to variables, there are really only two types of things we generally want to do with them.

  1. LHS (“left hand side”) look-ups
  2. RHS (“right hand side”) look-ups

The left/right terminology stems from the fact that these look-ups can be mapped onto variables on the left and right side of an assignment operation. But this can be slightly confusing, as it’s not always apparent when these assignment operations actually happen and how. Let’s see some examples.

An LHS look-up happens when we want to change a variable’s value. We don’t particularly care what its original value is, as we’ll be changing it anyway. In this case, that variable is always the target for some sort of assignment.

x = 2; //LHS look-up for x
function foo(a) {
...
}
//a perhaps less obvious LHS look-up for a = 2
foo(2);

An RHS look-up is when we care very much about the value of said variable, as we plan to use it for something later on. That variable is the source for some sort of assignment.

x = y; //LHS look-up for x, RHS look-up for y
function foo(a) {
...
}
//RHS look-up for foo
//LHS look-up for a = 2
foo(2);

Now, why do we care about this right/left hand side madness?

Well, because it drives what happens when a variable look-up reaches the global scope without having found anything. Let’s summarize this below, as follows:

If the JavaScript engine reaches the global scope without finding the variable reference it was looking for…

If it’s an RHS look-up, a RefferenceError is thrown.

If it’s an LHS look-up

  • and the program is running in Strict Mode, a RefferenceError is thrown.
  • and the program is not running in Strict Mode, a variable of that name is created in the global scope and returned.

Lexical scope

In terms of identifier look-up, JavaScript’s scope model is what is called Lexical Scope.

It gets its name from lex/lexing, which is the process through which the language compiler parses code, splits it into tokens (lexes) and then assigns meaning to them. The what and where of these tokens is defined by you, when you write the code blocks and identifiers, and so by the time they are parsed, they are pretty much set in stone. The lexer can thus predict how everything will be looked up during execution. In true JavaScript fashion, there are of course ways to hijack this, but they are frowned upon, performance costly and/or deprecated and we’ll choose to actively ignore them for the purpose of this exercise.

The lexer travels the different scopes, looking for identifiers, from the innermost scope being executed, and upwards until a match is found.

The lexical scope look-up only applies to first class identifiers, such as x. For a reference which looks like foo.bar.baz the only lexical scope look-up will be for foo. Once that is found, it will be the turn of property access rules to resolve the inner bar and baz.

What makes a scope?

We’ve of course touched in quite a bit of detail the lookup mechanics from inner to outer, to global scope. But what exactly creates these concentric scope bubbles in JavaScript. Well, the short answer to that is functions and blocks.

Function scope

Each function you create in JavaScript creates its own scope. They can, of course, be nested.

function foo(a) {
  var b;
  function bar() {
    var d;
  }
  var c = 1;
}

Above we have the scope created by foo, which contains the identifiers abcbar, and the scope created by bar, with its one identifier, d. We also have the global scope, with its identifier foo.

It’s important to note that it makes no difference where a declaration appears within a function scope, it belongs to that scope and will be available anywhere within it.

What is also highly relevant here is that fooabcbar, are not only accessible to foo, but also to its inner bar, as they are part of its outer scope. On the other hand, d is not accessible to foo, and nor are d and bar accessible in the global scope.

Block scope

While function scope is the design approach in JavaScript, the idea of block scope is also something that’s been on people’s minds for a while.

Consider the below:

for (var i=0; i<10; i++) {
...
}
if (foo) {
  var bar = !foo;
}

Now, wouldn’t it be nice if block scoping actually were a thing when it comes to these snippets?

Because it’s important to remember that there is no benefit in declaring variables in the larger scope, if they will only be of use in a smaller, in this case, “block scope”. A perfect illustration of this is that i identifier which is only useful to its corresponding for loop. Why would one require it to be accessible within its outer scope? within the scope of which it’s declared.All these snippets should look at least somewhat familiar, and they all illustrate block scoping. The same applies to that bar and its containing if.

If only block scope were an actual thing.

Well…

try {
...
}
catch(err) {
}

Here, err is actually available inside the catch, but not outside it, and that’s pretty block scope-ish if you ask me. And actually, it gets much better.

let

With the much awaited arrival of ES6, we got our hands on a new way of declaring variables which very much brings all our block scoping fantasies to life. I present to you, the let keyword.

let attaches the variable declaration to its containing block, thus hijacking the block’s scope for its variable declaration.

Here’s our previous snippet with the let magic sprinkled on top.

for (let i=0; i<10; i++) {
...
}
console.log(i); //RefferenceError
if (foo) {
  let bar = !foo;
}
console.log(bar); //RefferenceError

const

ES6 also introduces const, which again creates a block scoped variable, but its value is fixed (constant) and changing it will return an error.

if (foo) {
  let a = 1;
  const b = 2;
  a = 3; //works
  b = 3; //RefferenceError
}

Et voila!

That’s it in terms of JavaScript scope for today. We’ve not only covered the most important aspects of this concept, but scratched the surface towards other daunting exciting quirks of the language, which we’ll be covering in future posts.