1

I want to make sure to master the concept of variable scope. Ideally, I'd like a definition of the form:

  • x is the scope of identifier i just in case x ...

or of the form:

  • the scope of identifier i is identical to ...

Although I am new to programming, I have read through some general material on variable scope and some material on variable scope in JavaScript (including this). However, I cannot seem to find a precise, descriptive definition. I have tried to come up with my own, but I've noticed that it does not always fit how others speak.

It is probably best to illustrate my current understanding. Let's take 'context' nearly as a primitive to mean something like a region of code delimited by curly braces. Think about the main region of code, functions, and blocks (I bet I've left some things out). Let's think of variables in the broadest sense to include variables, parameters, and function names or identifiers, more generally. Then:

  • the scope of a variable v is identical to the set of contexts within which v is accessible.

Here's an example:

let a = 'foo';
function bar(param) {
  param += 2;  
  function newFunc() {
   console.log(param);
  }  
  newFunc(); 
}

Here we have three contexts:

  • The main context.
  • The context that results from defining the bar function.
  • The context that results from defining the newFunc function.

According to the above attempt at a definition, we have two scopes:

  • There is the global scope of built-in names like console, null, etc, as well as defined names a and bar. This scope is the set of contexts 1, 2, and 3 above.
  • There is the scope of param and newFunc. This scope is the set of contexts 2 and 3 above.

No scope comprises only context 3 because there is no new variable defined in context 3.

So, defining a function creates a context. The scope of a variable includes that new context just in case the variable is accessible from that context.

However, I know that people will say that the above code example includes 3 scopes and more or less identify those scopes with what I've called 'context'. However, I cannot make sense of this, and I do not know what precise definition of variable scope permits them to say there are 3 scopes in the above. It seems strange to me to talk about scope without talking about a variable/identifier's scope. More precisely, it seems context is undoubtedly independent of variables, but scope is not. Scope is a property of variables/identifiers; that's why we are talking about variable scope or the scope of some identifier or another when we talk about scope.

I have also seen people talk about variables going in and out of scope. To me, variables have a scope, period. The program moves from context to context and thereby moves in and out of the variables' scopes. Variables go in and out of context as the program runs. They might even always be in context (if they have a global scope) or always out of context (e.g., if they have function scope and that function is never invoked). But it seems inappropriate to say that variables go in and out of scope or that a variable's scope changes.

It has occurred to me that it is not correct to speak of the scope of an identifier unless there is a unique context within which an identifier is accessible. So, instead, we might have

  • S is a scope of identifier v just in case S is a context within which v is accessible.

Given this definition, we have three scopes:

  • Context 1 is a scope of a and bar.
  • Context 2 is a scope of a, bar, param, and newFunc.
  • Context 3 is a scope of a, bar, param, and newFunc.

In the above code, there are no contexts for which it is true that it is the scope of an identifier. For that to be the case, there would have to be a unique context for which the identifier is accessible. For example, if the code omitted newFun, then there wouldn't be a third context, and context 2 would be the scope of param and newFunc.

So, defining a function creates a context. The context is a scope of a variable just in case the variable is accessible within that new context.

There are problems here, though, too. Consider the following code:

for (let i = 0; i < 5; i += 1) {
  if (i % 2 === 0) {
    console.log(i);
  }
}

I have seen people say that there are three contexts here, but the inner, nested one of the if block is not a scope. But on the definition I just gave, that block would be a scope of i as well as built-ins like console.

That's where I am currently. Thanks, in advance, for your respectful help.

UPDATE

Given @A. Chiesa's response, I wrote a short thing to try to make sense of things. Perhaps that document says it best? Thanks again.

w-h-a
  • 37
  • 3
  • 3
    "*t seems strange to me to talk about scope without talking about a variable/identifier's scope.*" it's not variables that define the scope. You can have a *functional scope* that has no variables in it. It's still a valid scope but it's empty. – VLAZ Feb 10 '21 at 12:56
  • 2
    Lanfuage and terms are fluid. Variables have scope; of someone says it's "out of scope" they just mean it doesn't exist in the current context. Contexts exist whether or not there's anything "local" in it--functions most definitely have a context even if they define no local variables; that's what allows closures to exist. – Dave Newton Feb 10 '21 at 13:00
  • 1
    I'd add that it's probably not worth this many words at this stage, and that there's no single vocabulary that will fit every single developer or every language. – Dave Newton Feb 10 '21 at 13:07

1 Answers1

3

Disclaimer: what follows is an explanation in my own words. I'm pretty confident about the concepts, but take the specific terms with a grain of salt: it could be that the specification or some other developer uses different terms.

In JavaScript the "scope" that people tend to refer to is the so called "lexical scoping" of functions.

