liveBook · Manning

Lexical environment

As explained in Secrets of the JavaScript Ninja, Second Edition

An internal JavaScript structure that holds identifier bindings and links to an outer environment for name resolution.

Why can a function read a variable from an enclosing function, while a variable declared inside a loop sometimes remains visible after the loop? JavaScript tracks identifier bindings with lexical environments. These are internal engine structures—not objects you can inspect directly. “Scope” is the usual shorthand. section 5.4 section 5.5.2

An environment is associated with a piece of code, such as global code, a function, a block, or a catch clause. Evaluating nested code creates nested environments; each function invocation gets a fresh function environment. An inner environment can reach bindings in its outer environments, but an outer environment cannot reach inward to a function’s local bindings. section 5.4 section 5.4.1

How name lookup works:

  • Start in the current lexical environment, which maps local identifiers to bindings. section 5.4 section 5.4.2
  • If the name is absent, follow the outer-environment link and try again. Stop when a match is found; reaching the global environment without one produces a reference error. section 5.4.2
  • For a function, the outer link follows where the function was created, not merely where it is called. The function retains that environment through its internal [[Environment]] property. section 5.4.2

The loop-variable surprise

With `for (var i = 1; i < 3; i++)`, `i` is registered in the nearest function or global environment, so code after the loop can still read it. Change the declaration to `let i`, and it belongs to the loop’s block environment instead. `const` also uses the nearest environment, including blocks. The distinction is where the binding lives, not whether the loop can use it. section 5.5.2

Listing 5.8. Using const and let keywords
const GLOBAL_NINJA = "Yoshi";                                     #A

function reportActivity(){
  const functionActivity = "jumping";                             #B
  
  for(let i = 1; i < 3; i++) {                                    #C
      let forMessage = GLOBAL_NINJA + " " + functionActivity;     #C
      assert(forMessage === "Yoshi jumping",                      #D
             "Yoshi is jumping within the for block");            #D
      assert(i, "Current loop counter:" + i);                     #D
  } 
  
  assert(typeof i === "undefined" && typeof forMessage === "undefined", #E
        "Loop variables not accessible outside the loop");              #E
}

reportActivity();
assert(typeof functionActivity === "undefined"                        #F
    && typeof i === "undefined" && typeof forMessage === "undefined", #F
    "We cannot see function variables outside of a function");        #F

Compare this let-and-const loop with the var loop: the assertions after the loop make the different binding locations visible. section 5.5.2

Bindings are registered before their code executes. When a new environment is created, JavaScript first scans the relevant declarations, then begins execution. This explains why a function declaration can be called before its position in the code is reached. Function expressions and arrow functions do not receive that same declaration treatment. section 5.5.3

A closure keeps its surroundings

Suppose a constructor creates a local counter and assigns methods that read and increment it. Each constructor call creates a separate environment. Methods created during that call retain a reference to it, so they can use their own counter after the constructor returns; outside code cannot access that local binding directly. The same mechanism lets each timer callback retain its own animation variables without manually matching callbacks to state. section 5.6.1 section 5.6.3

Listing 5.11. Approximate private variables with closures
function Ninja() { 
  var feints = 0;                                        #A
  this.getFeints = function(){                           #B
    return feints;                                       #B
  };                                                     #B
  this.feint = function(){                               #C
    feints++;                                            #C
  };                                                     #C
}   
var ninja1 = new Ninja();  
assert(ninja1.feints === undefined,                       #D
       "And the private data is inaccessible to us.");    #D
ninja1.feint();                                             #E
assert(ninja1.getFeints()=== 1,                             #F
       "We're able to access the internal feint count.");   #F
  
var ninja2 = new Ninja();                                   #G
assert(ninja2.getFeints()=== 0, 
       "The second ninja object gets its own feints variable.");

Follow the constructor’s local variable through its two methods, then compare direct property access with access through a method. section 5.6.1

Keep three distinctions handy:

  • An execution context tracks a function call; its lexical environment tracks the identifiers available to that call. section 5.4.2 section 5.7
  • `var` ignores block environments when choosing where to register a binding; `let` and `const` do not. section 5.5.2
  • A closure can keep an environment available after the call that created it has finished. section 5.6.1 section 5.7

How Secrets of the JavaScript Ninja, Second Edition covers it

How deep it goes
Works through environment creation, outer-link lookup, declaration registration, and closure retention; treats environments as inaccessible engine internals.
Examples
Contrasts loop declarations and traces constructor methods retaining private variables and timer callbacks retaining animation state.
What it assumes
Assumes familiarity with functions and variable declarations while defining the internal environment model from the ground up.
What you can do after
Diagnose/debug with it by tracing where a binding is registered and which outer environments a function can search.
How it connects
Connects execution contexts, scope, block-scoped variables, function declarations, and closures.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Secrets of the JavaScript Ninja, Second Edition. The section links point to the parts of the book it draws on.