links: JS MOC


Why Global Scope?

Most applications are composed of multiple (individual) JS files. Let’s understand how all those separate files get stitched together into a single runtime context by JS engine.

With respect to browser-executed applications there are three main ways:

  1. Using Es Modules (not transpiling them into some other module bundle format), these files are loaded individually by the JS Environment. Each module then imports references to whichever other modules it needs to access. The separate module files cooperate within these shared imports without requiring shared global scope
  2. If a bundler is being used, all the files are typically concatenated into one single file that can be processed by a JS engine. Even with all the pieces of the application co-located in a single file, some mechanism is necessary for each piece to register a name to be referred by other pieces, as well as some facility for that access to occur.

In some build setups, the entire contents of the files are wrapped in a single enclosing scope, such as a wrapper function (example: UMD a.k.a universal module). Each piece can register for access from other pieces by way of local variables in that shared scope. For example.

(function wrappingOuterScope() {
	var moduleOne = (function one() {
		// ...
	})()
	
	var moduleTwo = (function two() {
		// ...
		function callModuleOneMethod() {
			moduleOne.someMethod()
		}
		// ...
	})()
})()

In above example moduleOne and moduleTwo local variables are inside the wrappingEnclosingScope() function scope are declared so these modules can access each other for cooperation

The wrappingOuterScope acts as a replacement for global scope, you can think of it as application level scope, a bucket where all top level identifiers are stored.

  1. And finally, whether a bundler tool is used for the application or whether the (non-ES module) files are simply loaded in the browser via script tags or other dynamic JS resource loading, if there is no single surrounding scope encompassing all these pieces, then global scope is the only way for them to cooperate each other.
var moduleOne = (function one(){
    // ..
})();
var moduleTwo = (function two(){
    // ..
 
    function callModuleOne() {
        moduleOne.someMethod();
    }
 
    // ..
})();

In above example, since there is no surrounding function scope, these moduleOne and moduleTwo declarations are dropped into the global scope. This is effectively same as:

module1.js:

var moduleOne = (function one() {
	// ..
})();

module2.js:

var moduleTwo = (function two(){
    // ..
 
    function callModuleOne() {
        moduleOne.someMethod();
    }
 
    // ..
})();

If these files are loaded separately as standalone .js files in a browser environment, each top-level variable declaration will end up as a global variable, since the global scope is the only shared resource between these two files — they’re independent programs in perspective to JS engine.

The global scope is also where:

JS exposes its built-ins:

  • primitives null, undefined, Infinity, NaN
  • natives: Date(), Object(), String() etc
  • global functions: eval(), parseInt() etc
  • namespaces: Math, Atomics, JSON
  • friends of JS: Intl, WebAssemly

The environment hosting the JS exposes it’s own built-ins:

  • console (and it’s methods)
  • the DOM (window, document, etc)
  • timers: (setTimeout(..), etc)
  • web platform APIs: navigator, history, geolocation, WebRTC etc

These are some of the many globals your program interacts with.

Where exactly is this global scope?

Different JS environments handle the global scope differently

Browser “window”

Any .js loaded in the browser environment via <script> tag, or <script src=..> script tag in markup or even dynamically created <script> DOM element, will mostly be pure environment of JS

If you access the global objects you will the properties declared in global scope

var studentName = "Subramanya";
 
function hello() {
	console.log(`Hello ${studentName}`)
}
 
hello()
// Hello Subramanya

The same can be seen with window object

var studentName = "Subramanya";
 
function hello() {
	console.log(`Hello ${window.studentName}`)
}
 
window.hello()
// Hello Subramanya

That’s what expected and so it can be called pure, but there are some naunces here too.

Globals shadowing Globals

If you declare a variable with let or const that is already in the global scope, the global scope variable gets shadowed by this declared variable

window.someThing = 42;
 
let someThing = "Hello";
 
console.log(someThing);
// Hello
 
console.log(window.someThing);
// 42

In order to avoid shadowing, use var at the outer most scope a.k.a global scope and let and const for block scopes

Dom Globals

