0

I've been attempting to solidify my understanding of JS's execution contexts, and am having trouble getting an explanation as to why the code below does not print out "hello world".

var foo = "foo";

function test1() {
    console.log(foo)
    var bar = "hello world";
    test2();
}

function test2() {
    console.log(bar);
}

test1();

It is my (admittedly very shaky) understanding that test2(), being executed inside of test1(), has access to test1()'s execution context and should be able to resolve the variable name bar by moving up the scope chain into test1()'s execution context where bar is defined. Instead, I get a reference error when attempting to print bar.

I can understand how the code below works due to JS's lexical scoping, but I would appreciate an explanation how JS interprets each differently in terms of execution contexts and scope-chains.

function test1() {
    console.log(foo)
    var bar = "hello world";
    function test2() {
        console.log(bar);
    }
    test2();
}

test1();

So far, my best attempt at finding an explanation myself was by modifying the first code block like so:

var foo = "foo";
var bar = "not hello world :("

function test1() {
    console.log(foo)
    var bar = "hello world";
    test2();
}

function test2() {
    console.log(bar);
}

test1();

Here, the call to test2() prints out "not hello world :(" defined in the global scope. My first thought was that test2() was going up the scope chain to test1's execution context, to the global execution context, but that doesn't seem right, as it would have found a definition for bar inside of test1() before reaching the global definition. Thus, my second guess is that the definition of test2() is creating a "closure"-like capture of the global execution context when it was defined, and calling it within test1() produces a value for bar of null/undefined since that was its value at function definition (having no declaration at all to be hoisted). Thus, it does not move up into test1()'s execution context to search for the identifier.

Any explanations/resources would be tremendously helpful. I am obviously very new to the language, so apologies if my vocabulary/terminology isn't quite correct. Thanks in advance for your help.

Eggcellentos
  • 1,570
  • 1
  • 18
  • 25
Jon
  • 1
  • *test2(), being executed inside of test1(), has access to test1()'s execution context* — that is not correct. Scope is determined **lexically**, meaning "by what the code looks like as text". The statements inside `test2()` only have access to the scopes of the function itself and the functions that contain it. – Pointy Mar 16 '20 at 05:39
  • Thank you for your response @Pointy. The video that explained this concept to me was: https://www.youtube.com/watch?v=Nt-qa_LlUH0 Using the same visualizer (https://tylermcginnis.com/javascript-visualizer/) and this code, Tyler's visualizer implies that test2()'s execution context does exist inside of test1()'s. But I'm sure the visualizer could be incorrect? Could you clarify what exactly you mean by "contain it", lexically speaking? – Jon Mar 16 '20 at 05:43
  • There's a difference between the dynamic concept that that author calls "execution context" and the static concept of lexical scope. Lexical scope can be determined by *looking* at the code without running it at all. You don't know what values the variables in scope will have when the code does run, but you can determined what variables are and are not in scope just by working from the inside towards the outside. – Pointy Mar 16 '20 at 05:52
  • How would I determine the lexical scope of anonymous functions declared inside of function parameters (such as callbacks, etc) according to your definition? – Jon Mar 16 '20 at 06:23
  • Same way. It's all about the `{ }` nesting from the global level down to the body of the function. When working outwards from the function to trace the scope, you can go "out" through layers of nested `{ }` but not "in". Any variable or function declared in a `{ }` block you encounter by tracing outwards is in scope; variables in functions *inside* other `{ }` blocks are hidden. – Pointy Mar 16 '20 at 12:40
  • Ah, so a function declared in a function argument list has access to all variables in the outer blocks up to the block where the function it's being declared in and passed to resides, but does *not* have access to the variables inside of the function it's being passed to. The reason it seems like those functions have access to the internal variables is because they're passed those variables by the function using it as a callback - am I understanding that right? – Jon Mar 16 '20 at 21:46
  • Right. The basic rule is: **scope is static**. It has nothing to do with how a function is called or where it's called from. – Pointy Mar 16 '20 at 23:51
  • Gotcha. And so execution context, as I can reason now, is more the concept of "what are the *current values* of the variables that *are* in scope for this function, at this particular time of execution/when I call it?". Am I on the right track here terminology/concept wise? – Jon Mar 17 '20 at 04:42
  • Yes, that's correct. Honestly I'm not sure why that topic was such a focus in that video, as it doesn't seem particularly hard to grasp, while lexical scope and, certainly, the related concept of *closures* cause people more headaches while learning the language. – Pointy Mar 17 '20 at 12:34

1 Answers1

1

Where it's called from and the scope of the caller doesn't matter. What matters for resolving local variables is where it's declared.

test2() cannot access bar because there is no variable named bar in that scope when the function is declared. There is no way to manipulate that local scope after the fact. It's set in stone.

This means that a function can access a variable in the local scope, or in any scope that contains the declaration of that function. That means following the pairs of braces {} that contain the declaration of that function all the way back to the root of the file. Think of it like a tree: your function will have access to any variable between that function and the root of the tree. It will not have access to other branches of that tree.


And this is a really good thing. It enforces good encapsulation. You want your functions to be a black boxes from the perspective of the caller. You want local variables in your function to be truly local. And you want to know what variables a function has access to when you are writing the function, rather than being surprised by what values pop in when you run the function.

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • Thank you for your response, Alex. What you're saying makes sense. That concept is certainly not communicated well by the readily available info/articles on execution contexts, so I appreciate your explanation. Could you possible explain how to determine scoping for anonymous functions declared in parameters (such as anon functions passed as callbacks?) – Jon Mar 16 '20 at 06:20
  • 1
    Exactly the same rules apply for that. An anonymous function also only has access to the variables in the scope where that function is created. – Alex Wayne Mar 16 '20 at 06:50