Javascript: what I didn't understand
Mehdi Zed
Posted on April 17, 2021
Javascript is one of the most accessible languages. But between those who use it and those who master it, there is a clear difference. Javascript is full of nuances, fuzzy behaviors and hidden concepts. It'll drive you crazy if you don't know them.
The Javascript trap
A long time ago, in a galaxy far, far away, I arrived in a new team. Behind me, a strong PHP specialization. This day was important. I gave up my PHP religion to join a team of Javascript religion.
At this point, I am convinced of two things. Javascript is easy and I already mastered it perfectly. No need to really understand how the bowels of the language work to use it. It's going to be fine.
But soon I began to see some disturbing things on the horizon. I came across code, concepts and terms that were completely opaque. I didn't get worried right away. It was quite far from my area of intervention.
I should have worried right away.
A few weeks later, my first big mission in the team fell on my desk.
The entire rewriting of a hinge service for the product.
Without going into details, we can compare this service to a kind of CDN. The client sends a ZIP file, my service has to manage a lot of things. On-the-fly file extraction with recursion (zip in zip), upload, caching, static file service, versioning, metadata. All this while keeping 100% of calls under 200ms.
Doing this kind of thing correctly requires an internal understanding of how Javascript works. I didn't know that yet. I was about to suffer in front of mistakes and incomprehensible behaviors.
The Javascript trap had just closed on me.
On the surface, Javascript is very accessible and you quickly do wonders with it. A superficial knowledge of internal mechanics is often sufficient. Hence the number of people who use it without really knowing what they are doing.
But when you end up tackling more complex things, you're quickly lost, and your impostor syndrome begins to stare you intensely.
Unknown variables
Before I tell you about what drove me crazy when I started this service, let's go back a few years. Like many people, I learned Javascript on the job. I had to do it so I started doing it.
As required by the time, I write jQuery. I thought i was a god at it. I managed to do everything that was asked of me. In spite of what I was thinking, from time to time I got some big slaps in the face.
Simple things don't work. It bugs for no reason. And strangely enough, the fact that I'm banging my keyboard hard doesn't solve the problem.
My problems came from the first thing I didn't understand with Javascript: the internal workings of variables and types.
To understand what i'm talking about, let's look at some code.
*What will this code display and why?*
const originalEzio = {
"name": "ezio Auditore da Firenze",
"weapon": "Hidden Blade",
"metadata": {
"version": "Original",
"type": "Assassin"
}
};
originalEzio.name[0] = 'E';
function getHeroCopy(originalHero) {
let copyHero = {
name: originalHero.name,
weapon: originalHero.weapon,
metadata: originalHero.metadata
};
copyHero.metadata.version = 'Copy';
return copyHero;
}
const copyOfEzio = getHeroCopy(originalEzio);
console.log('Original : ', originalEzio);
console.log('Copy : ', copyOfEzio);
Yes, I know, it's look like a stupid Javascript trivia question. But please, play the game, take the time to predict what it will display.
Let's check your prediction by pressing play on the Repl just below.
If you can't explain this result, you're missing something in the foundations of language. A short explanation in a few words.
Variables are divided into two main categories: primitives and complexes.
- Primitives (string, number, boolean, …) point to unique values.
They are immutable. Hence the fact that the string does not change (line 10). By the way, if you add "use strict" at the beginning of the file, it throws immediately. In strict world, Javascript does not allow this devilry.
- Complexes (object, …) point to value references.
They are mutable. Line 16, I refer to the metadata object of the original hero and assign it to the metadata object of the copy. By changing the copy, I therefore change the reference of the original.
When I started, I didn't have these notions. And believe me, it's no fun not having them. A lot of people don't have them.
The goal today is not to give you a lesson. The goal is to point out the pitfalls I've encountered. To make sure that you avoid them.
I have a recommendation for you at the end of the article to understand and overcome all these pitfalls.
But before that, let's continue to point out the places where I wallowed.
What the fuck is this
For the rewriting of the service, I was helped by many internal and external library. Some more recent than others. Some better done than others. They used all the object dimension of Javascript.
Or more precisely, prototype oriented programming, an incomplete form of object programming.
Even today, despite the syntactic sugar of the classes, it is still prototypes. Javascript is not really an object-language. See you on twitter for some fight those who disagree.
// what you use
class Assassin {
constructor(name) {
this.name = name;
}
getCreed() {
return "Nothing is true, everything is permitted.";
}
}
//---------------
// what JS really does behind
function Assassin(name){
this.name = name;
}
Assassin.prototype.getCreed = function() {
return "Nothing is true, everything is permitted.";
}
In short, I got to know the contexts in Javascript. With these schizophrenic perimeter rules. I immediately started using my head to smash my keyboard.
Again, a annoying trivia question.
***What will this code display and why?***
const altair = {
name: "Altaïr Ibn-La'Ahad",
templarsKilled: ['Tamir', 'Talal', 'Sibrand'],
showTemplarsKilled: function() {
console.log(`List of templar killed (${this.templarsKilled.length}) by ${this.name}`)
this.templarsKilled.forEach(function(templarKilled) {
console.log(`${this.name} killed ${templarKilled}`)
});
}
};
altair.showTemplarsKilled();
You can check your prediction with the Repl just below.
Why doesn't the second log (line 8) work? Why does the first log (line 5) work? Why using an arrow function (line 7) solves the problem?
If you are not able to answer these questions, it is because the famous (this) Javascript context is blurred for you. And that's understandable. In Javascript, the context doesn't behave at all like in other languages.
We're dealing with a monster.
In theory, "this" represents the context of the function. An object associated with the invocation of the function. Except that it is not so simple. In truth, it will be determined according to how it is called.
Let's look at some examples.
Call in a function, the context will be the global object. If you don't know it, you tragically change the global object. This is evil.
this.creed = "Nothing is true, everything is permitted.";
function showCreed() {
console.log(this.creed)
}
showCreed();
Except in strict mode. In strict mode it is undefined. You don't know it, this time everything goes wrong.
"use strict"
this.creed = "Nothing is true, everything is permitted.";
function showCreed() {
console.log(this)
}
showCreed(); // undefined
Call in method of a function, the context will be the object in question, as we want. This is why the "showTemplarsKilled" function above works. But not the next nested function. The next one has its own context.
showTemplarsKilled: function() {
// this -> objet context
console.log(`List of templar killed (${this.templarsKilled.length}) by ${this.name}`)
this.templarsKilled.forEach(function(templarKilled) {
// this -> function context
console.log(`${this.name} killed ${templarKilled}`)
});
}
I don't know if you've ever seen code create variables like "self" or "_this" that passed the current context? That's exactly why. A relatively disgusting hack to keep the current context.
showTemplarsKilled: function() {
const self = this;
console.log(`List of templar killed (${self.templarsKilled.length}) by ${self.name}`)
self.templarsKilled.forEach(function(templarKilled) {
console.log(`${self.name} killed ${templarKilled}`)
});
}
Today, the most elegant way is to use an arrow function. In addition to making our code more readable and shorter, it passes the current context to the called function. Neat.
showTemplarsKilled: function() {
console.log(`List of templar killed (${this.templarsKilled.length}) by ${this.name}`)
this.templarsKilled.forEach(templarKilled => console.log(`${this.name} killed ${templarKilled}`));
}
I'm telling you I don't want to lecture, but I'm throwing myself into explanations anyway. Please stop me when I start going off all over the place like that.
Anyway, while I was doing this famous service, I was far from suspecting all this. And all these rules of context depending on where and how you call out made me freak out.
It made the speed and quality of what I was producing … let's say questionable. The first few weeks on it were laborious. And even if that wasn't true, I had the impression that my team was beginning to doubt what I could bring to the table.
With a lot of (too much) time and pain, I gradually managed, module by module, to produce something. However, this was just the beginning of my discoveries. I was not at the end of my pains.
Deployment
I'll pass the various adventures on the road, let's go straight to the deployment. At that point I am convinced that my stuff is working. I have 3 million tests. It's been running on dev for a week. I would have gladly bet an arm and two legs.
Monday morning, I finally deploy the service, it works perfectly.
But as the day went by, the more users were gradually using the new version, the more I saw the response time increase worryingly. In the middle of the afternoon, the first email from a customer arrives in my inbox.
This is clearly related to my service.
But even when i was looking precisely at the slow code, I didn't understand. The response times kept getting longer and longer. I was more and more in the fog.
It wasn't a big mistake It was a collection of subtle little errors that slowed down my application. Let's take a closer look at one of them. I promise, last interview question, then I'll leave you alone.
What's wrong with the following code?
function _load (assetFile, assetRoute) {
return this.cdn.getFileInfo(assetFile)
.then(assetInfo => this.setAssetInCache(JSON.Stringify(assetFile), assetInfo))
.then(() => this.getAssetFromCache(assetRoute))
.then(data => {
if (data) {
return Promise.resolve(data)
} else {
return Promise.reject("Can't get asset from cache.")
}
})
.catch(error => Promise.reject(error))
}
The problem is line 5 with the use of JSON.stringify. This is a blocking operation. In a non-blocking asynchronous world, you have to be very careful with this kind of thing.
JSON.stringify blocks the thread it is in. Since Javascript is single thread, this is problematic. So yes, the promise gives a delay to the blocking. But when the stringify executes, nothing executes until it's finished.
Thus blocking all the rest of the application.
Most of the time, stringify is not a problem. What needs to be stringified is so small that the function is done almost instantaneously. Except that here, thousands of files -more or less large- are processed simultaneously.
Millisecond by millisecond, the response time went up to 1 second per call!
The more users used the application, the more it was an ordeal for everyone.
That's the day I really started to get interested in the event loop.
How it works, what is at stake, the different phases. From timers to close callback to I/O polling. It was going to be very useful on NodeJS. But also on javascript in a general way in the browser.
So, it is important to know that even if the global functioning of the event loop in the browser and in NodeJS is the same, there are differences when zooming. I say this because you will always have a self-proclaimed "expert" to correct you -in an unbearable way- as if it was important.
Anyway, with a little time and crying a little blood, I ended up correcting all the incriminated places. The response time went under 200 ms. And I thought I was done learning the hard way.
Breaking point
A few weeks later, I attended a meeting with my colleagues. It was an important meeting where I was going to discuss technical issues. A new service was planned.
This meeting was going to be the breaking point that would really push me to act.
I hardly mentioned the meeting. Despite my learning about the service, it wasn't enough to keep up. Concepts and technical terms were flying around.
Following the discussion was becoming more and more complicated. Participating in it without saying anything stupid, even more so. It was about closures, generators, the risk of memory leaks and using proxies for advanced monitoring.
None of this was clear in my head. It was time to act to get out of this fog.
Raising your game
When I returned to my post after the meeting, I took my courage in both hands. I asked one of my colleagues for clarification on the content of the meeting. The discussion quickly turned around a book he had read.
My recommendation of the day : Secrets of the Javascript Ninja.
This book is the starting point of all my confidence with Javascript.
By explaining me deeply the internal workings, the behaviors on the surface have become clear. My code became fast and robust. The interview trap questions were easy.
It starts very softly with what happens in the browser with Javascript. Then, he quickly gets to the heart of the matter with the functions. Understanding -really- how they work changes everything.
Then the incredible part about the closures and the functioning of the lexical fields which was a revelation for me.
Then, generators, promises and prototypes. Finally, it ends with a deep dive into the holy event loop that I FINALLY understood. I came out of this book with a clear vision. Ready to fight.
So let's be clear. I have always been very honest with my recommendations. This book is not an easy read.
It's not for you if you're just starting Javascript. There are complex moments where I had to think more, read, reread and look at the diagrams to really get it. But that's what this book is all about.
This book is for those who have been using Javascript for some time and want to raise their game. It is for those who want to dominate this language. It is for those who want to create an expertise.
If it were that simple, everyone would be an expert. This book pushes you into the fog to get you out of it. There is no evolution without friction.
Epilogue
Like many people, I fell into the Javascript trap thinking it was an "easy" language. All my mistakes and painful moments could have been avoided by taking seriously the language learning process beforehand. It's up to you to see if you want to take the risk.
Posted on April 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.