Any id tags created in html are available in JS environment

consider this markup:

<ul id="my-todo-list">
   <li id="first">Write a book</li>
   ..
</ul>

And the JS for that page could be

first;
// <li id="first">..</li>
 
window["my-todo-list"]
// <ul id="my-todo-list">..</ul>

What’s in a (window) name?

name is a predefined global in browser’s context, so when you declare a name variable with var, you can observe this naunce

var name = 42
 
console.log(name, typeof name)
// "42" string

It prints the string “42” instead of number and when you check it’s type it shows as a string. The problem here is name is a getter and setter function in global scope. Now when you declare the name with 42 it converts to string

Web Workers

Web workers are web platform extension on top of browser-JS behavior, which allows to run a JS file in a completely separate thread. The global scope is called as self. It doesn’t have DOM elements but contains few APIs like navigator

var studentName = "Subramanya";
let studentID = 42;
 
function hello() {
    console.log(`Hello, ${ self.studentName }!`);
}
 
self.hello();
// Hello, Subramanya!
 
self.studentID;
// undefined

Web workers also contains pure global scope.

The web workers on on different threads which will be helpful to run CPU intensive tasks.

Developer Tools Console/REPL

The developer tools mainly exist for Developer Experience, So it doesn’t have to adhere to JS spec and relax some errors for developer convenience. It’s not a pure JS environment, but rather emulation of it, so there will be observable differences in behavior like

  • The behavior of global scope
  • Hoisting
  • Block scoping declarators (when used in outermost scope)

The key take away is Developer Tools are not the place to verify explicit and naunced behaviors of JS environment.

ES Modules(ESM)

ES6 introduced first class support for module pattern, One of the most obvious impact of using ESM is how it changes the behavior of the top-level scope in a file.

Consider this example

var studentName = "Subramanya";
 
function hello() {
    console.log(`Hello, ${ studentName }!`);
}
 
hello();
// Hello, Subramanya!
 
export hello;

If the above code is loaded as ES Module, even though, it runs same, there is differences in outermost scope.

The studentName and hello are not added to global window object, rather they are module-wide global and can’t be accessed in other files, unless you import it. It’s kind a function is wrapped around this file, which prevents adding these variables to window object.

You can still access many globals because of lexical scope, as you can think of on top of module scope there is global scope.

The studentName and hello doesn’t gets added as properties to module’s global scope as there’s no module wide scope object.

ESM encourages minimization of reliance on the global scope. you just import necessary modules and operate.

Node

Node treats all files as modules (ES Module or Common JS). The effect is that the top level can never actually global scope.

Consider this Example:

var studentName = "Subramanya";
 
function hello() {
    console.log(`Hello, ${ studentName }!`);
}
 
hello();
// Hello, Subramanya!
 
module.exports.hello = hello;

Before processing Node wraps this inside a function, so var and function declarations doesn’t gets added to global scope.

You can think of it like this (Not actual code):

function Module(module,require,__dirname,...) {
    var studentName = "Subramanya";
 
    function hello() {
        console.log(`Hello, ${ studentName }!`);
    }
 
    hello();
    // Hello, Subramanya!
 
    module.exports.hello = hello;
}

Node essentially invokes the Module(..) when it’s required. All require statements are essentially injected in the scope of every Module.

So to define global variables in Node, you can use global keyword similar to window in browser. global is reference to the real global object in Node.

global.studentName = "Subramanya";
 
function hello() {
    console.log(`Hello, ${ studentName }!`);
}
 
hello();
// Hello, Subramanya!
 
module.exports.hello = hello;

Remember, the identifier global is not defined by JS; it’s specifically defined by Node.

globalThis

As of ES2020, JS has finally defined a standardized reference to the global scope object, called globalThis. This is cross environment compatible.

Yet another trick to obtain a reference to the global scope object looks like this

const the globalScopeObject = (new Function("return this"))();
Note:
A function can be dynamically constructed by using Function() constructor similar to eval(..). This will automatically run in non-strict mode and creates functions only on global level

Globally aware


tags: scope, global source: