BFE.dev solution for JavaScript Quiz
5. scope

TL;DR

let and const in for creates new scope for each iteration, which means the i in console.log() points to different values.

for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0)
}
// 0
// 1
// 2
// 3
// 4

This behavior doesn't apply to var, meaning the i points to same gloabl scope. Because of setTimeout(), console.log() is called after for loop is done, so i is always 5.

for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0)
}
// 5
// 5
// 5
// 5
// 5

Below is detailed explanation. It is pretty complex, hang on!

An simple example of closure

Let's look at code below.

const a = 1

function func() {
  const a = 2
  return () => console.log(a)
}

console.log(a) // 1
func()() // 2

The answer is obvious, func() returns a closure in which a is bound to 2. Same identifier a somehow refers to different values based on where a is, have you wondered how it is done?

How this works could be found in ECMAScript spec.

Evaluation - to get the value of identifier

When meets an identifier a, we need to determine where to find the right value of it. From ECMAScript spec, the value of an identifier is determined by ResolveBinding(StringValue of Identifier).

ResolveBinding ( name [ , env ] )

  1. If env is not present or if env is undefined, then
    1. Set env to the running execution context's LexicalEnvironment.
  2. Assert: env is an Environment Record.
  3. If the source text matched by the syntactic production that is being evaluated is contained in strict mode code, let strict be true; else let strict be false.
  4. Return ? GetIdentifierReference(env, name, strict).

Two things are important here.

  1. The LexicalEnvironment on running execution context is where to resolve the binding.
  2. it returns not the value directly, but some reference.

GetIdentifierReference ( env, name, strict )

From ECMAScript spec, below is how to resolve the bindings recursively.

  1. If env is the value null, then
    1. Return the Reference Record { [[Base]]: unresolvable, [[ReferencedName]]: name, [[Strict]]: strict, [[ThisValue]]: empty }.
  2. Let exists be ? env.HasBinding(name).
  3. If exists is true, then
    1. Return the Reference Record { [[Base]]: env, [[ReferencedName]]: name, [[Strict]]: strict, [[ThisValue]]: empty }.
  4. Else,
    1. Let outer be env.[[OuterEnv]].
    2. Return ? GetIdentifierReference(outer, name, strict).

So we can see clearly that

  1. if identifier is not on the LexicalEnvironment, then it goes to its [[OuterEnv]] recursly, just like the prototype chain we are familiar with.
  2. it returns a Reference Record.

Environment Record

From ECMAScript spec

Environment Record is a specification type used to define the association of Identifiers to specific variables and functions, based upon the lexical nesting structure of ECMAScript code. Usually an Environment Record is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement. Each time such code is evaluated, a new Environment Record is created to record the identifier bindings that are created by that code.

Usually we talk about scope in JavaScript, so Environment Record is just the internal implementation.

Every Environment Record has an [[OuterEnv]] field, which is either null or a reference to an outer Environment Record. This is used to model the logical nesting of Environment Record values.

So we can see now why we are able to access variable outside of function.

Environment Record is abstract class, it has 3 sub classess

  1. A Declarative Environment Record is used to define the effect of ECMAScript language syntactic elements such as FunctionDeclarations, VariableDeclarations, and Catch clauses that directly associate identifier bindings with ECMAScript language values.
  2. An Object Environment Record is used to define the effect of ECMAScript elements such as WithStatement that associate identifier bindings with the properties of some object.
  3. A Global Environment Record is used for Script global declarations. It does not have an outer environment; its [[OuterEnv]] is null.

So what call globe scope is the Global Environment Record, and scope is Declarative Environment Record. The with is not used commonly and indeed it has strange behavior which needs different kind of scope.

What happens to the assignment ?

const a = 1

What happens to above statement? According to ECMAScript spec, it calles InitializeBinding on Environment Record.

  1. Assert: IsUnresolvableReference(V) is false.
  2. Let base be V.[[Base]].
  3. Assert: base is an Environment Record.
  4. Return ? base.InitializeBinding(V.[[ReferencedName]], W).

How Environment Record is created in function declaration ?

// global Environment Record
function func() {
  // inner Environment Record
  // [[OuterEnv]] is global Environment Record
}

