global pollution in JavaScript

JavaScript is far forgiving… Without realising, we land up adding things in the global scope and later run into massive problems.

Let’s first take a look at JavaScript process map to understand how JavaScript organises functions, variables and properties in memory. Entire magic happens in the HEAP.

Process Map of JavaScript

So, there are different types of “scopes“, like global, local, closure etc. Anything with var and let doesn’t go into the global scope but some variables (without var/let) and functions will go into global scope

// Global scope
g_a = 1;
// local to enclosing anonymous function (node.js)
var a = 1;
// Global scope
function hello(name) {
console.log("Hello", name);
}
// Local scope
var greet = function(name) {
console.log("Greetings", name);
}
Global vs Local

As JavaScript programmers, we may not think about such things while writing them but later we may run into trouble.

It gets worse

Functions have dual nature in JavaScript

  1. Act as functions (duh!)
  2. Act as constructors (huh?)

And how do we know which is which? Well, there is no definitive way of determining that.
However, there is a convention that if the function name is camelCase, it is to be used like a function

// add is camelCase
// Use it like a function
function add(a, b) {
return a + b;
}
// Use it like a function
let result = add(1, 2);
console.log("result", result);
// Do NOT use it with new
// Sigh! It doesn't give an error
let unexpected = new add(1, 2);
// NOW, it gives weird result
console.log("unexpected", unexpected);
Output
result 3
unexpected add {}
view raw camelCase.js hosted with ❤ by GitHub
Using function like a constructor

Functions can also be used to create objects as they act as constructors

/*
If a function name starts with uppercase
It is "intended" to be used as a constructor
Imagine a "class" around it, it will make sense
*/
// class Person {
function Person(id, name) {
this.id = id;
this.name = name;
}
// }
// Now let's create the object
let eich = new Person(1, "Brendan");
console.log(eich.id, eich.name);
// OOPS.. we called is like a function
// But there is no error
let brendan = Person(2, "Wrongdon");
// Why error? where did id and name go?
console.log(brendan.id, brendan.name);
Output
console.log(brendan.id, brendan.name);
^
TypeError: Cannot read property 'id' of undefined
Functions as constructors

Sheesh! All this is soooo confusing. Not really! Let’s take a look at Digital Concept Visuals to understand what is happening inside the Process Map

Mystery of the missing properties

When we do
let eich = new Person(1, "Brendan");
we create an “object” from the function.

However, when we just call it as a function
let result = Person(2, "Wrongdon");
there is not object created, we are just running the function with this point to where? global dictionary

OOPS! We just polluted the global dictionary with id and name.

function Person(id, name) {
this.id = id;
this.name = name;
}
let eich = Person(1, "Brendan");
console.log(global.id, global.name);
Pollution of global scope

So how do we avoid id?

  1. use new when function name is upper – Person, Emp, Product
  2. call like regular function when its camelCase – add, getProduct, calcDiscount

All that is fine, but what if someone else calls our functions incorrectly?

Well, there are a number of ways we can can handle it.

  1. new.target
  2. globalThis
  3. Pollyfill globalThis
Let's start with new.target
// Constructor function
// Not to be used as normal function
function Person(id, name) {
// Was this called using new?
if (!new.target)
throw "Error: use 'new Person()'";
this.id = id;
this.name = name;
}
// Expect error here
let result = Person(1, "Brendan");
Output
throw "Error: use 'new Person()'";
^
Error: use 'new Person()'
view raw new.target.js hosted with ❤ by GitHub
Let’s try new.target

Yay! We got it!
Well… almost

Sometimes we use functions like “injectors”… ‘Point and shoot’.
If I am writing a function, I can use another function to inject properties and methods into my own object by simply calling them using .call and .apply

// Constructor function
// Not to be used as normal function
function Person(id, name) {
// Was this called using new?
if (!new.target)
throw "Error: use 'new Person()'";
this.id = id;
this.name = name;
}
// We ARE creating an object of Visitor
function Visitor(id, name, visiting) {
// Using Person as "injector"
// Sadly! It doesn't work
Person.call(this, id, name);
this.visiting = visiting;
}
let ryan = new Visitor(2, "Dahl", "Eich");
Output
throw "Error: use 'new Person()'";
^
Error: use 'new Person()'
call and apply

Sadly, Person.call does not get called with new, hence Person function rejects it.
What if we want ONLY normal calls like Person(1, “Eich”) to be rejected and not these

  • new Person(1, “Eich”);
  • Person.call(this, 2, “Dahl”);
  • Person.apply(this, [2, “Dahl”]
globalThis to the rescue

Basically we just want to reject it if called as normal function. So what is unique about it when it’s called as global function? THIS!!!

If called as normal function this is global. So let’s just test for THAT

// Constructor function
// Not to be used as normal function
function Person(id, name) {
// Was this set to global?
if (this == global)
throw "Error: use 'new Person()'";
this.id = id;
this.name = name;
}
// Expect error here
// let result = Person(1, "Brendan");
function Visitor(id, name, visiting) {
Person.call(this, id, name);
this.visiting = visiting;
}
let ryan = new Visitor(2, "Dahl", "Eich");
view raw global.js hosted with ❤ by GitHub

Yay! We’ have done it! Umm.. not really
When running in node.js, it has a global dictionary, but when running in browsers, there is no global but it’s called window

Oh ok! Let’s update it then

// Constructor function
// Not to be used as normal function
function Person(id, name) {
// Was this called using new?
if (this == global || this == window)
throw "Error: use 'new Person()'";
this.id = id;
this.name = name;
}
// Expect error here
// let result = Person(1, "Brendan");
function Visitor(id, name, visiting) {
Person.call(this, id, name);
this.visiting = visiting;
}
global or window

Oops.. wherever we execute it, we will get an error… either for global or for window.

Introducing globalThis

globalThis is a property recently added to JavaScript, which gives us reference to the global/window/self

// Constructor function
// Not to be used as normal function
function Person(id, name) {
// Was this called using new?
if (this == globalThis)
throw "Error: use 'new Person()'";
this.id = id;
this.name = name;
}
// Expect error here
// let result = Person(1, "Brendan");
function Visitor(id, name, visiting) {
Person.call(this, id, name);
this.visiting = visiting;
}
let ryan = new Visitor(2, "Dahl", "Eich");
view raw globalThis.js hosted with ❤ by GitHub
globalThis

Finally!!! We have a way of rejecting ONLY pure function call but allow new, call and apply.
But will it work with earlier version of JavaScript? NOPE!
So, what do we do? Polyfill!

I am not going to go into details of how to polyfill globalThis but I can point you to another article which shows how ugly it’s going to be

https://mathiasbynens.be/notes/globalthis

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s