Javascript Scope

This article will attempt to deep dive in how scope works in javascript with practical examples and references to the official ECMAScript documentation.

Before we begin, there is a very important article you should read first detailing javascript execution context and how it works. This article will give you the perfect base to start understanding everything else in javascript.

So what is scope? There are so many technical terms that get loosely thrown around and oftentimes wrongly. Scope is not complicated, and if you read the previous artcle on execution context, you will get through this article without problem.

Execution Context

Everytime a function is invoked in javascript, a new execution context is created and appended to the top of the execution context stack or callstack. The execution context that is at the very top of the stack is referred to as the running execution context. Each execution context in the execution context stack is what is often referred to as a stack frame.

Let's quickly review how the process works:

The execution context has two phases, the creation phase and the execution phase. In the creation phase the LexicalEnvironment and VariableEnvironment state components get created.

Anytime any code is evaluated there is always at least the global execution context. If a function is invoked then a new execution context is created and pushed onto the top of the callstack. Each execution context has a LexicalEnvironment and VariableEnvironment state component.

The LexicalEnvironment component is a pointer to an Environment Record. The Environment Record defines the association of identifiers to specific variables and functions based upon the lexical nesting structure of your code. The Environment Record also contains a property called [[OuterEnv]] which references it's outer LexicalEnvironment.

JavaScript
function greet(name) {
  console.log(`hi ${name}`)
}

greet('david')

The below code represents how this might look, notice that the topmost execution context is the function execution context, it is ordered just as it would be if it were in the callstack.

GreetFunctionExecutionContext = {
  LexicalEnvironment: {
    arguments: {
      length: 1,
      0: 'david'
    },
    [[OuterEnv]]: <ref. to GlobalExecutionContext Environment Record>,
    [[ThisValue]]: window
  }
}

GlobalExecutionContext = {
  LexicalEnvironment: {
    foo: <ref. to greet function object>,
    [[OuterEnv]]: null
  }
}

This is a basic example, but it starts to allow you to visualize how the javascript engine works internally.

Execution Context and Scope

Execution contexts occur anytime a function is invoked. If there is no function, for example in the case of a for loop that is in global scope there is still a global execution context at the minimum. No matter what there is always at least the global execution context. A for loop will not create a new execution context, it only modifies the environment record of the running execution context. Another example to drive this point home, if you have some global code and a block is evaluated with let or const declarations, then a new Environment Record is created, and the running execution context's lexical environment is pointed to the new environment record.

How Scope Gets Updated

Let's try to solidify the above concepts before we get deeper into this.

JavaScript
function foo() {
  const x = 1

  if (x > 0) {
    const x = 2
    console.log(x) // 2
  }

  console.log(x) // 1
}

foo()

Functions in javascript are objects but special ones, they are special because they have an additional internal method, [[Call]]. For this reason you will often see functions in javascript referred to as callable objects. If we look at what the spec says about the [[Call]] internal method it says:

10.2.1 [[Call]] ( thisArgument, argumentsList )

It performs the following steps when called, pay attention to steps 2 and 3. On step 2 an abstract operation, PrepareForOrdinaryCall runs. In step 3, this new calleeContext returned from step 2 is set to the running execution context.

  1. Let callerContext be the running execution context.
  2. Let calleeContext be PrepareForOrdinaryCall(F, undefined).
  3. Assert: calleeContext is now the running execution context.
  4. If F.[[IsClassConstructor]] is true, then
    1. Let error be a newly created TypeError object.
    2. NOTE: error is created in calleeContext with F's associated Realm Record.
    3. Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
    4. Return ThrowCompletion(error).
  5. Perform OrdinaryCallBindThis(F, calleeContext, thisArgument).
  6. Let result be Completion(OrdinaryCallEvaluateBody(F, argumentsList)).
  7. Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
  8. If result.[[Type]] is return, return result.[[Value]].
  9. ReturnIfAbrupt(result).
  10. Return undefined.

Now let's take a look at the abstract operation, PrepareForOrdinaryCall which returns calleeContext. Pay attention to points 2, 7 and 8. Step 1 says that the callerContext is the running execution context, for this example let's assume that the running execution context is the global execution context.

10.2.1.1 PrepareForOrdinaryCall ( F, newTarget )

The abstract operation PrepareForOrdinaryCall takes arguments F (a function object) and newTarget (an Object or undefined) and returns an execution context.

On line 7 something happens, a new scope is created. We said before that scopes are Environment Records and on line 7 we get a new Function Environment Record and on line 8, the LexicalEnvironment of calleeContext is set to the scope created on line 7.

  1. Let callerContext be the running execution context.
  2. Let calleeContext be a new ECMAScript code execution context.
  3. Set the Function of calleeContext to F.
  4. Let calleeRealm be F.[[Realm]].
  5. Set the Realm of calleeContext to calleeRealm.
  6. Set the ScriptOrModule of calleeContext to F.[[ScriptOrModule]].
  7. Let localEnv be NewFunctionEnvironment(F, newTarget).
  8. Set the LexicalEnvironment of calleeContext to localEnv.
  9. Set the VariableEnvironment of calleeContext to localEnv.
  10. Set the PrivateEnvironment of calleeContext to F.[[PrivateEnvironment]].
  11. If callerContext is not already suspended, suspend callerContext.
  12. Push calleeContext onto the execution context stack; calleeContext is now the running execution context.
  13. NOTE: Any exception objects produced after this point are associated with calleeRealm.
  14. Return calleeContext.

When our example function is invoked, the above steps would take place. Now we need to figure out what happens on line 5 of the example code block when a const declaration inside of a block is encountered. When line 5 is encountered, the Environment Record will be modified for the current execution context. Here you can read the steps that occur when a block is evaluated or just read below.

14.2.2 Runtime Semantics: Evaluation

  1. Let oldEnv be the running execution context's LexicalEnvironment.
  2. Let blockEnv be NewDeclarativeEnvironment(oldEnv).
  3. Perform BlockDeclarationInstantiation(StatementList, blockEnv).
  4. Set the running execution context's LexicalEnvironment to blockEnv.
  5. Let blockValue be Completion(Evaluation of StatementList).
  6. Set the running execution context's LexicalEnvironment to oldEnv.
  7. Return ? blockValue.

Pay attention to what is happening here, when a function is invoked and a block is evaluated, a new execution context is not created, rather a new Envrionment Record is created and the running execution context's LexicalEnvironment is pointed to the new EnvironmentRecord. On step 2, a new block scope is created. When the block is exited the running execution contexts LexicalEnvironment is pointed back to the previous EnvironmentRecord before the block was entered.

JavaScript
function foo() {
  const x = 1

  if (x > 0) {
    // step 2 occurs and a new environment record (scope) is created
    const x = 2
    console.log(x) // 2
  }

  console.log(x) // 1
}

foo()

Scope Chain

Each time a variable or function is needed, the javascript compiler looks for it in it's current scope and if it can't find it there is checks the [[OuterEnv]] field of the Environment Record to see if the variable or function exists in the enclosing scope (outer Environment Record), it will continue doing this all the way up to the global scope and if it still can't find it there then the compiler will complain and throw an error.

Therefore, scope chain is a chain of Environment Records linked with the [[OuterEnv]] property. Scope lookup is another common term describing how the javascript compiler will continue to look for a variable in the outer Environment Record.

JavaScript
const name = 'david'

function greet() {
  console.log(`hi ${name}`)
}

greet()

Here is how the above code block is evaluated, note how name: 'david' exists in the GlobalExecutionContext but not in GreetFunctionExecutionContext.

When this code is executed the javascript compiler will see that there is no name variable in the current Lexical Environment so it will check in the [[OuterEnv]] field of the running execution context's environment record to see if it can find it there, if not it continues to move up the scope chain until the variable is found otherwise the compiler will throw an error complaining that the variable could not be found.

GreetFunctionExecutionContext = {
  LexicalEnvironment: {
    [[ThisValue]]: window,
    [[OuterEnv]]: <ref. to GlobalExecutionContext Environment Record>
  }
}


GlobalExecutionContext = {
  LexicalEnvironment: {
    foo: <ref. to greet function object>,
    name: 'david',
    [[OuterEnv]]: null
  }
}

Lexical Scope

Lexical scope refers to how scope is determined at lexing time which is the first stage in compilation. In simpler terms, lexical scope is determined by the physical placement of your code. Lexical scope is also referred to as static scope.

Looking at this code it's easy to see lexical scope, that is we can see which functions have access to certain variables due to the physical placement of the code.