Let's see how inner Environment Record is created. It is defined in ECMAScript spec

FunctionDeclaration : function BindingIdentifier ( FormalParameters ) { FunctionBody }

  1. Let name be StringValue of BindingIdentifier.
  2. Let sourceText be the source text matched by FunctionDeclaration.
  3. Let F be OrdinaryFunctionCreate(%Function.prototype%, sourceText, FormalParameters, FunctionBody, non-lexical-this, env, privateEnv).
  4. Perform SetFunctionName(F, name).
  5. Perform MakeConstructor(F).
  6. Return F.

This is the steps to create a new Function object, notice that OrdinaryFunctionCreate() takes env as parameter, it will be set as internal field [[Environment]] of F. So each function has its own [[Environment]] when created.

The inner scope won't be created until the function is executed, which is the internal method [[Call]] of function object as defined in ECMAScript spec

[[Call]] ( thisArgument, argumentsList )

  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.

In step 2 a new calleeContext is created by PrepareForOrdinaryCall()

PrepareForOrdinaryCall ( F, newTarget )

From ECMAScript Spec

  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.

In this part we can see that

  1. a new Code Execution context is created and pushed on top of the stack, working as running execution context.
  2. a new Function Environment Record (localEnv) is created by NewFunctionEnvironment. The Environment Record works as both LexicalEnvironment ( let/const) and VariableEnvironment (var) of the newly created context.

The context is popped in the logic of [[Call]] when call is done.

NewFunctionEnvironment(F, newTarget)

  1. Let env be a new Function Environment Record containing no bindings.
  2. Set env.[[FunctionObject]] to F.
  3. If F.[[ThisMode]] is lexical, set env.[[ThisBindingStatus]] to lexical.
  4. Else, set env.[[ThisBindingStatus]] to uninitialized.
  5. Set env.[[NewTarget]] to newTarget.
  6. Set env.[[OuterEnv]] to F.[[Environment]].
  7. Return env.

In the creation of new Function Environment Record, we can see the env.[[OuterEnv]] is set to function's `[[Environment]], which makes closure possible.

Difference between let and var in for loop.

According to ECMAScript spec, let/const is handled differently comparing to var.

For let and const, it works as below.

ForStatement : for ( LexicalDeclaration Expressionopt ; Expressionopt ) Statement

  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.

In step 2, a new Environment Record (loopEnv) is created and it is set to LexicalEnvironment of current running execution context to bind the lexical declarations

While for var it is much simpler, there is no new environment created.

ForStatement : for ( var VariableDeclarationList ; Expressionopt ; Expressionopt ) Statement

  1. Perform ? Evaluation of VariableDeclarationList.
  2. If the first Expression is present, let test be the first Expression; otherwise, let test be empty.
  3. If the second Expression is present, let increment be the second Expression; otherwise, let increment be empty.
  4. Return ? ForBodyEvaluation(test, increment, Statement, « », labelSet).

Also notice that even though ForBodyEvaluation() is called in both let/const and var, the perIterationLets is empty for var.

ForBodyEvaluation() is the true part that runs each iteration. ECMAScript Spec.

  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).

In step 2, we can see that a new Environment Record is created on each iteration in CreatePerIterationEnvironment(), and the bindings i are newly creating by copying the value.

  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).
      4. Set the running execution context's LexicalEnvironment to thisIterationEnv.
  2. Return unused.

So though this is called also for var, but the parameter of perIterationBindings is empty for var, so there is no Environment record created.

Summary in details

// Global Environment Record {}
//  ↳  Declarative Environment Record { i } - loop env
for (let i = 0; i < 5; i++) {
  //      ↳ Declaritive Environment Record { i } - iteration env, different on each iteration
  setTimeout(() => {
    //           ↳ Function Environment Record
    console.log(i)
  }, 0)
}

Above is the environment records involed. Since iteration env is different each time. i in console.log() has different values on each call.

// Global Environment Record {}
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    // ↳ Function Environment Record
    console.log(i)
  }, 0)
}

For var, there are no new Environment Record created, i points to the same Global Environment Record. Because of setTimeout(), console.log() are called with the same value of i which is 5.

You might also be able to find a solution fromcommunity posts or fromAI solution.