'What is the purpose of the script scope?

When inspecting scopes of a function in the DevTools console I noticed a "script" scope. After a bit of research it seems to be created for let and const variables.

Scopes of a function in a script without const or let variables:

the global scope

Scopes of a function in a script with a let variable:

a global scope and a script scope

Yet the following prints 1 in the console - variables in the script scope can still be accessed from other scripts:

<script>let v = 1</script>
<script>console.log(v)</script>

I've heard of ES6 modules in which top-level variables won't be accessible from outside a module. Is that what the scope is used for or does it have any another purpose?



Solution 1:[1]

When you declare a variable using var on the top level (i.e. not inside a function), it automatically becomes a global variable (so in browser you can access it as a property of window). It's different with variables declared using let and const—they don't become global variables. You can access them in another script tag, but you can't access them as properties of window.

See this example:

<script>
  var test1 = 42;
  let test2 = 43;
</script>
<script>
  console.log(test1); // 42
  console.log(window.test1); // 42
  console.log(test2); // 43
  console.log(window.test2); // undefined
</script>

Solution 2:[2]

JavaScript doesn't have "script scope."¹ What you're seeing there is just what Google's V8 JavaScript engine calls the part of the global environment that holds the new style of lexically-scoped globals created when you use let, const, and class at global scope. They're still globals, but they're different from the older style of globals created by var and function declarations at global scope (which V8 shows in Global under [[Scopes]]). The V8 debugger lists the two types of globals in those two different places.

You can stop reading here if you like, but if you want the nitty-gritty details, read on. :-)

So why are there two global parts to the global environment? In a word: History.

JavaScript's original form of globals (global var-scoped bindings²), had multiple issues. The main two were:

  • They weren't just globally-available identifiers, they were also properties on the global object (this at global scope, also accessible via the window global on browsers or the newer globalThis global defined by the spec). That meant you could look in the global object to find things that you didn't know the name of (by using for-in, Object.keys, or similar).
  • Repeated declarations for the same identifier weren't errors.

Aside from those issues at global scope, var had the issue that it didn't have block scope; and function declarations (which also create var-scoped bindings) in blocks were unspecified but allowed as an extension, resulting in largely incompatible semantics for them across JavaScript implementations.

When it came time to add a new way of declaring things with better semantics (let, const, class; "lexically-scoped bindings"), the committee that moves JavaScript forward (ECMA TC39) had to figure out how those new semantics would work at global scope. Their solution was to have two parts to the global environment — one for the old style, and other for the new style — but still treat it "logically" as a single environment. From the specification:

A global Environment Record is logically a single record but it is specified as a composite encapsulating an object Environment Record and a declarative Environment Record.

An "environment record" is a conceptual object that holds bindings² (variables and such) and some other things. Joining that up with what you're seeing in your screenshot:

  • The "object Environment Record" is the record that uses the properties of the global object for the var-scoped bindings. This is what V8 calls Global under [[Scopes]].
  • The "declarative Environment Record" is the record that holds the lexically-scoped bindings (directly, not in a separate object). This is what V8 calls Script under [[Scopes]].

In your screenshot, you have let f, which creates a lexically-scoped binding called "f", so V8 shows that under [[Scopes]].Script. If you had var f instead, V8 would show that under [[Scopes]].Global. But again, both are globals.


What does it mean when they say the two parts of the global environment are "logically" a single record? Basically they mean that it's not just two nested environments (although in many ways it behaves like it is), there is only one global scope (even though there are two parts to the environment related to it). One way you can see that is that you can't declare something with both var and let at global scope, it's an error:

var a = 1;
let a = 2; // SyntaxError: Identifier 'a' has already been declared

If they were just nested environments, you'd be allowed to do that — but how confusing that would be!

But while they aren't just nested, they are nested. You can prove that by creating the var-scoped global without using var (by assigning to a property on the global object):

window.a = "var-scoped a";
let a = "lexically-scoped a";
console.log(a);         // "lexically-scoped a"
console.log(window.a);  // "var-scoped a"

let b = "lexically-scoped b";
window.b = "var-scoped b";
console.log(b);         // "lexically-scoped b"
console.log(window.b);  // "var-scoped b"

It perhaps goes without saying that you shouldn't do that on purpose, but it demonstrates the nesting aspect of the dual environment.


¹ It does have module scope, which is different, but the top-level code in non-module scripts like yours are executed at global scope.

² A binding is the combination of a name (like a) and a storage slot for its current value. Variables are bindings. So are constants, parameters, the variable created by a function declaration, and various built-in things like this.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Michał Perłakowski
Solution 2