JavaScript
function foo() {
  const bar = 'bar'

  function baz() {
    console.log(bar)
  }
}

Types of Scope

When reading about scope you will often see that there are four types of scope: global scope, function scope (local scope), module scope, and block scope.

Function Scope (local scope)

Functions also create a scope for variables declared with var, let and const. As we seen above, it is a bit more complex than this and when a function is invoked a new function scope or more specifically, a new Function Environment Record is created with the NewFunctionEnvironment operation.

JavaScript
function foo() {
  const x = 1
}

console.log(x)  // ReferenceError: x is not defined

Module Scope

Module scope is basically like a function and that's what modules essentially are. They are javascript files that have type="module" and the file itself acts as one big function.

Anytime <script type="module"></script is evaluated, a Module Environment Record (module scope) is created by the NewModuleEnvironment operation.

If you want to read about the exact steps that take place, you can read this section of the spec detailing how a module environment record is created.

If you read the spec in the above link, pay attention to step 5.

16.2.1.6.4 InitializeEnvironment ()

  1. Let env be NewModuleEnvironment(realm.[[GlobalEnv]]).

This line is what creates the Module Environment Record otherwise known as module scope.

index.html
<script src="app1.js" type="module"></script>
<script src="app2.js" type="module"></script>

Global Scope

Anytime code is evaluated, there is always at least the global execution context. You can add a debugger statement inside any function and you will see (anonymous) inside of the call stack pane in chrome dev tools, this is simply the global execution context.

To expand on this a bit more, according to the spec:

Before it is evaluated, all ECMAScript code must be associated with a realm. Conceptually, a realm consists of a set of intrinsic objects, an ECMAScript global environment, all of the ECMAScript code that is loaded within the scope of that global environment, and other associated state and resources.

The abstract operation CreateRealm creates a new Realm Record and a Realm Record has a few fields, but we will only look at [[GlobalObject]] and [[GlobalEnv]]. The [[GlobalObject]] is an object or undefined and the [[GlobalEnv]] is a Global Environment Record or global scope. The [[OuterEnv]] of the Global Environment Record is null.

You can imagine the realm as a global execution context, since any time code is evaluated there must always be at least a global execution context.

The Global Environment Record or global scope is the default scope if none of the other rules apply. Here you can find the global object, which is window in the case of browsers, bindings for built in globals, properties of the global object ect. It is a single enclosing environment record that wraps an Object Environment Record and a Declarative Environment Record. The bindings for all the declarations in global code are contained in the Declarative Environment Record component of the Global Environment Record and bindings for all built-in globals as well as the binding object are contained in the Object Environment Record component of the Global Environment Record.

When this code is run, when you take a look at the call stack you will see (anonymous) and on top of that you will see foo. (anonymous) is the global execution context.

JavaScript
function foo() {
  debugger
}

foo()

We mentioned already that loops do not create a new execution context, they only modify the running execution context's Environment Record. When this code is run, you will only see (anonymous) in the call stack.

JavaScript
for (let i = 0; i < 3; i++) {
  debugger
}

// same goes for for in
for (const num of [1, 2, 3]) {
  debugger
}

In the below code block, const x = 1 is declared in global scope and is available in all the other files.

index.html
<script src="script.js"></script>
<script src="app1.js"></script>
<script src="app2.js"></script>

Block Scope

Curly brace pairs {} do not always create scopes, as is the case for function declarations and object literals. When a block is evaluated a new Declarative Environment Record (block scope) is created for let and const declarations, and bindings for each block scoped variable declared in the block are instantiated in the Environment Record.

JavaScript
{
  // here there is not necessarliy a scope yet
  
  var x = 1
  // still no block scope as a block scope is not created for var variables


  /* now the block needs to become a scope, by performing step 2
    Let blockEnv be NewDeclarativeEnvironment(oldEnv)
  */
  const x = 1
}

In other words, a block only becomes a scope if it has to in order to contain it's block scoped declarations (let, const).

How Block Scope Is Evaluated According to the Spec

14.2.2 Runtime Semantics: Evaluation

Block : { StatementList }

  1. Let oldEnv be the running execution context's LexicalEnvironment.
  2. Let blockEnv be NewDeclarativeEnvironment(oldEnv).
  3. Perform BlockDeclarationInstantiation(StatementList, blockEnv).
  4. Set the running execution context's LexicalEnvironment to blockEnv.
  5. Let blockValue be Completion(Evaluation of StatementList).
  6. Set the running execution context's LexicalEnvironment to oldEnv.
  7. Return ? blockValue.

