javascript
console.log([] == ![]); // true
Look at that. Drink it in. That right there is the foundational rot of the web. In any sane universe, an empty array compared to the negation of an empty array should return `false`. But JavaScript isn't sane. It’s a series of compromises, accidents, and "good enough" decisions made under the crushing pressure of the 1995 browser wars. Because `![]` coerces to `false`, and an empty array `[]` in a numeric comparison coerces to `0`, and `false` also coerces to `0`, the engine looks at this garbage and gives you a thumbs up.
This is the language that won. Not because it was the best, but because it was there, it was fast enough to ship, and it refused to die. I’ve been staring at this abyss since we were hacking together `document.layers` for Netscape 4, and I’m here to tell you that the "clean code" influencers selling you their $500 courses on "elegant architecture" are lying to you. JavaScript isn't elegant. It’s a dumpster fire that we’ve learned to use as a furnace to power the modern world.
## The 10-Day Sin of Brendan Eich
The mythos says Brendan Eich wrote the first version of JavaScript in ten days. You can feel every one of those ten days in the language's bones. It was originally called Mocha, then LiveScript, then finally JavaScript—a cynical marketing ploy to hitch a ride on the hype train of Sun Microsystems’ Java. It had nothing to do with Java. It was a Scheme-like functional core wrapped in a C-style skin, then beaten with a hammer until it could run inside a browser that crashed if you looked at it sideways.
The biggest sin wasn't the syntax; it was the Global Object. In the early days, everything was a property of `window`. You wanted a variable? It’s on the window. You wanted a function? Window. It was a shared communal toothbrush of a namespace. We didn't have modules. We had `<script>` tags. If you loaded two libraries that both wanted to use the variable `_`, they’d just fight it out in the street, overwriting each other until the page stopped rendering.
What is the fundamental nature of this beast? It’s a language designed to never throw an error if it can possibly guess what you meant. It’s the ultimate "yes man." You want to add a string to a number? Sure, `5 + "5"` is `"55"`. You want to subtract them? Fine, `5 - "5"` is `0`. It’s a loose, permissive nightmare that allowed the web to grow because any idiot could copy-paste a snippet and make a button sparkle, but it created a technical debt that we are still paying off thirty years later.
## The DOM: A Haunted House Built on Sand
Before we had V8 or Node.js, we had the Document Object Model (DOM). If JavaScript is the engine, the DOM is the rusted, jagged chassis it’s bolted to. In the late 90s and early 2000s, the "browser wars" meant that Microsoft and Netscape were actively trying to break each other's implementations.
You didn't write code; you wrote branches. `if (document.all)` for Internet Explorer, `if (document.layers)` for Netscape. It was a fragmented hellscape. The DOM is essentially a tree structure, but it’s a tree that hates you. Every time you touch it, the browser has to recalculate styles, reflow the layout, and repaint the screen. It’s the slowest part of the stack, yet we treat it like a playground.
The "clean code" crowd loves to talk about "separation of concerns," but in the DOM era, your concerns were inseparable from the browser's bugs. We used `innerHTML` because it was fast, even though it was a massive security hole for Cross-Site Scripting (XSS). We used `float: left` for layouts because CSS Grid was a fever dream. We lived in the dirt.
## Closures: The Memory Hostage Crisis
Let’s talk about memory. In a language like C, you manage your own memory. In JS, we have a garbage collector that acts like a distracted landlord. The most powerful and dangerous tool in your belt is the closure.
A closure happens when a function "remembers" its lexical scope even when that function is executed outside that scope. It sounds poetic. In reality, it’s a hostage situation. You’ve got a variable sitting in memory, and as long as some stray function has a reference to it, the garbage collector can’t touch it.
```javascript
function createHostage() {
let massiveData = new Array(1000000).fill("💸");
return function() {
console.log("I still have the money.");
return massiveData[0];
};
}
const ransom = createHostage();
In the IE6 days, closures combined with DOM references were the primary cause of memory leaks that would crawl up and eat your entire system’s RAM. You’d attach an event listener to a button, the listener would close over a large object, and even after the button was removed from the DOM, the object stayed in memory because the listener was still hanging around. It was a slow death by a thousand anonymous functions.
Table of Contents
V8: Putting a Turbocharger on a Lawn Mower
Everything changed in 2008 when Google dropped Chrome and the V8 engine. Before V8, JS was interpreted—the browser read the code line by line and executed it. It was slow. V8 introduced Just-In-Time (JIT) compilation. It took our duct-tape language and compiled it into machine code on the fly.
But to make JS fast, V8 has to make assumptions. It uses “Hidden Classes” (or Shapes). If you create an object {x: 1, y: 2}, V8 creates a hidden class for that shape. If you then create {x: 3, y: 4}, it reuses that class. But if you suddenly decide to do obj.z = 5, you’ve broken the shape. V8 has to dump its optimizations and create a new hidden class.
The “what is” of modern JS performance isn’t about writing clever algorithms; it’s about not upsetting the JIT compiler. It’s about keeping your objects “monomorphic.” If you pass different types of objects into the same function, the engine gives up on optimization and falls back to “deoptimization hell.”
Let’s look at the state of the environment now.
$ node -v
v21.6.1
$ node --max-old-space-size=4096 -e "
const heap = process.memoryUsage();
console.log('--- HEAP SNAPSHOT ---');
for (let key in heap) {
console.log(\`\${key}: \${Math.round(heap[key] / 1024 / 1024 * 100) / 100} MB\`);
}
"
--- HEAP SNAPSHOT ---
rss: 34.21 MB
heapTotal: 4.58 MB
heapUsed: 3.82 MB
external: 1.51 MB
arrayBuffers: 0.02 MB
We’re running servers on this now. We took a language designed to validate form inputs and put it on the backend. Why? Because the “Event Loop” made it easy to handle thousands of concurrent connections without the overhead of threads. Or so the marketing told us.
The Event Loop: The Single-Threaded Lie
The Event Loop is the most misunderstood part of the language. People say JS is “non-blocking.” That’s a lie. JS is single-threaded. If you run a heavy computation, you freeze the entire process. The “non-blocking” part comes from the fact that the engine delegates I/O tasks (like reading a file or making a network request) to the underlying system (via libuv in Node).
When those tasks finish, they drop a callback into the Task Queue. The Event Loop’s only job is to look at the Call Stack, and if it’s empty, push the first thing from the Task Queue onto the stack.
Then came Promises (ES6) and the Microtask Queue. Microtasks have priority over Macrotasks (like setTimeout). If you keep queuing Microtasks, you can starve the Macrotask queue, effectively freezing your “non-blocking” server. It’s a delicate balance of lies.
We moved from Callback Hell to Promise Hell, and finally to async/await (ES2017). Each layer of syntax is just more duct tape over the fact that we’re trying to manage asynchronous state in a language that was never meant to handle it.
Prototypes vs. Classes: The Syntactic Sugar Coating
In ES6, the “suits” decided JavaScript needed to look more like “real” languages, so they gave us the class keyword. It’s a facade. JavaScript doesn’t have classes; it has prototypes. When you “inherit” from a class, you’re just linking objects together in a chain.
If you look at obj.toString(), the engine looks at obj. It’s not there. It looks at obj.__proto__. Not there. It keeps going up the chain until it hits Object.prototype. This is “Prototypal Inheritance.” It’s incredibly flexible and incredibly confusing. You can change the prototype of a base object at runtime and break every single object that inherits from it. It’s like being able to change the DNA of a species while it’s still walking around.
The “clean code” influencers will tell you to use classes for “encapsulation.” But until very recently (ES2020), JS didn’t even have private fields. We used underscores (_privateVar) and hoped nobody would touch them. We were relying on the “honor system” in a production environment.
The Server-Side Takeover: Node.js and the NPM Abyss
When Ryan Dahl released Node.js, he didn’t just give us JS on the server; he gave us NPM (Node Package Manager). NPM is the largest software registry in the world, and it is a terrifying monument to our laziness.
Because the JS standard library is so pathetic—it didn’t even have a way to pad a string until a few years ago—we rely on third-party packages for everything. Remember left-pad? One developer deleted an 11-line function from NPM and half the internet’s build pipelines broke. That is the fragility of our ecosystem.
We have reached a point where a “Hello World” React app requires 300MB of node_modules. We are building skyscrapers on top of a swamp, and our solution is to just keep adding more layers of scaffolding.
$ node -e "console.log(new Error('The Stack Trace of Madness').stack)"
Error: The Stack Trace of Madness
at [eval]:1:13
at Script.runInThisContext (node:vm:129:12)
at Object.runInContext (node:vm:290:6)
at node:internal/process/execution:77:19
at [eval]-wrapper:6:22
at evalScript (node:internal/process/execution:76:60)
at node:internal/main/eval_string:28:3
Look at that stack trace. Half of it is “internal.” When something goes wrong in a modern JS stack, you’re digging through layers of transpiled code (Babel), bundled code (Webpack/Vite), and engine internals. We don’t write JavaScript anymore; we write a dialect that gets compiled into a different dialect that gets optimized into machine code that we hope behaves like the original logic.
The Modern Hellscape: ES2020, ES2023, and Beyond
The language is evolving faster than ever. We got Optional Chaining (?.) and Nullish Coalescing (??) in ES2020, which are basically “don’t crash if this is null” operators. We got Array.prototype.toSorted() in ES2023 because we finally realized that mutating the original array by default was a bad idea.
But every new feature adds to the “flavor” of the pain. We now have two module systems: CommonJS (require) and ECMAScript Modules (import). They don’t play well together. Trying to use an ESM-only package in a CJS project is a rite of passage that involves crying in a dark room at 3 AM while staring at a ERR_REQUIRE_ESM error.
$ node -e "require('some-esm-package')"
Error [ERR_REQUIRE_ESM]: require() of ES Module ... from [eval] not supported.
Instead change the require of index.js in [eval] to a dynamic import() which is available in all CommonJS modules.
at [eval]:1:1 {
code: 'ERR_REQUIRE_ESM'
}
This is the reality. It’s not about “unlocking potential” or “seamless integration.” It’s about fighting the tools every single day. We use TypeScript to add types to a language that was designed to be untyped, and then we spend 40% of our time fighting the compiler because it can’t figure out the complex union of three different API responses.
The Final Verdict: Why We Stay
So why do we do it? Why is JavaScript the most popular language in the world?
Because despite the [] == ![] nonsense, despite the event loop starvation, and despite the node_modules black hole, JavaScript is the only language that is everywhere. It’s the only language that can run on a $20 Android phone in a rural village and a $10,000 server in a data center.
It is the ultimate survivor. It outlasted Flash. It outlasted Silverlight. It outlasted Java Applets. It’s the cockroach of the computing world. You can hate it—I certainly do—but you have to respect its tenacity.
What is JavaScript? It’s the realization that “worse is better.” It’s the proof that if you make something accessible enough, even if it’s broken at the core, the community will build enough workarounds to make it a standard. We’ve built a global economy on a language that thinks typeof null is "object". (Yes, that’s a bug in the original C implementation of the engine that was never fixed because it would “break the web.”)
We are the janitors of this digital wasteland. We keep the lights on with Promises that might never resolve and closures that refuse to let go of the past. We don’t need “clean code.” We need code that survives the next browser update.
So, the next time some junior dev tells you about the “vibrant ecosystem” or the “comprehensive framework” they just found, just show them [] == ![]. Remind them that they are playing in a graveyard. Then, get back to work. There’s a memory leak in production, and the heap is climbing.
$ node --trace-gc -e "let a = []; while(true) a.push(new Array(1000).fill('💀'))"
[14232:0x64f26c0] 16 ms: Scavenge 2.3 (3.2) -> 1.9 (4.2) MB, 0.5 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[14232:0x64f26c0] 24 ms: Scavenge 2.4 (4.2) -> 2.1 (5.2) MB, 0.4 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[14232:0x64f26c0] 35 ms: Scavenge 3.5 (5.2) -> 3.1 (9.2) MB, 0.8 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure;
...
<--- Last several GCs --->
[14232:0x64f26c0] 156 ms: Mark-sweep 482.1 (485.2) -> 481.5 (485.2) MB, 12.4 / 0.0 ms (average mu = 0.142, current mu = 0.001) last resort GC in old space v8::FatalProcessOutOfMemory
Write until it hurts. Then write some more. That’s the JavaScript way. No apologies. No regrets. Just a lot of console.log statements and a prayer that the garbage collector actually does its job for once. The jungle doesn’t care about your “clean code.” It only cares if you can keep the engine running for one more frame.
Related Articles
Explore more insights and best practices: