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:
Scopes of a function in a script with a let
variable:
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:
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:
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 there 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 thewindow
global on browsers or the newerglobalThis
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 usingfor-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 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 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
.