Here is a short visualization of how that might look given the following code block.

JavaScript
function foo() {
  const x = 1

  if (x > 0) {
    const x = 2
  }

  console.log(x)
}

foo()

Once line 11 is evaluated, the foo function object will be invoked and the state of the program will look like this:

FooFunctionExecutionContext = {
  LexicalEnvironment: {
    x: 1,
    [[ThisValue]]: window,
    [[OuterEnv]]: <ref. to GlobalExecutionContext Environment Record>
  }
}

GlobalExecutionContext = {
  LexicalEnvironment: {
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  }
}

Now line 5 is evaluated and a new Declarative Environment Record is created.

FooFunctionExecutionContext = {
  LexicalEnvironment: {
    x: 2,
    [[ThisValue]]: window,
    [[OuterEnv]]: <ref. to GlobalExecutionContext Environment Record>
  }
}

GlobalExecutionContext = {
  LexicalEnvironment: {
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  }
}

When evaluation leaves the block then the Lexical Environment before the block was entered is restored.

FooFunctionExecutionContext = {
  LexicalEnvironment: {
    x: 1,
    [[ThisValue]]: window,
    [[OuterEnv]]: <ref. to GlobalExecutionContext Environment Record>
  }
}

GlobalExecutionContext = {
  LexicalEnvironment: {
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  }
}

var is Function Scoped but not Block Scoped

A new block scope is not created when a var binding is evaluated inside curly braces {}.

JavaScript
function foo() {
  var x = 1

  if (x > 0) {
    // no need to create a block scope
    var x = 2
  }

  console.log(x) // 2 not block scoped
}

console.log(x) // Uncaught ReferenceError: x is not defined

var can be Re-Declared

JavaScript
var x = 1

{
  var x = 2
}

// this logs 2 because var x = 2 is in the same scope as var x = 1
// we just wrote 
// var x = 1
// var x = 2
// console.log(x)
console.log(x) // 2

let and const Cannot be Re-Declared

The reason this works is when the block is evaluated a new Environment Record is created and the running execution context's lexical environment is pointed to this new environment record (blockEnv) and on line 7 of the below code block, the lexical environment is pointed back to oldEnv.

JavaScript
const x = 1

{
  const x = 2 // here blockEnv is created
}
// this is in global scope and the lexical environment
// of the global execution context is pointed back to oldEnv
console.log(x) // 1

Shadowing

We already talked about what scope lookup is, the compiler will look for a variable first in it's current scope and if it can't find it, it checks the [[OuterEnv]] field of the running execution context's Environment Record stopping as soon as a match is found. Below the x parameter in the second code block shadows the global variable let x = 1.

JavaScript
let x = 1

function foo() {
  x++
  console.log(x) // 2
}

foo()
console.log(x) // 2

Note how the global variable let x = 1 is incremented. Here in the below code block, the local x parameter shadows the global let x = 1.

JavaScript
let x = 1

function foo(x) {
  // imagine declaring a variable here
  // let x = 1
  x++
  console.log(x) // 2
}

foo(1)

console.log(x) // 1

Here is another case of shadowing. From the inner forEach there is no way to console.log the outer num paramter.

JavaScript
const nums = [[1, 2], [3, 4], [5, 6]]

nums.forEach(num => {
  console.log(num) // [1, 2]
  num.forEach(num => {
    // here there is no way to access the outer num
    // because it's been shadowed
    // this is a new scope and a new num parameter 
    console.log(num) // 1 2
  })
})

If we needed access to the outer num parameter from the inner forEach, then we need to name the parameters differently.

JavaScript
nums.forEach(num => {
  num.forEach(i => {
    // here we have access to both
    // `num` and `i`
  })
})

Scope with Loops

let declarations are special cased by for loops. If the initialization block is a let declaration, then everytime after the loop body is evaluated the following occurs:

  1. A new lexical scope is created with new let declared variables
  2. The binding values from the last iteration are used to re-initialize the new variables
  3. afterthought is evaluated in the new scope

What that means is at first a scope is created for the initialization block and then the values are copied over to the new scope. We will take a deeper look at this later in this article.

