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.

9
  • 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. Commented Mar 16, 2020 at 5:39
  • Thank you for your response @Pointy. The video that explained this concept to me was: youtube.com/watch?v=Nt-qa_LlUH0 Using the same visualizer (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? Commented Mar 16, 2020 at 5: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. Commented Mar 16, 2020 at 5:52
  • How would I determine the lexical scope of anonymous functions declared inside of function parameters (such as callbacks, etc) according to your definition? Commented Mar 16, 2020 at 6: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. Commented Mar 16, 2020 at 12:40

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

2 Comments

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.