IMHO it's best to think about scoping not from the point of view of a defined variable (or constant), but from the PoV of each line of code.

E.g.:

let a = 'foo';

function bar(param) {
  param += 2;  
  function newFunc() {
   for(let i = 0; i < 10; i++) {
     let v = 5;

     // what variables are "in scope" in this line?
   }
  }  
  newFunc(); 
}

JavaScript uses lexical scoping, that is: it doesn't matter when or how you call some code, what is accessible to the code itself is defined by the structure of the code itself (i.e., by the nesting of the brackets).

It's very simple: for a certain statement, you just look at every code block that contains the statement. In the commented line above, the code can access to many variables:

  • v
  • i
  • param
  • a
  • everything in the global scope

Albeit the structure of the code defines what is accessible by every statement, the lifetime of each variable is determined by the lifetime of the code using it. In other words, a variable whose lifetime (I'm using this term instead of scope, because I think it better describes the relevant part) would be terminated by, say, the end of a function block, can be "captured" by some other function, and end up in the execution scope of the latter. This is the concept of closure, and the source of many interesting techniques.

So, I would say that:

  • the lexical scope of a block of code includes every surrounding "structure" (code block, function, class or module boundary).
  • a variable is "in scope" to every line that can "see" it via lexical scoping.

A variable lifetime begins when it's corresponding var, let or const declaration is executed, and ends when there is no code that keeps it "alive", either directly or via a closure.

I'm pretty sure that many extra details could be added, specifying the old var statement, with it's functional scoping, and comparing it with let and const block scoping, but this answer seems already pretty long :D

Alberto Chiesa
  • 7,022
  • 2
  • 26
  • 53
  • Hi @A. Chiesa. Thank you for your help. I wrote a [short thing](https://github.com/w-h-a/js_foundations/blob/main/scope-note.md) to try to make sense of things now given your response. I would appreciate it if you'd have a look to see if you agree or not. Thanks again. – w-h-a Feb 10 '21 at 16:59
  • 1
    Just pay attention that the "global" scope is not some fancy way of saying that a variable is global. In every JS environment there is a proper global object. In browser's, it's called `window` (in node it's `global`), so if you write `var v = 10; console.log(window.v);` you'll get 10. Your example with a function called `main` loses completely this aspect. – Alberto Chiesa Feb 10 '21 at 17:10
  • Also, the explanation about what is a block scope is fairly convoluted: just "chase" curly braces, and you'll find every possible block. The function lexical scope includes every block in which the function definition is nested into. I would say there is no such thing as a "variable scope": that concept makes the whole mechanism seem more complex than it is. – Alberto Chiesa Feb 10 '21 at 17:14
  • First, I fixed the global scope definition in the linked document. Second, it is not enough to "chase" curly braces to find every block that is a scope. If you have a less convoluted definition of block scope, I'd appreciate a more precise and descriptive response. Maybe block scope is convoluted? Thanks again for your help. I think I'm getting it better. I am curious to see what others have to say too. – w-h-a Feb 10 '21 at 17:36
  • ` it is not enough to "chase" curly braces to find every block that is a scope`. Why? The only exceptions that come to my mind are cycles that define a cycle variable, with a single command in them. In every other case (global, class, function, block, nested functions, etc) you have always curly braces to denote the code block. Am I missing something? The algorithm is easy enough: when you reference a variable in a statement, the engine searches for a variable declared in the innermost block. It goes on, looking to the containing block, until (1) it finds a variable or (2) it errors out. – Alberto Chiesa Feb 10 '21 at 17:54
  • In my original post: Consider the following code: ```js for (let i = 0; i < 5; i += 1) { if (i % 2 === 0) { console.log(i); } } ``` I have seen people say that the inner, nested block is not a scope. It doesn't have a `let` or `const` declaration. – w-h-a Feb 10 '21 at 18:01
  • 1
    Irrelevant, IMO. Discussions about if a block of code without variables is a scope or not are just like discussing if a tree falling when no one can ear it makes noise or not. Who cares? The only relevant thing on variable resolution is where is the code that accesses the variable and where the referenced variable is defined. Everything else seems to me irrelevant (well, it is almost certainly implemented differently inside the Just-in-time optimizer, in V8, but it doesn't make any practical difference when coding). – Alberto Chiesa Feb 10 '21 at 21:15
  • 1
    In other words, when you access the variable i, it's completely irrelevant if the engine skips checking the innermost block because there are no definitions, or checks it and finds that the i variable is not defined there. Who cares? The thing is that the body of the for loop can access to the i variable because it is defined inside a containing lexical scope. When the for loop ends, there is no code still referencing i, so it gets garbage collected. If the native code optimizer allocates i on the stack or the heap, if it knows its type or not, it's an implementation detail. – Alberto Chiesa Feb 10 '21 at 21:20