Since for loops create a new block scope for variables declared with let and const for each loop iteration, in the below code, a new Environment Record will be created for each loop iteration. This means there is a new foo variable for every loop iteration.

JavaScript
for (const num of [1, 2, 3]) {
  // num is treated as if it were declared here
  const foo = num
}

In the case of forEach we pass a callback to it so for each iteration, a new Execution Context is created since the callback is called each time, and therefore a new Lexical Environment for each iteration is created with it's own Environment Record.

JavaScript
[1, 2, 3].forEach(num => {
  const foo = num
})

What scope is i in?

The scoping effect of the initialization block can be understood as if the declaration is made withing the loop body, which creates a new block scope(blockEnv), as long as a let declaration is present. If var would be used, the scope of i would not be the loop body.

JavaScript
for (let i = 0; i < 3; i++) {
  // imagine it is declared here
  // let i = 0
}

// or try to imagine it like this
let _i = 0

for (; i < 3; i++) {
  let i = _i
}

Loops Create a New Block Scope for let Declarations

This is an infamous interview question a lot of people get wrong and it deals with scope.

JavaScript
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(`val: ${i}`)
  }, 1000)
}

// val: 3
// val: 3
// val: 3

The reason this happens is that a new block scope is not created for var declarations. The above code can be re-written with let to achieve the same undesired result.

JavaScript
let i
for (i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(`val: ${i}`)
  }, 1000)
}

// val: 3
// val: 3
// val: 3

This can be fixed by just using let.

JavaScript
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(`val: ${i}`)
  }, 1000)
}

// 0, 1, 2

Why Does const Not Work

If a new Environment Record is created for each iteration of our loop then why can't we use const in the regular for loop?

JavaScript
for (const i = 0; i < 3; i++) {
  console.log(i)
}

// Uncaught TypeError: Assignment to constant variable.

Let's re-write this to better visualize what is happening

JavaScript
const _i = 0

for (; _i < 3; _i++) {
  // i is created for each iteration
  const i = _i
  // const cannot be re-assigned
  i++
}

The problem is that const variables cannot be re-assigned. The problem is coming from trying to increment const _i = 0. Re-assignment is not allowed for constants.

For Loop Scope Advanced

Remember, whenever a for loop runs and there is a let declaration in the initialization block, each time after the loop body is evaluated the following happens:

  1. A new lexical scope is created with new let variables
  2. The bindings from the last iteration are used to re-initialize new variables
  3. the afterthought is evaluated in the new scope.

Let's compare the 3 simplified steps that we talked about before with the spec.

A few notes about what is happening below, note in ForLoopEvaluation line 2, the operation NewDeclarationEnvironment runs and creates a new Declarative Environment Record referred to here as loopEnv. On line 6, the running execution context's lexical environment is pointed to this new declarative environment record. On line 12, the operation ForBodyEvaluation runs and on line 13, the loop finishes and the lexical environment is pointed back to oldEnv from step 1.

14.7.4.2 Runtime Semantics: ForLoopEvaluation

  1. Let oldEnv be the running execution context's LexicalEnvironment.
  2. Let loopEnv be NewDeclarativeEnvironment(oldEnv).
  3. Let isConst be IsConstantDeclaration of LexicalDeclaration.
  4. Let boundNames be the BoundNames of LexicalDeclaration.
  5. For each element dn of boundNames, do
    1. If isConst is true, then
      1. Perform ! loopEnv.CreateImmutableBinding(dn, true).
    2. Else,
      1. Perform ! loopEnv.CreateMutableBinding(dn, false).
  6. Set the running execution context's LexicalEnvironment to loopEnv.
  7. Let forDcl be Completion(Evaluation of LexicalDeclaration).
  8. If forDcl is an abrupt completion, then
    1. Set the running execution context's LexicalEnvironment to oldEnv.
    2. Return ? forDcl.
  9. If isConst is false, let perIterationLets be boundNames; otherwise let perIterationLets be a new empty List.
  10. If the first Expression is present, let test be the first Expression; otherwise, let test be empty.
  11. If the second Expression is present, let increment be the second Expression; otherwise, let increment be empty.
  12. Let bodyResult be Completion(ForBodyEvaluation(test, increment, Statement, perIterationLets, labelSet)).
  13. Set the running execution context's LexicalEnvironment to oldEnv.
  14. Return ? bodyResult.

