What is Lexical Scope in JavaScript?

Lexical scope is the set of rules for how the JavaScript engine finds variables and functions when executing code, relative to where you've defined them at author-time. The "lexical" refers to the lexing phase of compilation which takes place before code execution. This is in contrast to dynamic scope where the rules of scope can be generated dynamically, while the code is executing at runtime.

Defining Scope

When the JavaScript engine comes across a variable or function call, it looks to the current scope to find it by it's identifier name, and if it's not found, it looks to any parent scope(s) until it reaches the global scope.

So what defines the boundaries of scope? Scope is relative to the authored code as it was at lexing time, and depends on whether you’re using var or let/const.

If a var is referenced within a function, the current scope is the bounding curly braces of that function. A variable declared within the function can’t be accessed directly outside of that function (without explicitly returning it or assigning it to a higher scoped variable).

The global scope is the top boundary of scope, and vars declared in global scope can be accessed on the window object in a browser environment (but not lets or consts). Scopes are nested for functions within global scope, and again for functions within functions.

If the function is inside another function and the variable can't be found in that function scope, scope looks to the bounds of the parent function for the variable.

  var a = 1;
  let g = 2;
  const gc = 3;
  console.log(window.a) // 1 - a global var is available to window (browser environment)
  console.log(window.g) // undefined - let is not available to window
  console.log(window.gc) // undefined - const is not available to window
  function someFunction(b) {
    let c = b; // b is found within the current someFunction scope
    // This is called shadowing, where g is redefined in someFunction
    let g = 10; // g is defined within someFunction scope, so doesn't lookup g in global
    function childFunction(d) {
      // c isn't found in the childFunction scope, so scope looks
      // to the parent, where it finds c in the someFunction scope
      c = c + d; // 2 + 3
      // a isn't found in the childFunction scope, or in the parent
      // someFunction scope, but it is found in the global scope
      // this changes the value of a in the global scope
      a = a * d; // 1 * 3
  // c is not within scope as it's within the bounds of someFunction
  console.log(c) // ReferenceError: c is not defined
  console.log(window.a) // 3, as assigned in childFunction
  console.log(g) // 2 - not changed by childFunction, which defines it's own g

As mentioned, there are differences in using var and let/const when it comes to scope. Declaring a var defines it’s scope within the current function (or global scope if it’s not within a function). let or const on the other hand defines scope for that property within the current block, and not confined to just function bounds.

A block is usually deemed to be the current block of code within curly braces (but not always), and blocks can be created anywhere the curly braces syntax is legal.

  for(var i = 0; i < 2; i++) { console.log(i); } // 0, 1
  // var i is declared in the enclosing scope, not within the for loop
  console.log(i) // 2
  for(let x = 0; x < 2; x++) { console.log(x); } // 0, 1
  // let x is defined within the for loop block scope and not the enclosing scope
  console.log(x) // ReferenceError: x is not defined
  for(var i = 0; i < 2; i++) { var y = 20; }
  // var y is still within the enclosing scope and isn't block scoped
  console.log(y) // 20
  for(let x = 0; x < 2; x++) { var z = 30; }
  console.log(z) // 30
  console.log(x) // ReferenceError: x is not defined
  // {} defines a block
    let b = 100; // let b is only available within the block scope
    console.log(b) // 100
  console.log(b) // ReferenceError: b is not defined
    var c = 200; // var c is not block scoped by curly braces alone
    console.log(c) // 200
  console.log(c) // 200

As in the last example with the explicit curly braces block, creating blocks shows the JS engine that the enclosing lets or consts can be garbage collected after use, as they can’t be accessed outside of that block, which is a performance improvement.

Scope Lookup

There are two types of scope lookup, a left hand side (LHS) reference and right hand side (RHS) reference. An LHS reference lookup occurs when a variable is being assigned, whereas an RHS reference looks to scope to find the source and value of a variable or function. An LHS reference will therefore occur with an assignment operation using = or when an argument is passed to a function, where the assignment of the function parameter is implicit.

  const a = 1;
  function someFunction(b) {  // when 1 is passed to parameter b, b = 1 assignment occurs,
                              // an LHS lookup to scope for b
    console.log(b) // finding the source of b in scope is an RHS lookup
    x = 2; // LHS lookup for x
    // What will happen? It depends if you're using strict mode
  console.log(x); // if not in strict mode, 2 is output - x declared global in someFunction
  console.log(window.x);  // if not in strict mode, 2 is output
                          // because var a is declared in global scope from someFunction
  console.log(x); // if in strict mode - ReferenceError: x is not defined

There's a difference here in what happens if the lookup fails to find anything in scope. For an assignment lookup (LHS), if nothing has been found up to and including the global scope, then the global scope will declare the new variable as a var. If the code is authored in strict mode however ("use strict"), the JavaScript engine will throw a ReferenceError when trying to assign x.

If a lookup to scope to find the source of a variable (RHS) finds nothing declared for that identifier name then again, a ReferenceError will be thrown. If a variable is found but the code is trying to do something that's not allowed for that variable data type, (for example, you try to perform an array method on an integer), then a TypeError is thrown.

  function someFunction() {
    a = 1;
    // someFunction scope can’t find a, so looks to global scope
    // global can’t find a, so global creates var a,
    // as we’re not in strict mode
  console.log(a); // 1

  "use strict"; // now using strict mode
  function someFunction() {    
    a = 1; // ReferenceError: a is not defined
    // a is not in someFunction scope, or in global scope
    // ReferenceError thrown as we're in strict mode

  var a = 1;
  a.push("2"); // Uncaught TypeError, a.push is not a function
  // a is found in scope, so it's not a ReferenceError

You can see the difference between a ReferenceError and a TypeError, an indication as to what may have occurred in your code when you come across them.

I highly recommend Kyle Simpson’s book Scope & Closures for more detailed reading on the subject of scope in JavaScript.

If you have any questions or comments or want to connect, you can follow me on Twitter, or sign up to the newsletter in the footer for front-end articles to your inbox 👇

Back home