Here notice step 2, the CreatePerIterationEnvironment operation runs, this is the first iteration of the loop. Here the variables are copied over from loopEnv which was setup in ForLoopEvaluation step 2. In this first iteration, the increment expression does not occur, it starts in the second iteration.

14.7.4.3 ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet )

  1. Let V be undefined.
  2. Perform ? CreatePerIterationEnvironment(perIterationBindings).
  3. Repeat,
    1. If test is not empty, then
      1. Let testRef be ? Evaluation of test.
      2. Let testValue be ? GetValue(testRef).
      3. If ToBoolean(testValue) is false, return V.
    2. Let result be Completion(Evaluation of stmt).
    3. If LoopContinues(result, labelSet) is false, return ? UpdateEmpty(result, V).
    4. If result.[[Value]] is not empty, set V to result.[[Value]].
    5. Perform ? CreatePerIterationEnvironment(perIterationBindings).
    6. If increment is not empty, then
      1. Let incRef be ? Evaluation of increment.
      2. Perform ? GetValue(incRef).

Below on step 1.d a new Environment Record, thisIterationEnv, is created and the Lexical Environment of the current execution context is pointed at it.

Step 1.e shows how the variables from the last iteration, lastIterationEnv, are copied over to thisIterationEnv, the new Environment Record from step 1.d.

14.7.4.4 CreatePerIterationEnvironment ( perIterationBindings )

  1. If perIterationBindings has any elements, then
    1. Let lastIterationEnv be the running execution context's LexicalEnvironment.
    2. Let outer be lastIterationEnv.[[OuterEnv]].
    3. Assert: outer is not null.
    4. Let thisIterationEnv be NewDeclarativeEnvironment(outer).
    5. For each element bn of perIterationBindings, do
      1. Perform ! thisIterationEnv.CreateMutableBinding(bn, false).
      2. Let lastValue be ? lastIterationEnv.GetBindingValue(bn, true).
      3. Perform ! thisIterationEnv.InitializeBinding(bn, lastValue).
    6. Set the running execution context's LexicalEnvironment to thisIterationEnv.
  2. Return unused.
JavaScript
for (let i = 0; i < 3; i++) {
  console.log(i)
}

// first evaluation of the loop setup in ForLoopEvaluation step 2
// loopEnv
{ 
  let i
  i = 0;
  __i = { i }
}

// CreatePerIterationEnvironment(perIterationBindings)
// thisIterationEnv
// first iteration no increment expression
{ 
  let { i } = __i
  if (i < 3) console.log(i) // 0
  __i = { i }
}

// second iteration increment begins
{ 
  let { i } = __i
  i++
  if (i < 3) console.log(i) // 1
  __i = { i }
}   
{ 
  let { i } = __i
  i++
  if (i < 3) console.log(i) // 2
  __i = { i }
} 
{ 
  let { i } = __i // 2
  i++ // 3
  // this will not run but a scope was created for it
  if (i < 3) console.log(i)
}

Note that the first iteration of the loop the initialization block is creates a new scope to initialize the variables and then those values are copied over to the next scope inside of CreatePerIteration (step 1e). Step 1e details how the bindings from the lastIterationEnv are used to initialize new bindings for thisIterationEnv.

Now this is a trick question, we will take an example from mdn and break it down. This is a good example because it really shows how well you understand scope with for loops and closures are also present.

JavaScript
for (
  let i = 0, getI = () => i, incrementI = () => i++;
  getI() < 3;
  incrementI()
) {
  console.log(i);
}

// 0, 0, 0

On line 2, both functions form a closure which is a function bundled together with reference to it's lexical environment or outer Environment Record ([[OuterEnv]]). Also remember that the variable values are copied over to the new scope.

JavaScript
// ForLoopEvaluation (loopEnv)
{
  let i, getI, incrementI
  i = 0
  // both of these functions close over this particular i variable
  getI = () => i
  incrementI = () => i++
  $$lastIterationEnv = { i, getI, incrementI }
}

// First iteration
// note this is special cased and the increment is not evaluated the first iteration
{
  // this is a new `i` variable
  let { i, getI, incrementI } = $$lastIterationEnv

  // getI() returns 1 instead of 0
  // i inside the loop body is not updated however
  // now the value i = 0 is copied over to the next scope (environment record)
  if (getI() < 3) console.log(i) // 0
  $$lastIterationEnv = { i, getI, incrementI }
}

{
  let { i, getI, incrementI } = $$lastIterationEnv
  // updated `i` from first scope due to closure, not the `i` variable in this scope
  // more technically, updated `i` in a different environment record, not this
  // particular environment record.
  incrementI() // 1
  if (getI() < 3) console.log(i) // 0
  $$lastIterationEnv = { i, getI, incrementI }
}

{
  let { i, getI, incrementI } = $$lastIterationEnv
  incrementI() // 2
  if (getI() < 3) console.log(i) // 0
  $$lastIterationEnv = { i, getI, incrementI }
}

{
  let { i, getI, incrementI } = $$lastIterationEnv
  incrementI() // 3
  if (getI() < 3) console.log(i)  // does not run
  $$lastIterationEnv = { i, getI, incrementI }
}

Parameter Scope

This is a bit interesting but if you read the spec closely, whenever there are parameters that have defaults, a seperate scope is created for those parameters.

In the below code block, x = 2 is not re-assigning let x = 1 from the outer scope, the compiler sees it as there is a new block scope and x = 2 is an attempt to re-assign let x = 3.

JavaScript
let x = 1
{
  x = 2 // this is trying to re-assign let x = 3 not let x = 1
  let x = 3
}
// Uncaught ReferenceError: Cannot access 'x' before initialization

The reason this happens is because in the execution phase, x is hoisted to the top of its scope.

JavaScript
let x = 1
{
  // x: uninitialized (hoisted to the top of the scope/environment record)
  x = 2
  // the only way to initialize let or const is with assignment and declaration statement
  // ie not x = 2, rather let x = 2. Same with `const`
  let x = 3
}
// Uncaught ReferenceError: Cannot access 'x' before initialization

Now that we understand a bit about errors and how they work, let's break down the below code block. The first thing to note is that default parameters are being used here, the second thing to note is the param b = () => a is forming a closure. The fact that default parameters are being used means that a seperate scope will be formed to prevent parameter expressions, b = () => a, from closing over declarations in the function body.

The scope of the function body is nested inside the parent scope created for the non simple parameters.

javascript
function sum(/* new scope */ a = 1, b = () => a) {
  // here a new scope is created
  return a + b()
}

sum() // 2

The same rules still apply, the nested child scope of the function body has access to the parent scope created for the parameters.

javascript
function sum(a = 1, b = () => a) {
  console.log(a) // 1
  console.log(b) // () => a
  return a + b()
}

sum() // 2

// this will not work since a parent scope 
// can not reach into a child scope

function sum(a = 1, b = () => console.log(c)) {
  const c = 2
  b()
}

sum()
// Uncaught ReferenceError: c is not defined
JavaScript
function sum(a = b, b = 1) {
  return a + b
}
sum()
// Uncaught ReferenceError: Cannot access 'b' before initialization


// but this works
function sum(a = 1, b = a) {
  return a + b
}
sum() // 2


// this will also work
function sum(b = () => a, a = 1) {
  return a + b() // by this time both parameters have been intialized
}
sum() // 2

There is one interesting edge case detailed in the spec under FunctionDeclarationInstantiation on line 28.e.6.

NOTE: A var with the same name as a formal parameter initially has the same value as the corresponding initialized parameter.

What this means is that if you shadow a parameter with var, it will be initialized to that value rather than the typical undefined value we expect with hoisting.

JavaScript
function sum(a = 1, b = () => a) {
  var a // this will not be auto initialized to undefined
  console.log(a) // 1 not undefined
}

sum()

Review

This was a bit of a long article but these are very important concepts to understand. Having a solid foundation in these concepts is essential since this is a complex topic and there is a lot of wrong advice online. Anytime you want to dig deep, make sure to read the official ECMAScript documentation.

  • There are four scopes: global, module, function (local), and block.
  • The spec details certain operations such as New Declarative Environment, NewFunctionEnvironment ect. These operations create Environment Records.
  • Every time code is executed in javascript there is always at least the Global Execution Context.
  • Every execution context has a Lexical Environment component.
  • A Lexical Environment is a pointer to an Environment Record.
  • When a for loop is executed, a new execution context is not created. Rather a new Environment Record is created and the running execution context's lexical environment is pointed to the new Environment Record.
  • Scopes are Environment Records.